talon-agent 3.11.1 → 3.12.0
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/package.json +1 -1
- package/prompts/discord.md +1 -1
- package/prompts/telegram.md +1 -1
- package/src/backend/claude-sdk/handler.ts +2 -1
- package/src/backend/codex/handler/message.ts +2 -0
- package/src/backend/kilo/handler/message.ts +2 -0
- package/src/backend/openai-agents/handler/message.ts +2 -0
- package/src/backend/opencode/handler/message.ts +2 -0
- package/src/backend/shared/handler-to-events.ts +1 -0
- package/src/backend/shared/handler-types.ts +2 -0
- package/src/backend/shared/prompt-format.ts +13 -6
- package/src/core/agent-runtime/capabilities.ts +2 -0
- package/src/core/background/pulse.ts +4 -1
- package/src/core/types.ts +2 -0
- package/src/core/weaver/weaver.ts +1 -0
- package/src/frontend/discord/handlers/delivery.ts +1 -0
- package/src/frontend/discord/middleware.ts +5 -0
- package/src/frontend/telegram/handlers/delivery.ts +1 -0
- package/src/frontend/telegram/middleware.ts +15 -0
- package/src/storage/db.ts +5 -0
- package/src/storage/history.ts +8 -2
- package/src/storage/repositories/history-repo.ts +13 -0
- package/src/storage/sql/db.sql +5 -0
- package/src/storage/sql/history.sql +16 -10
- package/src/storage/sql/schema.sql +4 -0
- package/src/storage/sql/statements.generated.ts +23 -10
package/package.json
CHANGED
package/prompts/discord.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
## Discord Mode
|
|
2
2
|
|
|
3
|
-
In servers (guilds), you'll see messages prefixed with [Name]
|
|
3
|
+
In servers (guilds), you'll see messages prefixed with `[Name (@handle)]:` — use their name naturally in prose, and the `@handle` when you need to address someone directly. In DMs, just one user.
|
|
4
4
|
|
|
5
5
|
How replies are delivered (end_turn / send / react and what counts as a valid turn) is defined in the **Response flow** section at the end of these instructions — that contract wins over anything here.
|
|
6
6
|
|
package/prompts/telegram.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
## Telegram Mode
|
|
2
2
|
|
|
3
|
-
In groups, you'll see messages prefixed with [Name]
|
|
3
|
+
In groups, you'll see messages prefixed with `[Name (@handle)]:` — use their name naturally in prose. The `@handle` is their real Telegram username: use it verbatim (with the `@`) whenever you actually need to ping someone, and especially in scheduled/cron messages, where a display name is just inert text and will not notify anyone. A sender with no username shows as `[Name]:` and simply can't be pinged.
|
|
4
4
|
|
|
5
5
|
How replies are delivered (end_turn / send / react and what counts as a valid turn) is defined in the **Response flow** section at the end of these instructions — that contract wins over anything here.
|
|
6
6
|
|
|
@@ -151,7 +151,7 @@ export async function* runChatTurn(
|
|
|
151
151
|
): AsyncIterable<AgentEvent> {
|
|
152
152
|
const config = getConfig();
|
|
153
153
|
|
|
154
|
-
const { chatId, text, senderName, isGroup } = params;
|
|
154
|
+
const { chatId, text, senderName, senderHandle, isGroup } = params;
|
|
155
155
|
const session = getSession(chatId);
|
|
156
156
|
const t0 = Date.now();
|
|
157
157
|
|
|
@@ -196,6 +196,7 @@ export async function* runChatTurn(
|
|
|
196
196
|
let prompt = formatUserPrompt({
|
|
197
197
|
text,
|
|
198
198
|
senderName: senderName ?? "user",
|
|
199
|
+
senderHandle,
|
|
199
200
|
isGroup,
|
|
200
201
|
messageId: params.messageId,
|
|
201
202
|
});
|
|
@@ -200,6 +200,7 @@ export async function handleMessage(
|
|
|
200
200
|
chatId,
|
|
201
201
|
text,
|
|
202
202
|
senderName,
|
|
203
|
+
senderHandle,
|
|
203
204
|
isGroup,
|
|
204
205
|
messageId,
|
|
205
206
|
onTextBlock,
|
|
@@ -261,6 +262,7 @@ export async function handleMessage(
|
|
|
261
262
|
const prompt = formatUserPrompt({
|
|
262
263
|
text,
|
|
263
264
|
senderName: senderName ?? "user",
|
|
265
|
+
senderHandle,
|
|
264
266
|
isGroup,
|
|
265
267
|
messageId,
|
|
266
268
|
});
|
|
@@ -61,6 +61,7 @@ export async function handleMessage(
|
|
|
61
61
|
chatId,
|
|
62
62
|
text,
|
|
63
63
|
senderName,
|
|
64
|
+
senderHandle,
|
|
64
65
|
isGroup,
|
|
65
66
|
messageId,
|
|
66
67
|
onTextBlock,
|
|
@@ -96,6 +97,7 @@ export async function handleMessage(
|
|
|
96
97
|
const prompt = formatUserPrompt({
|
|
97
98
|
text,
|
|
98
99
|
senderName: senderName ?? "user",
|
|
100
|
+
senderHandle,
|
|
99
101
|
isGroup,
|
|
100
102
|
messageId,
|
|
101
103
|
});
|
|
@@ -105,6 +105,7 @@ export async function handleMessage(
|
|
|
105
105
|
chatId,
|
|
106
106
|
text,
|
|
107
107
|
senderName,
|
|
108
|
+
senderHandle,
|
|
108
109
|
isGroup,
|
|
109
110
|
messageId,
|
|
110
111
|
onTextBlock,
|
|
@@ -143,6 +144,7 @@ export async function handleMessage(
|
|
|
143
144
|
let prompt = formatUserPrompt({
|
|
144
145
|
text,
|
|
145
146
|
senderName: senderName ?? "user",
|
|
147
|
+
senderHandle,
|
|
146
148
|
isGroup,
|
|
147
149
|
messageId,
|
|
148
150
|
});
|
|
@@ -62,6 +62,7 @@ export async function handleMessage(
|
|
|
62
62
|
chatId,
|
|
63
63
|
text,
|
|
64
64
|
senderName,
|
|
65
|
+
senderHandle,
|
|
65
66
|
isGroup,
|
|
66
67
|
messageId,
|
|
67
68
|
onTextBlock,
|
|
@@ -93,6 +94,7 @@ export async function handleMessage(
|
|
|
93
94
|
const prompt = formatUserPrompt({
|
|
94
95
|
text,
|
|
95
96
|
senderName: senderName ?? "user",
|
|
97
|
+
senderHandle,
|
|
96
98
|
isGroup,
|
|
97
99
|
messageId,
|
|
98
100
|
});
|
|
@@ -28,6 +28,8 @@ export type QueryParams = {
|
|
|
28
28
|
model?: string;
|
|
29
29
|
text: string;
|
|
30
30
|
senderName: string;
|
|
31
|
+
/** Sender's platform handle without `@` (Telegram username, Discord username). */
|
|
32
|
+
senderHandle?: string;
|
|
31
33
|
isGroup?: boolean;
|
|
32
34
|
/**
|
|
33
35
|
* Provider message ID. Telegram is numeric; Discord snowflakes are strings.
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* doesn't" drift that motivated the shared-framework refactor.
|
|
11
11
|
*
|
|
12
12
|
* Examples:
|
|
13
|
-
* group: "[2026-05-15 11:01:23] [Dylan] [msg_id:2485]: actual text"
|
|
13
|
+
* group: "[2026-05-15 11:01:23] [Dylan (@dylanneve1)] [msg_id:2485]: actual text"
|
|
14
14
|
* DM: "[2026-05-15 11:01:23] [msg_id:2485] actual text"
|
|
15
15
|
* DM (no msg_id): "[2026-05-15 11:01:23] actual text"
|
|
16
16
|
*/
|
|
@@ -25,6 +25,13 @@ export type PromptFormatInputs = {
|
|
|
25
25
|
text: string;
|
|
26
26
|
/** Display name of the sender (e.g. "Dylan"). */
|
|
27
27
|
senderName: string;
|
|
28
|
+
/**
|
|
29
|
+
* Platform handle of the sender WITHOUT the leading `@` (e.g. `dylanneve1`).
|
|
30
|
+
* Rendered next to the display name in group chats so the model can address
|
|
31
|
+
* or mention someone correctly — display names are not addressable, handles
|
|
32
|
+
* are. Absent for users who have no handle set.
|
|
33
|
+
*/
|
|
34
|
+
senderHandle?: string;
|
|
28
35
|
/** True when the chat is a group; influences whether the `[Name]` label is included. */
|
|
29
36
|
isGroup?: boolean;
|
|
30
37
|
/** Provider message id (Telegram numeric, Discord snowflake string). */
|
|
@@ -47,11 +54,11 @@ export function formatUserPrompt(inputs: PromptFormatInputs): string {
|
|
|
47
54
|
inputs.messageId !== undefined ? ` [msg_id:${inputs.messageId}]` : "";
|
|
48
55
|
|
|
49
56
|
if (inputs.isGroup) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
inputs.
|
|
54
|
-
);
|
|
57
|
+
const handle = inputs.senderHandle?.trim().replace(/^@+/, "");
|
|
58
|
+
const who = handle
|
|
59
|
+
? `${inputs.senderName} (@${handle})`
|
|
60
|
+
: inputs.senderName;
|
|
61
|
+
return joinNonEmpty(timeTag, `[${who}]${msgIdHint}:`, inputs.text);
|
|
55
62
|
}
|
|
56
63
|
|
|
57
64
|
// DM: no [Name] label needed (Telegram already shows sender)
|
|
@@ -51,6 +51,8 @@ export interface ChatRunParams {
|
|
|
51
51
|
model: ModelRef;
|
|
52
52
|
text: string;
|
|
53
53
|
senderName: string;
|
|
54
|
+
/** Sender's platform handle without `@` (Telegram username, Discord username). */
|
|
55
|
+
senderHandle?: string;
|
|
54
56
|
isGroup?: boolean;
|
|
55
57
|
/** Provider message ID. Telegram is numeric; Discord snowflakes are strings. */
|
|
56
58
|
messageId?: number | string;
|
|
@@ -147,7 +147,10 @@ async function pulseChat(chatId: string): Promise<void> {
|
|
|
147
147
|
.map((m) => {
|
|
148
148
|
const time = formatSmartTimestamp(m.timestamp);
|
|
149
149
|
const media = m.mediaType ? ` [${m.mediaType}]` : "";
|
|
150
|
-
|
|
150
|
+
const who = m.senderHandle
|
|
151
|
+
? `${m.senderName} (@${m.senderHandle})`
|
|
152
|
+
: m.senderName;
|
|
153
|
+
return `[msg:${m.msgId} ${time}] ${who}${media}: ${m.text.slice(0, 200)}`;
|
|
151
154
|
})
|
|
152
155
|
.join("\n");
|
|
153
156
|
|
package/src/core/types.ts
CHANGED
|
@@ -214,6 +214,8 @@ export type ExecuteParams = {
|
|
|
214
214
|
numericChatId: number;
|
|
215
215
|
prompt: string;
|
|
216
216
|
senderName: string;
|
|
217
|
+
/** Sender's platform handle without `@` (Telegram username, Discord username). */
|
|
218
|
+
senderHandle?: string;
|
|
217
219
|
isGroup: boolean;
|
|
218
220
|
/** Provider message ID. Numeric for Telegram, string snowflake for Discord. */
|
|
219
221
|
messageId?: number | string;
|
|
@@ -52,6 +52,7 @@ export async function processAndReply(p: ProcessAndReplyParams): Promise<void> {
|
|
|
52
52
|
numericChatId: p.numericChatId,
|
|
53
53
|
prompt: p.prompt,
|
|
54
54
|
senderName: p.senderName,
|
|
55
|
+
senderHandle: p.senderUsername,
|
|
55
56
|
isGroup: p.isGroup,
|
|
56
57
|
// Use the real Discord snowflake string, not the hashed numeric.
|
|
57
58
|
// The hash collides with Telegram-style 32-bit IDs and Discord's API
|
|
@@ -40,6 +40,9 @@ export function registerMiddleware(client: Client, config: TalonConfig): void {
|
|
|
40
40
|
const numericMessageId = deriveNumericChatId(msg.id);
|
|
41
41
|
const senderId = deriveNumericChatId(msg.author.id);
|
|
42
42
|
const senderName = getSenderName(msg);
|
|
43
|
+
// Display names aren't addressable; the username is. Persist it so
|
|
44
|
+
// later readers can mention the person rather than guess a handle.
|
|
45
|
+
const senderHandle = msg.author.username;
|
|
43
46
|
const replyToMsgId = msg.reference?.messageId
|
|
44
47
|
? deriveNumericChatId(msg.reference.messageId)
|
|
45
48
|
: undefined;
|
|
@@ -59,6 +62,7 @@ export function registerMiddleware(client: Client, config: TalonConfig): void {
|
|
|
59
62
|
msgId: numericMessageId,
|
|
60
63
|
senderId,
|
|
61
64
|
senderName,
|
|
65
|
+
senderHandle,
|
|
62
66
|
text: msg.content || `(${mediaType})`,
|
|
63
67
|
replyToMsgId,
|
|
64
68
|
timestamp,
|
|
@@ -69,6 +73,7 @@ export function registerMiddleware(client: Client, config: TalonConfig): void {
|
|
|
69
73
|
msgId: numericMessageId,
|
|
70
74
|
senderId,
|
|
71
75
|
senderName,
|
|
76
|
+
senderHandle,
|
|
72
77
|
text: msg.content,
|
|
73
78
|
replyToMsgId,
|
|
74
79
|
timestamp,
|
|
@@ -28,6 +28,10 @@ export function registerMiddleware(bot: Bot, config: TalonConfig): void {
|
|
|
28
28
|
bot.on("message", (ctx, next) => {
|
|
29
29
|
const chatId = String(ctx.chat.id);
|
|
30
30
|
const sender = getSenderName(ctx.from);
|
|
31
|
+
// The handle is the only addressable form of a user — persist it with
|
|
32
|
+
// every row so later readers (history views, heartbeat-composed
|
|
33
|
+
// messages) can mention someone instead of guessing.
|
|
34
|
+
const senderHandle = ctx.from?.username;
|
|
31
35
|
const senderId = ctx.from?.id ?? 0;
|
|
32
36
|
const msgId = ctx.message.message_id;
|
|
33
37
|
const replyToMsgId = ctx.message.reply_to_message?.message_id;
|
|
@@ -44,6 +48,7 @@ export function registerMiddleware(bot: Bot, config: TalonConfig): void {
|
|
|
44
48
|
msgId,
|
|
45
49
|
senderId,
|
|
46
50
|
senderName: sender,
|
|
51
|
+
senderHandle,
|
|
47
52
|
text: ctx.message.text,
|
|
48
53
|
replyToMsgId,
|
|
49
54
|
timestamp,
|
|
@@ -53,6 +58,7 @@ export function registerMiddleware(bot: Bot, config: TalonConfig): void {
|
|
|
53
58
|
msgId,
|
|
54
59
|
senderId,
|
|
55
60
|
senderName: sender,
|
|
61
|
+
senderHandle,
|
|
56
62
|
text: ctx.message.caption || "(photo)",
|
|
57
63
|
replyToMsgId,
|
|
58
64
|
timestamp,
|
|
@@ -64,6 +70,7 @@ export function registerMiddleware(bot: Bot, config: TalonConfig): void {
|
|
|
64
70
|
msgId,
|
|
65
71
|
senderId,
|
|
66
72
|
senderName: sender,
|
|
73
|
+
senderHandle,
|
|
67
74
|
text: ctx.message.caption || `(sent ${name})`,
|
|
68
75
|
replyToMsgId,
|
|
69
76
|
timestamp,
|
|
@@ -74,6 +81,7 @@ export function registerMiddleware(bot: Bot, config: TalonConfig): void {
|
|
|
74
81
|
msgId,
|
|
75
82
|
senderId,
|
|
76
83
|
senderName: sender,
|
|
84
|
+
senderHandle,
|
|
77
85
|
text: "(voice message)",
|
|
78
86
|
replyToMsgId,
|
|
79
87
|
timestamp,
|
|
@@ -84,6 +92,7 @@ export function registerMiddleware(bot: Bot, config: TalonConfig): void {
|
|
|
84
92
|
msgId,
|
|
85
93
|
senderId,
|
|
86
94
|
senderName: sender,
|
|
95
|
+
senderHandle,
|
|
87
96
|
text: ctx.message.sticker.emoji || "(sticker)",
|
|
88
97
|
replyToMsgId,
|
|
89
98
|
timestamp,
|
|
@@ -95,6 +104,7 @@ export function registerMiddleware(bot: Bot, config: TalonConfig): void {
|
|
|
95
104
|
msgId,
|
|
96
105
|
senderId,
|
|
97
106
|
senderName: sender,
|
|
107
|
+
senderHandle,
|
|
98
108
|
text: ctx.message.caption || "(video)",
|
|
99
109
|
replyToMsgId,
|
|
100
110
|
timestamp,
|
|
@@ -105,6 +115,7 @@ export function registerMiddleware(bot: Bot, config: TalonConfig): void {
|
|
|
105
115
|
msgId,
|
|
106
116
|
senderId,
|
|
107
117
|
senderName: sender,
|
|
118
|
+
senderHandle,
|
|
108
119
|
text: ctx.message.caption || "(GIF)",
|
|
109
120
|
replyToMsgId,
|
|
110
121
|
timestamp,
|
|
@@ -117,6 +128,7 @@ export function registerMiddleware(bot: Bot, config: TalonConfig): void {
|
|
|
117
128
|
msgId,
|
|
118
129
|
senderId,
|
|
119
130
|
senderName: sender,
|
|
131
|
+
senderHandle,
|
|
120
132
|
text: ctx.message.caption || `(audio: ${title})`,
|
|
121
133
|
replyToMsgId,
|
|
122
134
|
timestamp,
|
|
@@ -127,6 +139,7 @@ export function registerMiddleware(bot: Bot, config: TalonConfig): void {
|
|
|
127
139
|
msgId,
|
|
128
140
|
senderId,
|
|
129
141
|
senderName: sender,
|
|
142
|
+
senderHandle,
|
|
130
143
|
text: "(video note)",
|
|
131
144
|
replyToMsgId,
|
|
132
145
|
timestamp,
|
|
@@ -137,6 +150,7 @@ export function registerMiddleware(bot: Bot, config: TalonConfig): void {
|
|
|
137
150
|
msgId,
|
|
138
151
|
senderId,
|
|
139
152
|
senderName: sender,
|
|
153
|
+
senderHandle,
|
|
140
154
|
text: `(shared location: ${ctx.message.location.latitude}, ${ctx.message.location.longitude})`,
|
|
141
155
|
replyToMsgId,
|
|
142
156
|
timestamp,
|
|
@@ -152,6 +166,7 @@ export function registerMiddleware(bot: Bot, config: TalonConfig): void {
|
|
|
152
166
|
msgId,
|
|
153
167
|
senderId,
|
|
154
168
|
senderName: sender,
|
|
169
|
+
senderHandle,
|
|
155
170
|
text: `(shared contact: ${name})`,
|
|
156
171
|
replyToMsgId,
|
|
157
172
|
timestamp,
|
package/src/storage/db.ts
CHANGED
|
@@ -84,6 +84,11 @@ function ensureSchema(database: SqlDatabase): void {
|
|
|
84
84
|
} catch {
|
|
85
85
|
/* duplicate column or no such table — both mean nothing to do */
|
|
86
86
|
}
|
|
87
|
+
try {
|
|
88
|
+
database.exec(dbSql.addHistorySenderHandleColumn);
|
|
89
|
+
} catch {
|
|
90
|
+
/* duplicate column or no such table — both mean nothing to do */
|
|
91
|
+
}
|
|
87
92
|
try {
|
|
88
93
|
database.exec(dbSql.addSessionsMetricsColumn);
|
|
89
94
|
} catch {
|
package/src/storage/history.ts
CHANGED
|
@@ -142,7 +142,13 @@ function formatMessage(m: HistoryMessage): string {
|
|
|
142
142
|
: "";
|
|
143
143
|
const fileTag = m.filePath ? ` (file: ${m.filePath})` : "";
|
|
144
144
|
const time = formatSmartTimestamp(m.timestamp);
|
|
145
|
-
|
|
145
|
+
// The handle rides along with the name: a reader deciding to mention
|
|
146
|
+
// someone (a scheduled message, a heartbeat-composed reply) has no other
|
|
147
|
+
// way to learn it, and a display name notifies nobody.
|
|
148
|
+
const who = m.senderHandle
|
|
149
|
+
? `${m.senderName} (@${m.senderHandle})`
|
|
150
|
+
: m.senderName;
|
|
151
|
+
return `[msg:${m.msgId} ${time}] ${who}${replyTag}${mediaTag}${stickerTag}${fileTag}: ${m.text}`;
|
|
146
152
|
}
|
|
147
153
|
|
|
148
154
|
export function getRecentFormatted(chatId: string, limit = 20): string {
|
|
@@ -209,7 +215,7 @@ export function getKnownUsers(chatId: string): string {
|
|
|
209
215
|
return users
|
|
210
216
|
.map(
|
|
211
217
|
(u) =>
|
|
212
|
-
`${u.name} (user_id: ${u.senderId}) — ${u.messageCount} msgs, last seen ${formatRelativeAge(u.lastSeen)}`,
|
|
218
|
+
`${u.name}${u.handle ? ` (@${u.handle})` : ""} (user_id: ${u.senderId}) — ${u.messageCount} msgs, last seen ${formatRelativeAge(u.lastSeen)}`,
|
|
213
219
|
)
|
|
214
220
|
.join("\n");
|
|
215
221
|
}
|
|
@@ -13,6 +13,12 @@ export type HistoryMessage = {
|
|
|
13
13
|
msgId: number;
|
|
14
14
|
senderId: number;
|
|
15
15
|
senderName: string;
|
|
16
|
+
/**
|
|
17
|
+
* Platform handle without `@` (Telegram username / Discord username).
|
|
18
|
+
* Undefined for users who have none. Display names can't be mentioned;
|
|
19
|
+
* this is the addressable form a later reader needs.
|
|
20
|
+
*/
|
|
21
|
+
senderHandle?: string;
|
|
16
22
|
text: string;
|
|
17
23
|
replyToMsgId?: number;
|
|
18
24
|
timestamp: number;
|
|
@@ -27,6 +33,7 @@ type Row = {
|
|
|
27
33
|
msg_id: number;
|
|
28
34
|
sender_id: number;
|
|
29
35
|
sender_name: string;
|
|
36
|
+
sender_handle: string | null;
|
|
30
37
|
text: string;
|
|
31
38
|
reply_to_msg_id: number | null;
|
|
32
39
|
timestamp: number;
|
|
@@ -40,6 +47,7 @@ function rowToMessage(row: Row): HistoryMessage {
|
|
|
40
47
|
msgId: row.msg_id,
|
|
41
48
|
senderId: row.sender_id,
|
|
42
49
|
senderName: row.sender_name,
|
|
50
|
+
senderHandle: row.sender_handle ?? undefined,
|
|
43
51
|
text: row.text,
|
|
44
52
|
replyToMsgId: row.reply_to_msg_id ?? undefined,
|
|
45
53
|
timestamp: row.timestamp,
|
|
@@ -57,6 +65,7 @@ export function insert(chatId: string, msg: HistoryMessage): void {
|
|
|
57
65
|
msg.msgId,
|
|
58
66
|
msg.senderId,
|
|
59
67
|
msg.senderName,
|
|
68
|
+
msg.senderHandle ?? null,
|
|
60
69
|
msg.text,
|
|
61
70
|
msg.replyToMsgId ?? null,
|
|
62
71
|
msg.timestamp,
|
|
@@ -168,6 +177,8 @@ export function latestMsgId(chatId: string): number | undefined {
|
|
|
168
177
|
export type KnownUser = {
|
|
169
178
|
senderId: number;
|
|
170
179
|
name: string;
|
|
180
|
+
/** Latest known platform handle without `@`, if the user has one. */
|
|
181
|
+
handle?: string;
|
|
171
182
|
lastSeen: number;
|
|
172
183
|
messageCount: number;
|
|
173
184
|
};
|
|
@@ -181,10 +192,12 @@ export function knownUsers(chatId: string): KnownUser[] {
|
|
|
181
192
|
last_seen: number;
|
|
182
193
|
message_count: number;
|
|
183
194
|
name: string;
|
|
195
|
+
handle: string | null;
|
|
184
196
|
}>;
|
|
185
197
|
return rows.map((r) => ({
|
|
186
198
|
senderId: r.sender_id,
|
|
187
199
|
name: r.name,
|
|
200
|
+
handle: r.handle ?? undefined,
|
|
188
201
|
lastSeen: r.last_seen,
|
|
189
202
|
messageCount: r.message_count,
|
|
190
203
|
}));
|
package/src/storage/sql/db.sql
CHANGED
|
@@ -10,6 +10,11 @@ PRAGMA wal_checkpoint(TRUNCATE)
|
|
|
10
10
|
-- "no such table" (fresh databases get the column via schema.sql).
|
|
11
11
|
ALTER TABLE media_index ADD COLUMN content_hash TEXT
|
|
12
12
|
|
|
13
|
+
-- name: addHistorySenderHandleColumn
|
|
14
|
+
-- Column reconciliation for databases that shipped before sender handles
|
|
15
|
+
-- were recorded. Fresh databases get the column via schema.sql.
|
|
16
|
+
ALTER TABLE history_messages ADD COLUMN sender_handle TEXT
|
|
17
|
+
|
|
13
18
|
-- name: addSessionsMetricsColumn
|
|
14
19
|
-- Column reconciliation for databases that shipped before per-session
|
|
15
20
|
-- metrics existed. Fresh databases get the column via schema.sql.
|
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
|
|
4
4
|
-- name: insert
|
|
5
5
|
INSERT OR IGNORE INTO history_messages
|
|
6
|
-
(chat_id, msg_id, sender_id, sender_name,
|
|
7
|
-
timestamp, media_type, sticker_file_id, file_path)
|
|
8
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
6
|
+
(chat_id, msg_id, sender_id, sender_name, sender_handle, text,
|
|
7
|
+
reply_to_msg_id, timestamp, media_type, sticker_file_id, file_path)
|
|
8
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
9
9
|
|
|
10
10
|
-- name: recent
|
|
11
|
-
SELECT msg_id, sender_id, sender_name, text, reply_to_msg_id,
|
|
11
|
+
SELECT msg_id, sender_id, sender_name, sender_handle, text, reply_to_msg_id,
|
|
12
12
|
timestamp, media_type, sticker_file_id, file_path
|
|
13
13
|
FROM history_messages
|
|
14
14
|
WHERE chat_id = ? ORDER BY id DESC LIMIT ?
|
|
@@ -16,7 +16,7 @@ WHERE chat_id = ? ORDER BY id DESC LIMIT ?
|
|
|
16
16
|
-- name: recentBefore
|
|
17
17
|
-- Scroll-back pagination: the window of messages strictly older than a
|
|
18
18
|
-- given msg_id, newest-first (the repository reverses to chronological).
|
|
19
|
-
SELECT msg_id, sender_id, sender_name, text, reply_to_msg_id,
|
|
19
|
+
SELECT msg_id, sender_id, sender_name, sender_handle, text, reply_to_msg_id,
|
|
20
20
|
timestamp, media_type, sticker_file_id, file_path
|
|
21
21
|
FROM history_messages
|
|
22
22
|
WHERE chat_id = ? AND msg_id < ? ORDER BY id DESC LIMIT ?
|
|
@@ -30,7 +30,7 @@ DELETE FROM history_messages WHERE chat_id = ?
|
|
|
30
30
|
-- name: searchFts
|
|
31
31
|
-- The match param must already be a valid FTS5 expression
|
|
32
32
|
-- (see history.ts ftsQuery).
|
|
33
|
-
SELECT msg_id, sender_id, sender_name, text, reply_to_msg_id,
|
|
33
|
+
SELECT msg_id, sender_id, sender_name, sender_handle, text, reply_to_msg_id,
|
|
34
34
|
timestamp, media_type, sticker_file_id, file_path
|
|
35
35
|
FROM history_messages
|
|
36
36
|
WHERE chat_id = ?
|
|
@@ -39,20 +39,20 @@ ORDER BY id DESC LIMIT ?
|
|
|
39
39
|
|
|
40
40
|
-- name: bySenderName
|
|
41
41
|
-- The fragment param is LIKE-escaped by the repository (backslash escape).
|
|
42
|
-
SELECT msg_id, sender_id, sender_name, text, reply_to_msg_id,
|
|
42
|
+
SELECT msg_id, sender_id, sender_name, sender_handle, text, reply_to_msg_id,
|
|
43
43
|
timestamp, media_type, sticker_file_id, file_path
|
|
44
44
|
FROM history_messages
|
|
45
45
|
WHERE chat_id = ? AND lower(sender_name) LIKE ? ESCAPE '\'
|
|
46
46
|
ORDER BY id DESC LIMIT ?
|
|
47
47
|
|
|
48
48
|
-- name: byMsgId
|
|
49
|
-
SELECT msg_id, sender_id, sender_name, text, reply_to_msg_id,
|
|
49
|
+
SELECT msg_id, sender_id, sender_name, sender_handle, text, reply_to_msg_id,
|
|
50
50
|
timestamp, media_type, sticker_file_id, file_path
|
|
51
51
|
FROM history_messages
|
|
52
52
|
WHERE chat_id = ? AND msg_id = ? ORDER BY id DESC LIMIT 1
|
|
53
53
|
|
|
54
54
|
-- name: bySenderId
|
|
55
|
-
SELECT msg_id, sender_id, sender_name, text, reply_to_msg_id,
|
|
55
|
+
SELECT msg_id, sender_id, sender_name, sender_handle, text, reply_to_msg_id,
|
|
56
56
|
timestamp, media_type, sticker_file_id, file_path
|
|
57
57
|
FROM history_messages
|
|
58
58
|
WHERE chat_id = ? AND sender_id = ? ORDER BY id DESC LIMIT ?
|
|
@@ -66,7 +66,13 @@ SELECT sender_id,
|
|
|
66
66
|
COUNT(*) AS message_count,
|
|
67
67
|
(SELECT sender_name FROM history_messages i
|
|
68
68
|
WHERE i.chat_id = o.chat_id AND i.sender_id = o.sender_id
|
|
69
|
-
ORDER BY i.id DESC LIMIT 1) AS name
|
|
69
|
+
ORDER BY i.id DESC LIMIT 1) AS name,
|
|
70
|
+
-- Most recent NON-NULL handle: people set a username long after
|
|
71
|
+
-- their first message, and the newest row may predate it.
|
|
72
|
+
(SELECT i.sender_handle FROM history_messages i
|
|
73
|
+
WHERE i.chat_id = o.chat_id AND i.sender_id = o.sender_id
|
|
74
|
+
AND i.sender_handle IS NOT NULL
|
|
75
|
+
ORDER BY i.id DESC LIMIT 1) AS handle
|
|
70
76
|
FROM history_messages o
|
|
71
77
|
WHERE chat_id = ?
|
|
72
78
|
GROUP BY sender_id
|
|
@@ -11,6 +11,10 @@ CREATE TABLE IF NOT EXISTS history_messages (
|
|
|
11
11
|
msg_id INTEGER NOT NULL,
|
|
12
12
|
sender_id INTEGER NOT NULL,
|
|
13
13
|
sender_name TEXT NOT NULL,
|
|
14
|
+
-- Platform handle without `@` (Telegram username / Discord username).
|
|
15
|
+
-- Null for users who have none — display names are not addressable, so
|
|
16
|
+
-- this is what a later reader needs to actually mention someone.
|
|
17
|
+
sender_handle TEXT,
|
|
14
18
|
text TEXT NOT NULL,
|
|
15
19
|
reply_to_msg_id INTEGER,
|
|
16
20
|
timestamp INTEGER NOT NULL,
|
|
@@ -16,6 +16,10 @@ CREATE TABLE IF NOT EXISTS history_messages (
|
|
|
16
16
|
msg_id INTEGER NOT NULL,
|
|
17
17
|
sender_id INTEGER NOT NULL,
|
|
18
18
|
sender_name TEXT NOT NULL,
|
|
19
|
+
-- Platform handle without \`@\` (Telegram username / Discord username).
|
|
20
|
+
-- Null for users who have none — display names are not addressable, so
|
|
21
|
+
-- this is what a later reader needs to actually mention someone.
|
|
22
|
+
sender_handle TEXT,
|
|
19
23
|
text TEXT NOT NULL,
|
|
20
24
|
reply_to_msg_id INTEGER,
|
|
21
25
|
timestamp INTEGER NOT NULL,
|
|
@@ -293,6 +297,9 @@ export const dbSql = {
|
|
|
293
297
|
-- attempts this on every open and swallows "duplicate column name" /
|
|
294
298
|
-- "no such table" (fresh databases get the column via schema.sql).
|
|
295
299
|
ALTER TABLE media_index ADD COLUMN content_hash TEXT`,
|
|
300
|
+
addHistorySenderHandleColumn: `-- Column reconciliation for databases that shipped before sender handles
|
|
301
|
+
-- were recorded. Fresh databases get the column via schema.sql.
|
|
302
|
+
ALTER TABLE history_messages ADD COLUMN sender_handle TEXT`,
|
|
296
303
|
addSessionsMetricsColumn: `-- Column reconciliation for databases that shipped before per-session
|
|
297
304
|
-- metrics existed. Fresh databases get the column via schema.sql.
|
|
298
305
|
ALTER TABLE sessions ADD COLUMN metrics TEXT NOT NULL DEFAULT '{"lifetime":{"counters":{"queries":0,"toolCalls":0,"turnsWithTools":0,"apiCalls":0,"inputTokens":0,"outputTokens":0,"cacheReadTokens":0,"cacheWriteTokens":0,"failedTurns":0,"flowViolationRetries":0,"flowViolationCapExhausted":0,"trailingTextDropped":0},"latency":{"count":0,"sumMs":0,"minMs":null,"maxMs":0},"toolCallsByName":{},"backend":{},"cacheHitPercent":{"count":0,"sumMs":0,"minMs":null,"maxMs":0},"toolCallsPerTurn":{"count":0,"sumMs":0,"minMs":null,"maxMs":0},"apiCallsPerTurn":{"count":0,"sumMs":0,"minMs":null,"maxMs":0}},"buckets":{}}'`,
|
|
@@ -326,16 +333,16 @@ WHERE chat_id = ? AND status IN (/* statuses */)`,
|
|
|
326
333
|
|
|
327
334
|
export const historySql = {
|
|
328
335
|
insert: `INSERT OR IGNORE INTO history_messages
|
|
329
|
-
(chat_id, msg_id, sender_id, sender_name,
|
|
330
|
-
timestamp, media_type, sticker_file_id, file_path)
|
|
331
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
332
|
-
recent: `SELECT msg_id, sender_id, sender_name, text, reply_to_msg_id,
|
|
336
|
+
(chat_id, msg_id, sender_id, sender_name, sender_handle, text,
|
|
337
|
+
reply_to_msg_id, timestamp, media_type, sticker_file_id, file_path)
|
|
338
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
339
|
+
recent: `SELECT msg_id, sender_id, sender_name, sender_handle, text, reply_to_msg_id,
|
|
333
340
|
timestamp, media_type, sticker_file_id, file_path
|
|
334
341
|
FROM history_messages
|
|
335
342
|
WHERE chat_id = ? ORDER BY id DESC LIMIT ?`,
|
|
336
343
|
recentBefore: `-- Scroll-back pagination: the window of messages strictly older than a
|
|
337
344
|
-- given msg_id, newest-first (the repository reverses to chronological).
|
|
338
|
-
SELECT msg_id, sender_id, sender_name, text, reply_to_msg_id,
|
|
345
|
+
SELECT msg_id, sender_id, sender_name, sender_handle, text, reply_to_msg_id,
|
|
339
346
|
timestamp, media_type, sticker_file_id, file_path
|
|
340
347
|
FROM history_messages
|
|
341
348
|
WHERE chat_id = ? AND msg_id < ? ORDER BY id DESC LIMIT ?`,
|
|
@@ -343,23 +350,23 @@ WHERE chat_id = ? AND msg_id < ? ORDER BY id DESC LIMIT ?`,
|
|
|
343
350
|
deleteChat: `DELETE FROM history_messages WHERE chat_id = ?`,
|
|
344
351
|
searchFts: `-- The match param must already be a valid FTS5 expression
|
|
345
352
|
-- (see history.ts ftsQuery).
|
|
346
|
-
SELECT msg_id, sender_id, sender_name, text, reply_to_msg_id,
|
|
353
|
+
SELECT msg_id, sender_id, sender_name, sender_handle, text, reply_to_msg_id,
|
|
347
354
|
timestamp, media_type, sticker_file_id, file_path
|
|
348
355
|
FROM history_messages
|
|
349
356
|
WHERE chat_id = ?
|
|
350
357
|
AND id IN (SELECT rowid FROM history_fts WHERE history_fts MATCH ?)
|
|
351
358
|
ORDER BY id DESC LIMIT ?`,
|
|
352
359
|
bySenderName: `-- The fragment param is LIKE-escaped by the repository (backslash escape).
|
|
353
|
-
SELECT msg_id, sender_id, sender_name, text, reply_to_msg_id,
|
|
360
|
+
SELECT msg_id, sender_id, sender_name, sender_handle, text, reply_to_msg_id,
|
|
354
361
|
timestamp, media_type, sticker_file_id, file_path
|
|
355
362
|
FROM history_messages
|
|
356
363
|
WHERE chat_id = ? AND lower(sender_name) LIKE ? ESCAPE '\\'
|
|
357
364
|
ORDER BY id DESC LIMIT ?`,
|
|
358
|
-
byMsgId: `SELECT msg_id, sender_id, sender_name, text, reply_to_msg_id,
|
|
365
|
+
byMsgId: `SELECT msg_id, sender_id, sender_name, sender_handle, text, reply_to_msg_id,
|
|
359
366
|
timestamp, media_type, sticker_file_id, file_path
|
|
360
367
|
FROM history_messages
|
|
361
368
|
WHERE chat_id = ? AND msg_id = ? ORDER BY id DESC LIMIT 1`,
|
|
362
|
-
bySenderId: `SELECT msg_id, sender_id, sender_name, text, reply_to_msg_id,
|
|
369
|
+
bySenderId: `SELECT msg_id, sender_id, sender_name, sender_handle, text, reply_to_msg_id,
|
|
363
370
|
timestamp, media_type, sticker_file_id, file_path
|
|
364
371
|
FROM history_messages
|
|
365
372
|
WHERE chat_id = ? AND sender_id = ? ORDER BY id DESC LIMIT ?`,
|
|
@@ -369,7 +376,13 @@ WHERE chat_id = ? AND sender_id = ? ORDER BY id DESC LIMIT ?`,
|
|
|
369
376
|
COUNT(*) AS message_count,
|
|
370
377
|
(SELECT sender_name FROM history_messages i
|
|
371
378
|
WHERE i.chat_id = o.chat_id AND i.sender_id = o.sender_id
|
|
372
|
-
ORDER BY i.id DESC LIMIT 1) AS name
|
|
379
|
+
ORDER BY i.id DESC LIMIT 1) AS name,
|
|
380
|
+
-- Most recent NON-NULL handle: people set a username long after
|
|
381
|
+
-- their first message, and the newest row may predate it.
|
|
382
|
+
(SELECT i.sender_handle FROM history_messages i
|
|
383
|
+
WHERE i.chat_id = o.chat_id AND i.sender_id = o.sender_id
|
|
384
|
+
AND i.sender_handle IS NOT NULL
|
|
385
|
+
ORDER BY i.id DESC LIMIT 1) AS handle
|
|
373
386
|
FROM history_messages o
|
|
374
387
|
WHERE chat_id = ?
|
|
375
388
|
GROUP BY sender_id
|