talon-agent 5.26.1 → 5.26.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/README.md +1 -0
- package/package.json +1 -1
- package/src/app.ts +42 -1
- package/src/bootstrap.ts +171 -57
- package/src/cli/commands/backup.ts +4 -1
- package/src/cli/commands/history.ts +152 -0
- package/src/cli/index.ts +6 -0
- package/src/core/backup/boot/version-checkpoint.ts +199 -0
- package/src/core/backup/index.ts +3 -0
- package/src/core/backup/passphrase.ts +55 -2
- package/src/core/backup/restore.ts +32 -4
- package/src/core/backup/scheduler.ts +125 -13
- package/src/core/backup/status.ts +26 -3
- package/src/core/update/self-update.ts +144 -47
- package/src/frontend/discord/admin.ts +2 -2
- package/src/frontend/discord/callbacks/components/backend-select.ts +4 -4
- package/src/frontend/discord/commands/admin.ts +22 -3
- package/src/frontend/discord/commands/definitions.ts +7 -0
- package/src/frontend/native/chats/chat-lifecycle.ts +5 -2
- package/src/frontend/native/chats/chats.ts +14 -2
- package/src/frontend/native/chats/empty-chat-sweep.ts +4 -1
- package/src/frontend/native/chats/reset.ts +39 -17
- package/src/frontend/native/commands/session.ts +2 -2
- package/src/frontend/native/surface/models.ts +3 -3
- package/src/frontend/native/turn/turn-meta.ts +0 -6
- package/src/frontend/presentation/model-commands.ts +8 -11
- package/src/frontend/presentation/session-status.ts +12 -13
- package/src/frontend/presentation/text-commands.ts +6 -9
- package/src/frontend/telegram/admin/sessions.ts +2 -1
- package/src/frontend/telegram/callbacks/model/backend.ts +5 -6
- package/src/frontend/telegram/commands/admin.ts +21 -3
- package/src/frontend/whatsapp/commands.ts +5 -8
- package/src/storage/backup/index.ts +1 -1
- package/src/storage/chat-settings.ts +13 -0
- package/src/storage/db.ts +5 -0
- package/src/storage/history.ts +167 -17
- package/src/storage/media-index.ts +4 -3
- package/src/storage/repositories/history-repo.ts +75 -7
- package/src/storage/sessions.ts +60 -1
- package/src/storage/sql/history.sql +45 -4
- package/src/storage/sql/schema.sql +13 -0
- package/src/storage/sql/statements.generated.ts +49 -5
- package/src/storage/turn-meta.ts +1 -1
|
@@ -131,11 +131,19 @@ export function insertMany(
|
|
|
131
131
|
});
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
/**
|
|
135
|
-
|
|
134
|
+
/**
|
|
135
|
+
* Most-recent `limit` messages, in chronological order. `floorId` is the
|
|
136
|
+
* chat's context-reset marker: only rows with a larger id are returned
|
|
137
|
+
* (0 = every row).
|
|
138
|
+
*/
|
|
139
|
+
export function recent(
|
|
140
|
+
chatId: string,
|
|
141
|
+
limit: number,
|
|
142
|
+
floorId = 0,
|
|
143
|
+
): HistoryMessage[] {
|
|
136
144
|
const rows = getDatabase()
|
|
137
145
|
.prepare(historySql.recent)
|
|
138
|
-
.all(chatId, limit) as Row[];
|
|
146
|
+
.all(chatId, floorId, limit) as Row[];
|
|
139
147
|
return rows.reverse().map(rowToMessage);
|
|
140
148
|
}
|
|
141
149
|
|
|
@@ -147,10 +155,11 @@ export function recentBefore(
|
|
|
147
155
|
chatId: string,
|
|
148
156
|
beforeMsgId: number,
|
|
149
157
|
limit: number,
|
|
158
|
+
floorId = 0,
|
|
150
159
|
): HistoryMessage[] {
|
|
151
160
|
const rows = getDatabase()
|
|
152
161
|
.prepare(historySql.recentBefore)
|
|
153
|
-
.all(chatId, beforeMsgId, limit) as Row[];
|
|
162
|
+
.all(chatId, beforeMsgId, floorId, limit) as Row[];
|
|
154
163
|
return rows.reverse().map(rowToMessage);
|
|
155
164
|
}
|
|
156
165
|
|
|
@@ -158,10 +167,11 @@ export function recentBeforeTime(
|
|
|
158
167
|
chatId: string,
|
|
159
168
|
beforeTs: number,
|
|
160
169
|
limit: number,
|
|
170
|
+
floorId = 0,
|
|
161
171
|
): HistoryMessage[] {
|
|
162
172
|
const rows = getDatabase()
|
|
163
173
|
.prepare(historySql.recentBeforeTime)
|
|
164
|
-
.all(chatId, beforeTs, limit) as Row[];
|
|
174
|
+
.all(chatId, beforeTs, floorId, limit) as Row[];
|
|
165
175
|
return rows.reverse().map(rowToMessage);
|
|
166
176
|
}
|
|
167
177
|
|
|
@@ -173,8 +183,66 @@ export function setFilePath(
|
|
|
173
183
|
getDatabase().prepare(historySql.setFilePath).run(filePath, chatId, msgId);
|
|
174
184
|
}
|
|
175
185
|
|
|
176
|
-
|
|
177
|
-
|
|
186
|
+
/**
|
|
187
|
+
* Hard-delete a chat's rows and its state. Only the operator's explicit
|
|
188
|
+
* purge reaches this (history.ts purgeChatHistory). Returns rows deleted.
|
|
189
|
+
*/
|
|
190
|
+
export function purgeChat(chatId: string): number {
|
|
191
|
+
return inTransaction(() => {
|
|
192
|
+
const db = getDatabase();
|
|
193
|
+
const result = db.prepare(historySql.purgeChat).run(chatId) as {
|
|
194
|
+
changes: number | bigint;
|
|
195
|
+
};
|
|
196
|
+
db.prepare(historySql.purgeChatState).run(chatId);
|
|
197
|
+
return Number(result.changes);
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** A chat's soft-reset / soft-delete state. */
|
|
202
|
+
export type ChatHistoryState = {
|
|
203
|
+
/** Rows with an id at or under this predate the last context reset. */
|
|
204
|
+
clearedThroughId: number;
|
|
205
|
+
clearedAt?: number;
|
|
206
|
+
hiddenAt?: number;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
export function chatState(chatId: string): ChatHistoryState | undefined {
|
|
210
|
+
const row = getDatabase().prepare(historySql.chatState).get(chatId) as
|
|
211
|
+
| {
|
|
212
|
+
cleared_through_id: number;
|
|
213
|
+
cleared_at: number | null;
|
|
214
|
+
hidden_at: number | null;
|
|
215
|
+
}
|
|
216
|
+
| undefined;
|
|
217
|
+
if (!row) return undefined;
|
|
218
|
+
return {
|
|
219
|
+
clearedThroughId: row.cleared_through_id,
|
|
220
|
+
clearedAt: row.cleared_at ?? undefined,
|
|
221
|
+
hiddenAt: row.hidden_at ?? undefined,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function markCleared(chatId: string, at: number): void {
|
|
226
|
+
getDatabase().prepare(historySql.markCleared).run(chatId, chatId, at);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function markHidden(chatId: string, at: number): void {
|
|
230
|
+
getDatabase().prepare(historySql.markHidden).run(chatId, chatId, at, at);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export type HiddenChat = { chatId: string; hiddenAt: number; total: number };
|
|
234
|
+
|
|
235
|
+
export function hiddenChats(): HiddenChat[] {
|
|
236
|
+
const rows = getDatabase().prepare(historySql.hiddenChats).all() as Array<{
|
|
237
|
+
chat_id: string;
|
|
238
|
+
hidden_at: number;
|
|
239
|
+
total: number;
|
|
240
|
+
}>;
|
|
241
|
+
return rows.map((r) => ({
|
|
242
|
+
chatId: r.chat_id,
|
|
243
|
+
hiddenAt: r.hidden_at,
|
|
244
|
+
total: r.total,
|
|
245
|
+
}));
|
|
178
246
|
}
|
|
179
247
|
|
|
180
248
|
/**
|
package/src/storage/sessions.ts
CHANGED
|
@@ -23,6 +23,7 @@ import { recordError } from "../util/watchdog.js";
|
|
|
23
23
|
import { files } from "../util/paths.js";
|
|
24
24
|
import { importLegacyJson } from "./legacy-import.js";
|
|
25
25
|
import { dbErrorFields } from "./db.js";
|
|
26
|
+
import { kvGet, kvSet } from "./kv.js";
|
|
26
27
|
import * as repo from "./repositories/sessions-repo.js";
|
|
27
28
|
|
|
28
29
|
export type {
|
|
@@ -534,18 +535,76 @@ function removeSessionRow(chatId: string): void {
|
|
|
534
535
|
}
|
|
535
536
|
}
|
|
536
537
|
|
|
538
|
+
// ── Replaced-session archive ───────────────────────────────────────────────
|
|
539
|
+
|
|
540
|
+
/** A backend session id a reset replaced — kept so it can be re-linked. */
|
|
541
|
+
export type ArchivedSession = {
|
|
542
|
+
chatId: string;
|
|
543
|
+
sessionId: string;
|
|
544
|
+
/** Why the session was replaced ("reset", "backend-unavailable", …). */
|
|
545
|
+
reason: string;
|
|
546
|
+
/** When it was replaced (ms epoch). */
|
|
547
|
+
at: number;
|
|
548
|
+
turns: number;
|
|
549
|
+
lastModel?: string;
|
|
550
|
+
sessionName?: string;
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
const SESSION_ARCHIVE_KEY = "sessions.archive";
|
|
554
|
+
/** Newest entries kept; the transcripts themselves stay on disk regardless. */
|
|
555
|
+
const SESSION_ARCHIVE_MAX = 500;
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Remember a session id that is about to be dropped. A reset used to
|
|
559
|
+
* forget it outright, leaving the backend transcript on disk with nothing
|
|
560
|
+
* pointing at it; with the id kept, a chat reset by mistake (or by a boot
|
|
561
|
+
* reconcile) can be re-linked to its old conversation. Never throws.
|
|
562
|
+
*/
|
|
563
|
+
function archiveSessionId(entry: ArchivedSession): void {
|
|
564
|
+
try {
|
|
565
|
+
const prior = kvGet<ArchivedSession[]>(SESSION_ARCHIVE_KEY);
|
|
566
|
+
const list = Array.isArray(prior) ? prior : [];
|
|
567
|
+
list.push(entry);
|
|
568
|
+
kvSet(SESSION_ARCHIVE_KEY, list.slice(-SESSION_ARCHIVE_MAX));
|
|
569
|
+
} catch (err) {
|
|
570
|
+
logError("sessions", `Failed to archive session chat=${entry.chatId}`, err);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/** Archived (replaced) session ids, oldest first; one chat's when given. */
|
|
575
|
+
export function getArchivedSessions(chatId?: string): ArchivedSession[] {
|
|
576
|
+
const list = kvGet<ArchivedSession[]>(SESSION_ARCHIVE_KEY);
|
|
577
|
+
if (!Array.isArray(list)) return [];
|
|
578
|
+
return chatId ? list.filter((e) => e.chatId === chatId) : list;
|
|
579
|
+
}
|
|
580
|
+
|
|
537
581
|
/**
|
|
538
582
|
* Reset the chat's conversation state (backend session id, turns, usage)
|
|
539
583
|
* while carrying its metrics forward. Resets fire on /new, model switches
|
|
540
584
|
* and error recovery — none of which should erase the chat's accounting
|
|
541
585
|
* history (that's what makes per-session metrics survive anything short
|
|
542
586
|
* of deleting the chat). Use deleteSession() to drop the chat entirely.
|
|
587
|
+
*
|
|
588
|
+
* The replaced backend session id is archived (see getArchivedSessions),
|
|
589
|
+
* never just discarded. Talon's own chat history is untouched: clearing
|
|
590
|
+
* it is a separate, explicit call.
|
|
543
591
|
*/
|
|
544
|
-
export function resetSession(chatId: string): void {
|
|
592
|
+
export function resetSession(chatId: string, reason = "reset"): void {
|
|
545
593
|
const session = cache.get(chatId);
|
|
546
594
|
const turns = session?.turns ?? 0;
|
|
547
595
|
const name = session?.sessionName;
|
|
548
596
|
const metrics = session?.metrics;
|
|
597
|
+
if (session?.sessionId) {
|
|
598
|
+
archiveSessionId({
|
|
599
|
+
chatId,
|
|
600
|
+
sessionId: session.sessionId,
|
|
601
|
+
reason,
|
|
602
|
+
at: Date.now(),
|
|
603
|
+
turns,
|
|
604
|
+
...(session.lastModel ? { lastModel: session.lastModel } : {}),
|
|
605
|
+
...(name ? { sessionName: name } : {}),
|
|
606
|
+
});
|
|
607
|
+
}
|
|
549
608
|
removeSessionRow(chatId);
|
|
550
609
|
const hasHistory =
|
|
551
610
|
metrics &&
|
|
@@ -9,10 +9,13 @@ INSERT OR IGNORE INTO history_messages
|
|
|
9
9
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
10
10
|
|
|
11
11
|
-- name: recent
|
|
12
|
+
-- The `id > ?` floor is the chat's context-reset marker (0 for none; see
|
|
13
|
+
-- chatState below): rows at or under it stay stored and searchable but
|
|
14
|
+
-- are no longer the chat's current conversation.
|
|
12
15
|
SELECT msg_id, sender_id, sender_name, sender_handle, text, reply_to_msg_id,
|
|
13
16
|
timestamp, media_type, sticker_file_id, file_path, attachments
|
|
14
17
|
FROM history_messages
|
|
15
|
-
WHERE chat_id = ? ORDER BY id DESC LIMIT ?
|
|
18
|
+
WHERE chat_id = ? AND id > ? ORDER BY id DESC LIMIT ?
|
|
16
19
|
|
|
17
20
|
-- name: recentBefore
|
|
18
21
|
-- Scroll-back pagination: the window of messages strictly older than a
|
|
@@ -20,7 +23,7 @@ WHERE chat_id = ? ORDER BY id DESC LIMIT ?
|
|
|
20
23
|
SELECT msg_id, sender_id, sender_name, sender_handle, text, reply_to_msg_id,
|
|
21
24
|
timestamp, media_type, sticker_file_id, file_path, attachments
|
|
22
25
|
FROM history_messages
|
|
23
|
-
WHERE chat_id = ? AND msg_id < ? ORDER BY id DESC LIMIT ?
|
|
26
|
+
WHERE chat_id = ? AND msg_id < ? AND id > ? ORDER BY id DESC LIMIT ?
|
|
24
27
|
|
|
25
28
|
-- name: recentBeforeTime
|
|
26
29
|
-- Time-cursor variant of recentBefore for the read_history `before` date
|
|
@@ -28,14 +31,52 @@ WHERE chat_id = ? AND msg_id < ? ORDER BY id DESC LIMIT ?
|
|
|
28
31
|
SELECT msg_id, sender_id, sender_name, sender_handle, text, reply_to_msg_id,
|
|
29
32
|
timestamp, media_type, sticker_file_id, file_path, attachments
|
|
30
33
|
FROM history_messages
|
|
31
|
-
WHERE chat_id = ? AND timestamp < ? ORDER BY id DESC LIMIT ?
|
|
34
|
+
WHERE chat_id = ? AND timestamp < ? AND id > ? ORDER BY id DESC LIMIT ?
|
|
32
35
|
|
|
33
36
|
-- name: setFilePath
|
|
34
37
|
UPDATE history_messages SET file_path = ? WHERE chat_id = ? AND msg_id = ?
|
|
35
38
|
|
|
36
|
-
-- name:
|
|
39
|
+
-- name: purgeChat
|
|
40
|
+
-- The ONLY statement that deletes history rows. Reached solely through the
|
|
41
|
+
-- operator's explicit `talon history purge` (history.ts purgeChatHistory);
|
|
42
|
+
-- resets, backend switches and chat deletion never delete rows.
|
|
37
43
|
DELETE FROM history_messages WHERE chat_id = ?
|
|
38
44
|
|
|
45
|
+
-- name: purgeChatState
|
|
46
|
+
DELETE FROM history_chat_state WHERE chat_id = ?
|
|
47
|
+
|
|
48
|
+
-- name: chatState
|
|
49
|
+
SELECT cleared_through_id, cleared_at, hidden_at
|
|
50
|
+
FROM history_chat_state WHERE chat_id = ?
|
|
51
|
+
|
|
52
|
+
-- name: markCleared
|
|
53
|
+
-- Soft reset: move the chat's context floor to its newest stored row. The
|
|
54
|
+
-- rows stay; readers that build the bot's context skip everything at or
|
|
55
|
+
-- under the floor. Parameters: chat_id, chat_id, cleared_at.
|
|
56
|
+
INSERT INTO history_chat_state (chat_id, cleared_through_id, cleared_at)
|
|
57
|
+
VALUES (?, (SELECT COALESCE(MAX(id), 0) FROM history_messages WHERE chat_id = ?), ?)
|
|
58
|
+
ON CONFLICT(chat_id) DO UPDATE SET
|
|
59
|
+
cleared_through_id = excluded.cleared_through_id,
|
|
60
|
+
cleared_at = excluded.cleared_at
|
|
61
|
+
|
|
62
|
+
-- name: markHidden
|
|
63
|
+
-- Soft delete: a chat the user deleted is hidden (and its context floor
|
|
64
|
+
-- moved, so a chat that reappears under the same id starts fresh). The
|
|
65
|
+
-- rows stay. Parameters: chat_id, chat_id, cleared_at, hidden_at.
|
|
66
|
+
INSERT INTO history_chat_state (chat_id, cleared_through_id, cleared_at, hidden_at)
|
|
67
|
+
VALUES (?, (SELECT COALESCE(MAX(id), 0) FROM history_messages WHERE chat_id = ?), ?, ?)
|
|
68
|
+
ON CONFLICT(chat_id) DO UPDATE SET
|
|
69
|
+
cleared_through_id = excluded.cleared_through_id,
|
|
70
|
+
cleared_at = excluded.cleared_at,
|
|
71
|
+
hidden_at = excluded.hidden_at
|
|
72
|
+
|
|
73
|
+
-- name: hiddenChats
|
|
74
|
+
SELECT s.chat_id, s.hidden_at,
|
|
75
|
+
(SELECT COUNT(*) FROM history_messages h WHERE h.chat_id = s.chat_id) AS total
|
|
76
|
+
FROM history_chat_state s
|
|
77
|
+
WHERE s.hidden_at IS NOT NULL
|
|
78
|
+
ORDER BY s.hidden_at DESC
|
|
79
|
+
|
|
39
80
|
-- name: searchFts
|
|
40
81
|
-- The match param must already be a valid FTS5 expression
|
|
41
82
|
-- (see history.ts ftsQuery).
|
|
@@ -52,6 +52,19 @@ CREATE TRIGGER IF NOT EXISTS history_au AFTER UPDATE OF text, sender_name ON his
|
|
|
52
52
|
VALUES (new.id, new.text, new.sender_name);
|
|
53
53
|
END;
|
|
54
54
|
|
|
55
|
+
-- Per-chat history state. Chat history is never deleted by a reset, a
|
|
56
|
+
-- backend switch or a chat deletion: a reset records a context floor
|
|
57
|
+
-- (`cleared_through_id`, the newest history_messages.id at reset time) so
|
|
58
|
+
-- the bot's context starts fresh after it while every row stays stored
|
|
59
|
+
-- and searchable; deleting a chat in a client sets `hidden_at`. Only the
|
|
60
|
+
-- operator's explicit `talon history purge` removes rows.
|
|
61
|
+
CREATE TABLE IF NOT EXISTS history_chat_state (
|
|
62
|
+
chat_id TEXT PRIMARY KEY,
|
|
63
|
+
cleared_through_id INTEGER NOT NULL DEFAULT 0,
|
|
64
|
+
cleared_at INTEGER,
|
|
65
|
+
hidden_at INTEGER
|
|
66
|
+
);
|
|
67
|
+
|
|
55
68
|
-- Typed memory: one row per claim, with an FTS5 index over subject +
|
|
56
69
|
-- text. Kinds are lifecycles, not labels (docs/memory-persona-plan.md
|
|
57
70
|
-- §3.1): `directive` is durable human intent, `fact` is durable and
|
|
@@ -57,6 +57,19 @@ CREATE TRIGGER IF NOT EXISTS history_au AFTER UPDATE OF text, sender_name ON his
|
|
|
57
57
|
VALUES (new.id, new.text, new.sender_name);
|
|
58
58
|
END;
|
|
59
59
|
|
|
60
|
+
-- Per-chat history state. Chat history is never deleted by a reset, a
|
|
61
|
+
-- backend switch or a chat deletion: a reset records a context floor
|
|
62
|
+
-- (\`cleared_through_id\`, the newest history_messages.id at reset time) so
|
|
63
|
+
-- the bot's context starts fresh after it while every row stays stored
|
|
64
|
+
-- and searchable; deleting a chat in a client sets \`hidden_at\`. Only the
|
|
65
|
+
-- operator's explicit \`talon history purge\` removes rows.
|
|
66
|
+
CREATE TABLE IF NOT EXISTS history_chat_state (
|
|
67
|
+
chat_id TEXT PRIMARY KEY,
|
|
68
|
+
cleared_through_id INTEGER NOT NULL DEFAULT 0,
|
|
69
|
+
cleared_at INTEGER,
|
|
70
|
+
hidden_at INTEGER
|
|
71
|
+
);
|
|
72
|
+
|
|
60
73
|
-- Typed memory: one row per claim, with an FTS5 index over subject +
|
|
61
74
|
-- text. Kinds are lifecycles, not labels (docs/memory-persona-plan.md
|
|
62
75
|
-- §3.1): \`directive\` is durable human intent, \`fact\` is durable and
|
|
@@ -518,24 +531,55 @@ export const historySql = {
|
|
|
518
531
|
reply_to_msg_id, timestamp, media_type, sticker_file_id, file_path,
|
|
519
532
|
attachments)
|
|
520
533
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
521
|
-
recent:
|
|
534
|
+
recent: `-- The \`id > ?\` floor is the chat's context-reset marker (0 for none; see
|
|
535
|
+
-- chatState below): rows at or under it stay stored and searchable but
|
|
536
|
+
-- are no longer the chat's current conversation.
|
|
537
|
+
SELECT msg_id, sender_id, sender_name, sender_handle, text, reply_to_msg_id,
|
|
522
538
|
timestamp, media_type, sticker_file_id, file_path, attachments
|
|
523
539
|
FROM history_messages
|
|
524
|
-
WHERE chat_id = ? ORDER BY id DESC LIMIT ?`,
|
|
540
|
+
WHERE chat_id = ? AND id > ? ORDER BY id DESC LIMIT ?`,
|
|
525
541
|
recentBefore: `-- Scroll-back pagination: the window of messages strictly older than a
|
|
526
542
|
-- given msg_id, newest-first (the repository reverses to chronological).
|
|
527
543
|
SELECT msg_id, sender_id, sender_name, sender_handle, text, reply_to_msg_id,
|
|
528
544
|
timestamp, media_type, sticker_file_id, file_path, attachments
|
|
529
545
|
FROM history_messages
|
|
530
|
-
WHERE chat_id = ? AND msg_id < ? ORDER BY id DESC LIMIT ?`,
|
|
546
|
+
WHERE chat_id = ? AND msg_id < ? AND id > ? ORDER BY id DESC LIMIT ?`,
|
|
531
547
|
recentBeforeTime: `-- Time-cursor variant of recentBefore for the read_history \`before\` date
|
|
532
548
|
-- parameter: the newest \`limit\` messages strictly older than a timestamp.
|
|
533
549
|
SELECT msg_id, sender_id, sender_name, sender_handle, text, reply_to_msg_id,
|
|
534
550
|
timestamp, media_type, sticker_file_id, file_path, attachments
|
|
535
551
|
FROM history_messages
|
|
536
|
-
WHERE chat_id = ? AND timestamp < ? ORDER BY id DESC LIMIT ?`,
|
|
552
|
+
WHERE chat_id = ? AND timestamp < ? AND id > ? ORDER BY id DESC LIMIT ?`,
|
|
537
553
|
setFilePath: `UPDATE history_messages SET file_path = ? WHERE chat_id = ? AND msg_id = ?`,
|
|
538
|
-
|
|
554
|
+
purgeChat: `-- The ONLY statement that deletes history rows. Reached solely through the
|
|
555
|
+
-- operator's explicit \`talon history purge\` (history.ts purgeChatHistory);
|
|
556
|
+
-- resets, backend switches and chat deletion never delete rows.
|
|
557
|
+
DELETE FROM history_messages WHERE chat_id = ?`,
|
|
558
|
+
purgeChatState: `DELETE FROM history_chat_state WHERE chat_id = ?`,
|
|
559
|
+
chatState: `SELECT cleared_through_id, cleared_at, hidden_at
|
|
560
|
+
FROM history_chat_state WHERE chat_id = ?`,
|
|
561
|
+
markCleared: `-- Soft reset: move the chat's context floor to its newest stored row. The
|
|
562
|
+
-- rows stay; readers that build the bot's context skip everything at or
|
|
563
|
+
-- under the floor. Parameters: chat_id, chat_id, cleared_at.
|
|
564
|
+
INSERT INTO history_chat_state (chat_id, cleared_through_id, cleared_at)
|
|
565
|
+
VALUES (?, (SELECT COALESCE(MAX(id), 0) FROM history_messages WHERE chat_id = ?), ?)
|
|
566
|
+
ON CONFLICT(chat_id) DO UPDATE SET
|
|
567
|
+
cleared_through_id = excluded.cleared_through_id,
|
|
568
|
+
cleared_at = excluded.cleared_at`,
|
|
569
|
+
markHidden: `-- Soft delete: a chat the user deleted is hidden (and its context floor
|
|
570
|
+
-- moved, so a chat that reappears under the same id starts fresh). The
|
|
571
|
+
-- rows stay. Parameters: chat_id, chat_id, cleared_at, hidden_at.
|
|
572
|
+
INSERT INTO history_chat_state (chat_id, cleared_through_id, cleared_at, hidden_at)
|
|
573
|
+
VALUES (?, (SELECT COALESCE(MAX(id), 0) FROM history_messages WHERE chat_id = ?), ?, ?)
|
|
574
|
+
ON CONFLICT(chat_id) DO UPDATE SET
|
|
575
|
+
cleared_through_id = excluded.cleared_through_id,
|
|
576
|
+
cleared_at = excluded.cleared_at,
|
|
577
|
+
hidden_at = excluded.hidden_at`,
|
|
578
|
+
hiddenChats: `SELECT s.chat_id, s.hidden_at,
|
|
579
|
+
(SELECT COUNT(*) FROM history_messages h WHERE h.chat_id = s.chat_id) AS total
|
|
580
|
+
FROM history_chat_state s
|
|
581
|
+
WHERE s.hidden_at IS NOT NULL
|
|
582
|
+
ORDER BY s.hidden_at DESC`,
|
|
539
583
|
searchFts: `-- The match param must already be a valid FTS5 expression
|
|
540
584
|
-- (see history.ts ftsQuery).
|
|
541
585
|
SELECT msg_id, sender_id, sender_name, sender_handle, text, reply_to_msg_id,
|
package/src/storage/turn-meta.ts
CHANGED
|
@@ -121,7 +121,7 @@ export function getTurnMeta<T>(chatId: string, msgId: string): T | null {
|
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
/** Forget a chat
|
|
124
|
+
/** Forget a chat's turn meta — only the operator's history purge does. */
|
|
125
125
|
export function clearTurnMeta(chatId: string): void {
|
|
126
126
|
ensureLoaded();
|
|
127
127
|
try {
|