talon-agent 4.3.2 → 4.4.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/src/app.ts +1 -1
- package/src/backend/claude-sdk/doctor.ts +1 -1
- package/src/backend/claude-sdk/state.ts +1 -1
- package/src/backend/codex/doctor.ts +1 -1
- package/src/backend/codex/init.ts +1 -1
- package/src/backend/codex/state.ts +1 -1
- package/src/backend/openai-agents/doctor.ts +1 -1
- package/src/backend/openai-agents/init.ts +1 -1
- package/src/backend/openai-agents/state.ts +1 -1
- package/src/backend/remote-server/factory.ts +1 -1
- package/src/backend/remote-server/mcp.ts +1 -1
- package/src/backend/remote-server/server-bindings.ts +1 -1
- package/src/backend/remote-server/state.ts +1 -1
- package/src/backend/shared/system-prompt.ts +1 -1
- package/src/bootstrap.ts +3 -3
- package/src/cli/chat.ts +1 -1
- package/src/cli/config.ts +2 -2
- package/src/cli/doctor.ts +1 -1
- package/src/cli/index.ts +8 -0
- package/src/cli/memory.ts +166 -0
- package/src/cli/setup.ts +1 -1
- package/src/core/agent-runtime/backend-registry.ts +2 -2
- package/src/core/agent-runtime/model-ref.ts +2 -2
- package/src/core/auth/expiry-monitor.ts +1 -1
- package/src/{util/config.ts → core/config/index.ts} +8 -8
- package/src/core/{doctor.ts → doctor/index.ts} +11 -11
- package/src/core/engine/backend-controller/legacy.ts +1 -1
- package/src/core/engine/backend-controller/pool.ts +1 -1
- package/src/core/engine/backend-controller/rebind.ts +1 -1
- package/src/core/engine/backend-controller/state.ts +1 -1
- package/src/core/engine/gateway-actions/plugins.ts +1 -1
- package/src/core/engine/model-audit.ts +1 -1
- package/src/core/{notify.ts → frontend-runtime/admin-notify.ts} +1 -1
- package/src/core/frontend-runtime/capabilities.ts +1 -1
- package/src/core/models/active-model.ts +1 -1
- package/src/core/plugin/builtins.ts +2 -2
- package/src/core/plugin/manage.ts +1 -1
- package/src/core/plugin/native-runtimes.ts +1 -1
- package/src/frontend/discord/admin.ts +1 -1
- package/src/frontend/discord/callbacks/components/index.ts +1 -1
- package/src/frontend/discord/callbacks/components/settings.ts +1 -1
- package/src/frontend/discord/callbacks/components/types.ts +1 -1
- package/src/frontend/discord/callbacks/modals.ts +1 -1
- package/src/frontend/discord/commands/admin.ts +2 -2
- package/src/frontend/discord/commands/definitions.ts +1 -1
- package/src/frontend/discord/commands/info.ts +1 -1
- package/src/frontend/discord/commands/router.ts +1 -1
- package/src/frontend/discord/commands/session.ts +1 -1
- package/src/frontend/discord/commands/settings.ts +1 -1
- package/src/frontend/discord/handlers/delivery.ts +1 -1
- package/src/frontend/discord/handlers/messages.ts +1 -1
- package/src/frontend/discord/handlers/queue.ts +1 -1
- package/src/frontend/discord/handlers/state.ts +1 -1
- package/src/frontend/discord/index.ts +1 -1
- package/src/frontend/discord/middleware.ts +1 -1
- package/src/frontend/discord/render.ts +1 -1
- package/src/frontend/discord/runtime.ts +1 -1
- package/src/frontend/native/extensions.ts +4 -1
- package/src/frontend/native/index.ts +1 -1
- package/src/frontend/native/runtime.ts +1 -1
- package/src/frontend/native/settings.ts +1 -1
- package/src/frontend/shared/model-commands.ts +1 -1
- package/src/frontend/shared/plan-usage-report.ts +1 -1
- package/src/frontend/shared/reasoning-levels.ts +1 -1
- package/src/frontend/shared/session-status.ts +1 -1
- package/src/frontend/teams/index.ts +1 -1
- package/src/frontend/teams/runtime.ts +1 -1
- package/src/frontend/telegram/actions/chat-info.ts +1 -1
- package/src/frontend/telegram/actions/coerce.ts +15 -0
- package/src/frontend/telegram/actions/index.ts +2 -2
- package/src/frontend/telegram/actions/media.ts +1 -1
- package/src/frontend/telegram/actions/messaging.ts +3 -4
- package/src/frontend/telegram/actions/moderation/chat.ts +63 -0
- package/src/frontend/telegram/actions/moderation/index.ts +74 -0
- package/src/frontend/telegram/actions/moderation/members.ts +77 -0
- package/src/frontend/telegram/actions/moderation/permissions.ts +69 -0
- package/src/frontend/telegram/actions/moderation/topics.ts +48 -0
- package/src/frontend/telegram/actions/moderation/types.ts +16 -0
- package/src/frontend/telegram/actions/rich-messages.ts +51 -0
- package/src/frontend/telegram/actions/{shared.ts → send.ts} +9 -59
- package/src/frontend/telegram/admin/background.ts +66 -0
- package/src/frontend/telegram/admin/chunked-reply.ts +20 -0
- package/src/frontend/telegram/admin/health.ts +81 -0
- package/src/frontend/telegram/admin/sessions.ts +105 -0
- package/src/frontend/telegram/admin.ts +57 -262
- package/src/frontend/telegram/callbacks/auth.ts +1 -1
- package/src/frontend/telegram/callbacks/effort.ts +1 -1
- package/src/frontend/telegram/callbacks/index.ts +3 -3
- package/src/frontend/telegram/callbacks/metrics.ts +1 -1
- package/src/frontend/telegram/callbacks/model/backend.ts +156 -0
- package/src/frontend/telegram/callbacks/model/control.ts +29 -0
- package/src/frontend/telegram/callbacks/model/nav.ts +54 -0
- package/src/frontend/telegram/callbacks/model/select.ts +100 -0
- package/src/frontend/telegram/callbacks/model/types.ts +39 -0
- package/src/frontend/telegram/callbacks/model/views.ts +116 -0
- package/src/frontend/telegram/callbacks/model.ts +61 -331
- package/src/frontend/telegram/callbacks/pulse.ts +1 -1
- package/src/frontend/telegram/callbacks/{shared.ts → query.ts} +3 -2
- package/src/frontend/telegram/callbacks/settings.ts +1 -1
- package/src/frontend/telegram/callbacks/whatsapp.ts +1 -1
- package/src/frontend/telegram/commands/admin.ts +108 -77
- package/src/frontend/telegram/commands/index.ts +1 -1
- package/src/frontend/telegram/commands/settings.ts +65 -43
- package/src/frontend/telegram/commands/state.ts +1 -1
- package/src/frontend/telegram/commands/whatsapp-pairing.ts +1 -1
- package/src/frontend/telegram/handlers/context.ts +1 -1
- package/src/frontend/telegram/handlers/delivery.ts +2 -2
- package/src/frontend/telegram/handlers/messages.ts +1 -1
- package/src/frontend/telegram/handlers/queue.ts +1 -1
- package/src/frontend/telegram/handlers/state.ts +1 -1
- package/src/frontend/telegram/helpers/diagnostics.ts +1 -1
- package/src/frontend/telegram/index.ts +1 -1
- package/src/frontend/telegram/middleware.ts +167 -202
- package/src/frontend/telegram/model-menu.ts +1 -1
- package/src/frontend/terminal/builtins/context.ts +184 -0
- package/src/frontend/terminal/builtins/help.ts +35 -0
- package/src/frontend/terminal/builtins/index.ts +23 -0
- package/src/frontend/terminal/builtins/model.ts +198 -0
- package/src/frontend/terminal/builtins/session.ts +101 -0
- package/src/frontend/terminal/builtins/status.ts +202 -0
- package/src/frontend/terminal/command-registry.ts +80 -0
- package/src/frontend/terminal/commands.ts +11 -674
- package/src/frontend/terminal/index.ts +1 -1
- package/src/frontend/terminal/input/history.ts +64 -0
- package/src/frontend/terminal/input/keys.ts +143 -0
- package/src/frontend/terminal/input/parts.ts +33 -0
- package/src/frontend/terminal/input/paste.ts +53 -0
- package/src/frontend/terminal/input/render.ts +42 -0
- package/src/frontend/terminal/input/state.ts +60 -0
- package/src/frontend/terminal/input.ts +27 -342
- package/src/frontend/whatsapp/connection.ts +1 -1
- package/src/frontend/whatsapp/index.ts +2 -2
- package/src/frontend/whatsapp/pairing-service.ts +1 -1
- package/src/frontend/whatsapp/runtime.ts +1 -1
- package/src/plugins/github/provision.ts +1 -1
- package/src/plugins/mempalace/provision.ts +1 -1
- package/src/plugins/playwright/provision.ts +1 -1
- package/src/storage/memory.ts +505 -0
- package/src/storage/repositories/memory-repo.ts +308 -0
- package/src/storage/sql/memory.sql +95 -0
- package/src/storage/sql/schema.sql +81 -0
- package/src/storage/sql/statements.generated.ts +146 -0
- package/src/frontend/telegram/actions/moderation.ts +0 -255
- /package/src/core/{doctor-types.ts → doctor/types.ts} +0 -0
- /package/src/core/{pairing-broker.ts → frontend-runtime/pairing-broker.ts} +0 -0
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* Records every message into the in-memory history buffer
|
|
2
|
+
* Update-level middleware — runs for ALL messages, before handlers.
|
|
3
|
+
* Records every message into the in-memory history buffer, keeps the
|
|
4
|
+
* update offset / forum topic / userbot access current, and wires the
|
|
5
|
+
* per-message-type handlers. `registerMiddleware` binds each piece in the
|
|
6
|
+
* order grammY must see them.
|
|
4
7
|
*/
|
|
5
8
|
|
|
6
|
-
import type { Bot } from "grammy";
|
|
7
|
-
import type {
|
|
9
|
+
import type { Bot, Context, Filter, NextFunction } from "grammy";
|
|
10
|
+
import type { Message } from "grammy/types";
|
|
11
|
+
import type { TalonConfig } from "../../core/config/index.js";
|
|
8
12
|
import { pushMessage } from "../../storage/history.js";
|
|
13
|
+
import type { HistoryMessage } from "../../storage/repositories/history-repo.js";
|
|
9
14
|
import { allowChat, revokeChat } from "./userbot.js";
|
|
10
15
|
import { registerChat } from "../../core/background/pulse.js";
|
|
11
16
|
import { log } from "../../util/log.js";
|
|
@@ -26,214 +31,165 @@ import {
|
|
|
26
31
|
handleVideoNoteMessage,
|
|
27
32
|
} from "./handlers/index.js";
|
|
28
33
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
});
|
|
34
|
+
// ── Update-offset tracking (every update, before anything else) ──────────
|
|
35
|
+
// Telegram redelivers any update whose id was never confirmed; the
|
|
36
|
+
// shutdown path confirms this one so a process-ending command can't be
|
|
37
|
+
// served twice. See update-offset.ts.
|
|
38
|
+
function trackUpdateOffset(ctx: Context, next: NextFunction): Promise<void> {
|
|
39
|
+
noteUpdateId(ctx.update.update_id);
|
|
40
|
+
return next();
|
|
41
|
+
}
|
|
38
42
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
// Keep the ambient forum topic current so outbound sends (which have no
|
|
44
|
-
// reply anchor — drafts, media, plain sends) land in the topic the
|
|
45
|
-
// conversation is actually happening in, not General.
|
|
46
|
-
noteInboundThread(ctx.chat.id, ctx.message);
|
|
47
|
-
// The handle is the only addressable form of a user — persist it with
|
|
48
|
-
// every row so later readers (history views, heartbeat-composed
|
|
49
|
-
// messages) can mention someone instead of guessing.
|
|
50
|
-
const senderHandle = ctx.from?.username;
|
|
51
|
-
const senderId = ctx.from?.id ?? 0;
|
|
52
|
-
const msgId = ctx.message.message_id;
|
|
53
|
-
const replyToMsgId = ctx.message.reply_to_message?.message_id;
|
|
43
|
+
type HistoryEntry = Pick<
|
|
44
|
+
HistoryMessage,
|
|
45
|
+
"text" | "mediaType" | "stickerFileId"
|
|
46
|
+
>;
|
|
54
47
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
48
|
+
/** The media kinds history records, checked in this order. */
|
|
49
|
+
function mediaHistoryEntry(message: Message): HistoryEntry | undefined {
|
|
50
|
+
if ("photo" in message && message.photo) {
|
|
51
|
+
return { text: message.caption || "(photo)", mediaType: "photo" };
|
|
52
|
+
}
|
|
53
|
+
if ("document" in message && message.document) {
|
|
54
|
+
const name = message.document.file_name || "file";
|
|
55
|
+
return { text: message.caption || `(sent ${name})`, mediaType: "document" };
|
|
56
|
+
}
|
|
57
|
+
if ("voice" in message && message.voice) {
|
|
58
|
+
return { text: "(voice message)", mediaType: "voice" };
|
|
59
|
+
}
|
|
60
|
+
if ("sticker" in message && message.sticker) {
|
|
61
|
+
return {
|
|
62
|
+
text: message.sticker.emoji || "(sticker)",
|
|
63
|
+
mediaType: "sticker",
|
|
64
|
+
stickerFileId: message.sticker.file_id,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
if ("video" in message && message.video) {
|
|
68
|
+
return { text: message.caption || "(video)", mediaType: "video" };
|
|
69
|
+
}
|
|
70
|
+
if ("animation" in message && message.animation) {
|
|
71
|
+
return { text: message.caption || "(GIF)", mediaType: "animation" };
|
|
72
|
+
}
|
|
73
|
+
if ("audio" in message && message.audio) {
|
|
74
|
+
const title = message.audio.title || message.audio.file_name || "audio";
|
|
75
|
+
return {
|
|
76
|
+
text: message.caption || `(audio: ${title})`,
|
|
77
|
+
mediaType: "document", // treat audio like documents in history
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
if ("video_note" in message && message.video_note) {
|
|
81
|
+
return { text: "(video note)", mediaType: "video" };
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
61
85
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
} else if ("document" in ctx.message && ctx.message.document) {
|
|
84
|
-
const name = ctx.message.document.file_name || "file";
|
|
85
|
-
pushMessage(chatId, {
|
|
86
|
-
msgId,
|
|
87
|
-
senderId,
|
|
88
|
-
senderName: sender,
|
|
89
|
-
senderHandle,
|
|
90
|
-
text: ctx.message.caption || `(sent ${name})`,
|
|
91
|
-
replyToMsgId,
|
|
92
|
-
timestamp,
|
|
93
|
-
mediaType: "document",
|
|
94
|
-
});
|
|
95
|
-
} else if ("voice" in ctx.message && ctx.message.voice) {
|
|
96
|
-
pushMessage(chatId, {
|
|
97
|
-
msgId,
|
|
98
|
-
senderId,
|
|
99
|
-
senderName: sender,
|
|
100
|
-
senderHandle,
|
|
101
|
-
text: "(voice message)",
|
|
102
|
-
replyToMsgId,
|
|
103
|
-
timestamp,
|
|
104
|
-
mediaType: "voice",
|
|
105
|
-
});
|
|
106
|
-
} else if ("sticker" in ctx.message && ctx.message.sticker) {
|
|
107
|
-
pushMessage(chatId, {
|
|
108
|
-
msgId,
|
|
109
|
-
senderId,
|
|
110
|
-
senderName: sender,
|
|
111
|
-
senderHandle,
|
|
112
|
-
text: ctx.message.sticker.emoji || "(sticker)",
|
|
113
|
-
replyToMsgId,
|
|
114
|
-
timestamp,
|
|
115
|
-
mediaType: "sticker",
|
|
116
|
-
stickerFileId: ctx.message.sticker.file_id,
|
|
117
|
-
});
|
|
118
|
-
} else if ("video" in ctx.message && ctx.message.video) {
|
|
119
|
-
pushMessage(chatId, {
|
|
120
|
-
msgId,
|
|
121
|
-
senderId,
|
|
122
|
-
senderName: sender,
|
|
123
|
-
senderHandle,
|
|
124
|
-
text: ctx.message.caption || "(video)",
|
|
125
|
-
replyToMsgId,
|
|
126
|
-
timestamp,
|
|
127
|
-
mediaType: "video",
|
|
128
|
-
});
|
|
129
|
-
} else if ("animation" in ctx.message && ctx.message.animation) {
|
|
130
|
-
pushMessage(chatId, {
|
|
131
|
-
msgId,
|
|
132
|
-
senderId,
|
|
133
|
-
senderName: sender,
|
|
134
|
-
senderHandle,
|
|
135
|
-
text: ctx.message.caption || "(GIF)",
|
|
136
|
-
replyToMsgId,
|
|
137
|
-
timestamp,
|
|
138
|
-
mediaType: "animation",
|
|
139
|
-
});
|
|
140
|
-
} else if ("audio" in ctx.message && ctx.message.audio) {
|
|
141
|
-
const title =
|
|
142
|
-
ctx.message.audio.title || ctx.message.audio.file_name || "audio";
|
|
143
|
-
pushMessage(chatId, {
|
|
144
|
-
msgId,
|
|
145
|
-
senderId,
|
|
146
|
-
senderName: sender,
|
|
147
|
-
senderHandle,
|
|
148
|
-
text: ctx.message.caption || `(audio: ${title})`,
|
|
149
|
-
replyToMsgId,
|
|
150
|
-
timestamp,
|
|
151
|
-
mediaType: "document", // treat audio like documents in history
|
|
152
|
-
});
|
|
153
|
-
} else if ("video_note" in ctx.message && ctx.message.video_note) {
|
|
154
|
-
pushMessage(chatId, {
|
|
155
|
-
msgId,
|
|
156
|
-
senderId,
|
|
157
|
-
senderName: sender,
|
|
158
|
-
senderHandle,
|
|
159
|
-
text: "(video note)",
|
|
160
|
-
replyToMsgId,
|
|
161
|
-
timestamp,
|
|
162
|
-
mediaType: "video",
|
|
163
|
-
});
|
|
164
|
-
} else if ("location" in ctx.message && ctx.message.location) {
|
|
165
|
-
pushMessage(chatId, {
|
|
166
|
-
msgId,
|
|
167
|
-
senderId,
|
|
168
|
-
senderName: sender,
|
|
169
|
-
senderHandle,
|
|
170
|
-
text: `(shared location: ${ctx.message.location.latitude}, ${ctx.message.location.longitude})`,
|
|
171
|
-
replyToMsgId,
|
|
172
|
-
timestamp,
|
|
173
|
-
});
|
|
174
|
-
} else if ("contact" in ctx.message && ctx.message.contact) {
|
|
175
|
-
const name = [
|
|
176
|
-
ctx.message.contact.first_name,
|
|
177
|
-
ctx.message.contact.last_name,
|
|
178
|
-
]
|
|
179
|
-
.filter(Boolean)
|
|
180
|
-
.join(" ");
|
|
181
|
-
pushMessage(chatId, {
|
|
182
|
-
msgId,
|
|
183
|
-
senderId,
|
|
184
|
-
senderName: sender,
|
|
185
|
-
senderHandle,
|
|
186
|
-
text: `(shared contact: ${name})`,
|
|
187
|
-
replyToMsgId,
|
|
188
|
-
timestamp,
|
|
189
|
-
});
|
|
190
|
-
}
|
|
86
|
+
/** The history row's content for one inbound message; nothing for kinds
|
|
87
|
+
* history doesn't record. */
|
|
88
|
+
function historyEntryFor(message: Message): HistoryEntry | undefined {
|
|
89
|
+
if ("text" in message && message.text) {
|
|
90
|
+
return { text: message.text };
|
|
91
|
+
}
|
|
92
|
+
const media = mediaHistoryEntry(message);
|
|
93
|
+
if (media) return media;
|
|
94
|
+
if ("location" in message && message.location) {
|
|
95
|
+
return {
|
|
96
|
+
text: `(shared location: ${message.location.latitude}, ${message.location.longitude})`,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
if ("contact" in message && message.contact) {
|
|
100
|
+
const name = [message.contact.first_name, message.contact.last_name]
|
|
101
|
+
.filter(Boolean)
|
|
102
|
+
.join(" ");
|
|
103
|
+
return { text: `(shared contact: ${name})` };
|
|
104
|
+
}
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
191
107
|
|
|
192
|
-
|
|
193
|
-
|
|
108
|
+
// ── History capture (runs for ALL messages, before handlers) ─────────────
|
|
109
|
+
function captureHistory(
|
|
110
|
+
ctx: Filter<Context, "message">,
|
|
111
|
+
next: NextFunction,
|
|
112
|
+
): Promise<void> {
|
|
113
|
+
const chatId = String(ctx.chat.id);
|
|
114
|
+
const sender = getSenderName(ctx.from);
|
|
115
|
+
// Keep the ambient forum topic current so outbound sends (which have no
|
|
116
|
+
// reply anchor — drafts, media, plain sends) land in the topic the
|
|
117
|
+
// conversation is actually happening in, not General.
|
|
118
|
+
noteInboundThread(ctx.chat.id, ctx.message);
|
|
119
|
+
// The handle is the only addressable form of a user — persist it with
|
|
120
|
+
// every row so later readers (history views, heartbeat-composed
|
|
121
|
+
// messages) can mention someone instead of guessing.
|
|
122
|
+
const senderHandle = ctx.from?.username;
|
|
123
|
+
const senderId = ctx.from?.id ?? 0;
|
|
124
|
+
const msgId = ctx.message.message_id;
|
|
125
|
+
const replyToMsgId = ctx.message.reply_to_message?.message_id;
|
|
194
126
|
|
|
195
|
-
//
|
|
196
|
-
|
|
197
|
-
//
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
// Ignore the bot reacting to messages itself.
|
|
202
|
-
if (mr.user?.id === ctx.me.id) return;
|
|
203
|
-
const added = newlyAddedEmojis(mr.old_reaction, mr.new_reaction);
|
|
204
|
-
recordReactionToBot(mr.chat.id, mr.message_id, added);
|
|
205
|
-
});
|
|
127
|
+
// Register this chat for userbot access
|
|
128
|
+
allowChat(ctx.chat.id);
|
|
129
|
+
// Only register groups for pulse (DMs don't need it — bot always responds)
|
|
130
|
+
const isGroup = ctx.chat.type === "group" || ctx.chat.type === "supergroup";
|
|
131
|
+
if (isGroup) registerChat(chatId);
|
|
132
|
+
const timestamp = ctx.message.date * 1000;
|
|
206
133
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
bio: req.bio,
|
|
218
|
-
at: Date.now(),
|
|
134
|
+
const entry = historyEntryFor(ctx.message);
|
|
135
|
+
if (entry) {
|
|
136
|
+
pushMessage(chatId, {
|
|
137
|
+
msgId,
|
|
138
|
+
senderId,
|
|
139
|
+
senderName: sender,
|
|
140
|
+
senderHandle,
|
|
141
|
+
replyToMsgId,
|
|
142
|
+
timestamp,
|
|
143
|
+
...entry,
|
|
219
144
|
});
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return next();
|
|
148
|
+
}
|
|
225
149
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
150
|
+
// ── Reaction tap — feed reactions on Talon's own messages to the soul ────
|
|
151
|
+
// Telegram only delivers `message_reaction` updates when subscribed via
|
|
152
|
+
// allowed_updates (see index.ts) and, in groups, when the bot is an admin.
|
|
153
|
+
// The handler is inert unless the soul is enabled.
|
|
154
|
+
function tapReaction(ctx: Filter<Context, "message_reaction">): void {
|
|
155
|
+
const mr = ctx.messageReaction;
|
|
156
|
+
// Ignore the bot reacting to messages itself.
|
|
157
|
+
if (mr.user?.id === ctx.me.id) return;
|
|
158
|
+
const added = newlyAddedEmojis(mr.old_reaction, mr.new_reaction);
|
|
159
|
+
recordReactionToBot(mr.chat.id, mr.message_id, added);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ── Join requests — cache for moderate(op="list_join_requests") ─────────
|
|
163
|
+
// Delivered only when subscribed via allowed_updates (see index.ts) and
|
|
164
|
+
// the bot admins a chat whose invite link requires approval. Stored, not
|
|
165
|
+
// enqueued: a join request isn't a conversation turn.
|
|
166
|
+
function cacheJoinRequest(ctx: Filter<Context, "chat_join_request">): void {
|
|
167
|
+
const req = ctx.chatJoinRequest;
|
|
168
|
+
recordJoinRequest(ctx.chat.id, {
|
|
169
|
+
userId: req.from.id,
|
|
170
|
+
name: getSenderName(req.from),
|
|
171
|
+
username: req.from.username,
|
|
172
|
+
bio: req.bio,
|
|
173
|
+
at: Date.now(),
|
|
234
174
|
});
|
|
175
|
+
log(
|
|
176
|
+
"bot",
|
|
177
|
+
`Join request for chat ${ctx.chat.id} from ${req.from.id} (@${req.from.username ?? "?"})`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
235
180
|
|
|
236
|
-
|
|
181
|
+
// ── Bot removed from group — revoke userbot access ─────────────────────
|
|
182
|
+
function revokeOnRemoval(ctx: Filter<Context, "my_chat_member">): void {
|
|
183
|
+
const newStatus = ctx.myChatMember.new_chat_member.status;
|
|
184
|
+
if (newStatus === "left" || newStatus === "kicked") {
|
|
185
|
+
const chatId = ctx.chat.id;
|
|
186
|
+
revokeChat(chatId);
|
|
187
|
+
log("bot", `Removed from chat ${chatId} — revoked userbot access`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ── Message handlers (delegated to handlers.ts) ──────────────────────────
|
|
192
|
+
function registerMessageHandlers(bot: Bot, config: TalonConfig): void {
|
|
237
193
|
bot.on("message:text", (ctx) => handleTextMessage(ctx, bot, config));
|
|
238
194
|
bot.on("message:photo", (ctx) => handlePhotoMessage(ctx, bot, config));
|
|
239
195
|
bot.on("message:document", (ctx) => handleDocumentMessage(ctx, bot, config));
|
|
@@ -248,3 +204,12 @@ export function registerMiddleware(bot: Bot, config: TalonConfig): void {
|
|
|
248
204
|
handleVideoNoteMessage(ctx, bot, config),
|
|
249
205
|
);
|
|
250
206
|
}
|
|
207
|
+
|
|
208
|
+
export function registerMiddleware(bot: Bot, config: TalonConfig): void {
|
|
209
|
+
bot.use(trackUpdateOffset);
|
|
210
|
+
bot.on("message", captureHistory);
|
|
211
|
+
bot.on("message_reaction", tapReaction);
|
|
212
|
+
bot.on("chat_join_request", cacheJoinRequest);
|
|
213
|
+
bot.on("my_chat_member", revokeOnRemoval);
|
|
214
|
+
registerMessageHandlers(bot, config);
|
|
215
|
+
}
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* Discord types leak in.
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
|
-
import type { TalonConfig } from "../../
|
|
29
|
+
import type { TalonConfig } from "../../core/config/index.js";
|
|
30
30
|
import type { Backend } from "../../core/agent-runtime/capabilities.js";
|
|
31
31
|
import {
|
|
32
32
|
buildModelMenuState,
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/** `/context` — the context window, broken down by segment. */
|
|
2
|
+
|
|
3
|
+
import pc from "picocolors";
|
|
4
|
+
import {
|
|
5
|
+
buildContextBreakdown,
|
|
6
|
+
estimateContextTokens,
|
|
7
|
+
apportionCells,
|
|
8
|
+
type ContextBreakdown,
|
|
9
|
+
type ContextSegmentKey,
|
|
10
|
+
} from "../../shared/status-context.js";
|
|
11
|
+
import { getRecentHistory } from "../../../storage/history.js";
|
|
12
|
+
import { formatTokenCount } from "../../shared/format.js";
|
|
13
|
+
import { getChatSettings } from "../../../storage/chat-settings.js";
|
|
14
|
+
import { resolveModelId as resolveModelName } from "../../../core/models/catalog.js";
|
|
15
|
+
import { getSessionInfo } from "../../../storage/sessions.js";
|
|
16
|
+
import type { Command, CommandContext } from "../command-registry.js";
|
|
17
|
+
|
|
18
|
+
// ── /context rendering ───────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
/** Each used segment gets its own colour; free is a dim hatch. */
|
|
21
|
+
const CONTEXT_SEGMENT_COLOR: Record<ContextSegmentKey, (s: string) => string> =
|
|
22
|
+
{
|
|
23
|
+
system: pc.blue,
|
|
24
|
+
tools: pc.yellow,
|
|
25
|
+
conversation: pc.cyan,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const CONTEXT_BAR_WIDTH = 42;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The segmented bar: one coloured run per segment (proportional to the window),
|
|
32
|
+
* then the free space as a dim `░` hatch. Cell counts come from
|
|
33
|
+
* `apportionCells`, so the runs always sum to exactly the bar width.
|
|
34
|
+
*/
|
|
35
|
+
function renderContextBar(bd: ContextBreakdown): string {
|
|
36
|
+
const weights = bd.segments.map((s) => s.tokens);
|
|
37
|
+
if (bd.windowKnown) weights.push(bd.free);
|
|
38
|
+
const cells = apportionCells(weights, CONTEXT_BAR_WIDTH);
|
|
39
|
+
let bar = "";
|
|
40
|
+
bd.segments.forEach((s, i) => {
|
|
41
|
+
bar += CONTEXT_SEGMENT_COLOR[s.key]("█".repeat(cells[i] ?? 0));
|
|
42
|
+
});
|
|
43
|
+
if (bd.windowKnown) {
|
|
44
|
+
bar += pc.dim("░".repeat(cells[bd.segments.length] ?? 0));
|
|
45
|
+
}
|
|
46
|
+
return bar;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** One aligned legend row per segment (and Free), colour-matched to the bar. */
|
|
50
|
+
function renderContextLegend(bd: ContextBreakdown): string[] {
|
|
51
|
+
const rows = bd.segments.map((s) => ({
|
|
52
|
+
dot: CONTEXT_SEGMENT_COLOR[s.key]("●"),
|
|
53
|
+
label: s.label,
|
|
54
|
+
tokens: s.tokens,
|
|
55
|
+
pct: s.pct,
|
|
56
|
+
}));
|
|
57
|
+
if (bd.windowKnown) {
|
|
58
|
+
rows.push({
|
|
59
|
+
dot: pc.dim("░"),
|
|
60
|
+
label: "Free",
|
|
61
|
+
tokens: bd.free,
|
|
62
|
+
pct: bd.freePct,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
const labelW = Math.max(...rows.map((r) => r.label.length));
|
|
66
|
+
const tokW = Math.max(...rows.map((r) => formatTokenCount(r.tokens).length));
|
|
67
|
+
return rows.map((r) => {
|
|
68
|
+
const label = r.label.padEnd(labelW);
|
|
69
|
+
const tok = formatTokenCount(r.tokens).padStart(tokW);
|
|
70
|
+
const pct = `${r.pct}%`.padStart(6);
|
|
71
|
+
return `${r.dot} ${label} ${pc.dim(tok)} ${pc.dim(pct)}`;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── /context ─────────────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
/** Window + a friendly model name, enriched from the backend like /status. */
|
|
78
|
+
async function resolveWindowAndModel(
|
|
79
|
+
be: CommandContext["backend"],
|
|
80
|
+
activeModel: string,
|
|
81
|
+
contextWindow: number,
|
|
82
|
+
): Promise<{ contextWindow: number; modelName: string }> {
|
|
83
|
+
let modelName = resolveModelName(activeModel);
|
|
84
|
+
if (be?.models?.getRawModelInfo) {
|
|
85
|
+
const mi = await be.models
|
|
86
|
+
.getRawModelInfo(activeModel)
|
|
87
|
+
.catch(() => undefined);
|
|
88
|
+
if (mi) {
|
|
89
|
+
if (mi.contextWindow) contextWindow ||= mi.contextWindow;
|
|
90
|
+
if (mi.displayName) modelName = mi.displayName;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return { contextWindow, modelName };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function writeContextReport(
|
|
97
|
+
ctx: CommandContext,
|
|
98
|
+
bd: ContextBreakdown,
|
|
99
|
+
modelName: string,
|
|
100
|
+
): void {
|
|
101
|
+
const windowStr = bd.windowKnown
|
|
102
|
+
? `${formatTokenCount(bd.max)} window`
|
|
103
|
+
: pc.dim("window unknown");
|
|
104
|
+
ctx.renderer.writeln(
|
|
105
|
+
` ${pc.bold("Context")} ${modelName} · ${windowStr}`,
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const usedStr = bd.windowKnown
|
|
109
|
+
? `${formatTokenCount(bd.used)} / ${formatTokenCount(bd.max)} · ${bd.usedPct}% used`
|
|
110
|
+
: `${formatTokenCount(bd.used)} used`;
|
|
111
|
+
ctx.renderer.writeln(
|
|
112
|
+
` ${bd.warn ? pc.yellow(usedStr) : pc.dim(usedStr)}${bd.warn ? pc.yellow(" · nearing limit") : ""}`,
|
|
113
|
+
);
|
|
114
|
+
ctx.renderer.writeln();
|
|
115
|
+
ctx.renderer.writeln(` ${renderContextBar(bd)}`);
|
|
116
|
+
ctx.renderer.writeln();
|
|
117
|
+
for (const line of renderContextLegend(bd)) {
|
|
118
|
+
ctx.renderer.writeln(` ${line}`);
|
|
119
|
+
}
|
|
120
|
+
// Explain only what's on screen: Tools is derivable (and shown) only
|
|
121
|
+
// when the backend reported a real fill.
|
|
122
|
+
const hasTools = bd.segments.some((s) => s.key === "tools");
|
|
123
|
+
ctx.renderer.writeln(
|
|
124
|
+
` ${pc.dim(
|
|
125
|
+
hasTools
|
|
126
|
+
? "System measured; Conversation estimated; Tools is the remainder."
|
|
127
|
+
: "System measured; Conversation estimated from stored history.",
|
|
128
|
+
)}`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export const contextCommand: Command = {
|
|
133
|
+
name: "context",
|
|
134
|
+
aliases: ["ctx"],
|
|
135
|
+
description: "Context-window usage, broken down",
|
|
136
|
+
async handler(_args, ctx) {
|
|
137
|
+
const chatId = ctx.chatId();
|
|
138
|
+
const u = getSessionInfo(chatId).usage;
|
|
139
|
+
const be = ctx.backend;
|
|
140
|
+
const activeModel = getChatSettings(chatId).model ?? ctx.config.model;
|
|
141
|
+
|
|
142
|
+
const { contextWindow, modelName } = await resolveWindowAndModel(
|
|
143
|
+
be,
|
|
144
|
+
activeModel,
|
|
145
|
+
u.contextWindow,
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
// System = the actual frozen prompt the model is running with. Measure
|
|
149
|
+
// it directly rather than rebuilding, so the number matches what was
|
|
150
|
+
// really sent (the prompt is frozen per session by design).
|
|
151
|
+
const parts = ctx.config.systemPromptParts;
|
|
152
|
+
const systemText = parts
|
|
153
|
+
? [parts.staticText, parts.dynamicText].filter(Boolean).join("\n")
|
|
154
|
+
: (ctx.config.systemPrompt ?? "");
|
|
155
|
+
const systemTokens = estimateContextTokens(systemText);
|
|
156
|
+
|
|
157
|
+
// Conversation = stored history for this chat. An estimate: the model's
|
|
158
|
+
// real in-window history may be smaller after compaction, which the
|
|
159
|
+
// breakdown clamps against the authoritative fill.
|
|
160
|
+
const history = getRecentHistory(chatId, 2000);
|
|
161
|
+
const conversationTokens = estimateContextTokens(
|
|
162
|
+
history.map((m) => m.text ?? "").join("\n"),
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const bd = buildContextBreakdown({
|
|
166
|
+
contextTokens: u.contextTokens,
|
|
167
|
+
contextWindow,
|
|
168
|
+
systemTokens,
|
|
169
|
+
conversationTokens,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
ctx.renderer.writeln();
|
|
173
|
+
if (!bd.known) {
|
|
174
|
+
ctx.renderer.writeln(
|
|
175
|
+
` ${pc.bold("Context")} ${pc.dim("no usage yet — send a message first")}`,
|
|
176
|
+
);
|
|
177
|
+
ctx.reprompt();
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
writeContextReport(ctx, bd, modelName);
|
|
182
|
+
ctx.reprompt();
|
|
183
|
+
},
|
|
184
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** `/help` and `/quit` — the registry's own commands. */
|
|
2
|
+
|
|
3
|
+
import pc from "picocolors";
|
|
4
|
+
import { getCommands, type Command } from "../command-registry.js";
|
|
5
|
+
|
|
6
|
+
export const helpCommand: Command = {
|
|
7
|
+
name: "help",
|
|
8
|
+
description: "Show available commands",
|
|
9
|
+
async handler(_args, ctx) {
|
|
10
|
+
ctx.renderer.writeln();
|
|
11
|
+
for (const cmd of getCommands()) {
|
|
12
|
+
if (cmd.name === "help") continue; // show help last
|
|
13
|
+
const nameStr = `/${cmd.name}`;
|
|
14
|
+
const argStr = cmd.argHint ? ` ${cmd.argHint}` : "";
|
|
15
|
+
const pad = " ".repeat(Math.max(1, 16 - nameStr.length - argStr.length));
|
|
16
|
+
ctx.renderer.writeln(
|
|
17
|
+
` ${pc.cyan(nameStr)}${pc.dim(argStr)}${pad}${pc.dim(cmd.description)}`,
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
// Help itself at the end
|
|
21
|
+
ctx.renderer.writeln(
|
|
22
|
+
` ${pc.cyan("/help")} ${pc.dim("Show available commands")}`,
|
|
23
|
+
);
|
|
24
|
+
ctx.reprompt();
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const quitCommand: Command = {
|
|
29
|
+
name: "quit",
|
|
30
|
+
aliases: ["exit"],
|
|
31
|
+
description: "Exit",
|
|
32
|
+
async handler(_args, ctx) {
|
|
33
|
+
ctx.close();
|
|
34
|
+
},
|
|
35
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in terminal commands, in registration order. The order is what
|
|
3
|
+
* `/help` prints and what `getCommands()` returns, so it is pinned by test.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Command } from "../command-registry.js";
|
|
7
|
+
import { modelCommand, effortCommand } from "./model.js";
|
|
8
|
+
import { statusCommand } from "./status.js";
|
|
9
|
+
import { contextCommand } from "./context.js";
|
|
10
|
+
import { resetCommand, resumeCommand, renameCommand } from "./session.js";
|
|
11
|
+
import { helpCommand, quitCommand } from "./help.js";
|
|
12
|
+
|
|
13
|
+
export const BUILTIN_COMMANDS: readonly Command[] = [
|
|
14
|
+
modelCommand,
|
|
15
|
+
effortCommand,
|
|
16
|
+
statusCommand,
|
|
17
|
+
contextCommand,
|
|
18
|
+
resetCommand,
|
|
19
|
+
resumeCommand,
|
|
20
|
+
renameCommand,
|
|
21
|
+
helpCommand,
|
|
22
|
+
quitCommand,
|
|
23
|
+
];
|