talon-agent 1.9.0 → 1.9.2
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 +4 -3
- package/prompts/mempalace.md +24 -4
- package/src/__tests__/disallowed-tools.test.ts +64 -0
- package/src/__tests__/handlers-stream.test.ts +0 -5
- package/src/__tests__/handlers.test.ts +6 -12
- package/src/__tests__/log.test.ts +4 -3
- package/src/__tests__/tool-functional.test.ts +615 -0
- package/src/__tests__/tool-id-coercion.test.ts +136 -0
- package/src/backend/claude-sdk/handler.ts +2 -3
- package/src/core/constants.ts +5 -0
- package/src/core/gateway.ts +12 -2
- package/src/core/tools/history.ts +6 -5
- package/src/core/tools/members.ts +2 -1
- package/src/core/tools/messaging.ts +9 -8
- package/src/core/tools/schemas.ts +34 -0
- package/src/core/tools/stickers.ts +3 -2
- package/src/frontend/telegram/commands.ts +2 -3
- package/src/frontend/telegram/handlers.ts +4 -12
- package/src/util/log.ts +4 -1
- package/src/__tests__/prompt-builder-extended.test.ts +0 -296
- package/src/__tests__/prompt-builder.test.ts +0 -106
- package/src/core/prompt-builder.ts +0 -40
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression + audit tests — ID-shaped tool params use the strict
|
|
3
|
+
* shared `idSchema` from `core/tools/schemas.ts`.
|
|
4
|
+
*
|
|
5
|
+
* Background: some MCP transport / model paths deliver `message_id`,
|
|
6
|
+
* `user_id`, `reply_to`, `offset_id` as JSON strings ("2081") rather
|
|
7
|
+
* than numbers (2081). Plain `z.number()` rejects those with
|
|
8
|
+
* `expected number, received string`, which manifested as
|
|
9
|
+
* `react`/`pin_message`/`get_member_info`/`download_media` failing
|
|
10
|
+
* out of nowhere even when the model formatted the call correctly
|
|
11
|
+
* for a number-typed JSON Schema field.
|
|
12
|
+
*
|
|
13
|
+
* Initial fix used `z.coerce.number().int()`, but per Copilot review
|
|
14
|
+
* that was too lax — `""`/`null` coerce to 0 and `true` to 1, which
|
|
15
|
+
* then pass `.int()` and reach the bot API. The current `idSchema` is
|
|
16
|
+
* a union: `z.number().int().positive()` OR a digit-only string that
|
|
17
|
+
* gets transformed to a positive integer. Everything else is rejected.
|
|
18
|
+
*/
|
|
19
|
+
import { describe, it, expect } from "vitest";
|
|
20
|
+
import { z } from "zod";
|
|
21
|
+
import { ALL_TOOLS } from "../core/tools/index.js";
|
|
22
|
+
|
|
23
|
+
const ID_FIELD_NAMES = new Set([
|
|
24
|
+
"message_id",
|
|
25
|
+
"user_id",
|
|
26
|
+
"reply_to",
|
|
27
|
+
"offset_id",
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
function getIdSchema(toolName: string, field: string): z.ZodTypeAny {
|
|
31
|
+
const tool = ALL_TOOLS.find((t) => t.name === toolName);
|
|
32
|
+
if (!tool) throw new Error(`tool ${toolName} not found`);
|
|
33
|
+
const schema = (tool.schema as Record<string, z.ZodTypeAny>)[field];
|
|
34
|
+
if (!schema) throw new Error(`field ${field} not found on ${toolName}`);
|
|
35
|
+
return schema;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe("ID-shaped tool params accept stringified numbers", () => {
|
|
39
|
+
// Spot-check a representative set across all four files.
|
|
40
|
+
const cases: Array<[string, string]> = [
|
|
41
|
+
["react", "message_id"],
|
|
42
|
+
["edit_message", "message_id"],
|
|
43
|
+
["delete_message", "message_id"],
|
|
44
|
+
["forward_message", "message_id"],
|
|
45
|
+
["pin_message", "message_id"],
|
|
46
|
+
["unpin_message", "message_id"],
|
|
47
|
+
["stop_poll", "message_id"],
|
|
48
|
+
["send", "reply_to"],
|
|
49
|
+
["get_member_info", "user_id"],
|
|
50
|
+
["create_sticker_set", "user_id"],
|
|
51
|
+
["add_sticker_to_set", "user_id"],
|
|
52
|
+
["read_chat_history", "offset_id"],
|
|
53
|
+
["get_message_by_id", "message_id"],
|
|
54
|
+
["download_media", "message_id"],
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
for (const [tool, field] of cases) {
|
|
58
|
+
it(`${tool}.${field} accepts a number`, () => {
|
|
59
|
+
const s = getIdSchema(tool, field);
|
|
60
|
+
const out = s.parse(2081);
|
|
61
|
+
expect(out).toBe(2081);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it(`${tool}.${field} accepts a numeric string and coerces it`, () => {
|
|
65
|
+
const s = getIdSchema(tool, field);
|
|
66
|
+
const out = s.parse("2081");
|
|
67
|
+
expect(out).toBe(2081);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it(`${tool}.${field} rejects a non-numeric string`, () => {
|
|
71
|
+
const s = getIdSchema(tool, field);
|
|
72
|
+
expect(() => s.parse("abc")).toThrow();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it(`${tool}.${field} rejects a non-integer`, () => {
|
|
76
|
+
const s = getIdSchema(tool, field);
|
|
77
|
+
expect(() => s.parse(1.5)).toThrow();
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("Audit: every ID-shaped field in tool schemas is the strict idSchema", () => {
|
|
83
|
+
/**
|
|
84
|
+
* For each ID field on every tool, this test asserts the full
|
|
85
|
+
* accept/reject contract of `idSchema` — not just "accepts a
|
|
86
|
+
* string." That guards against a future replacement that passes
|
|
87
|
+
* the loose check (`safeParse("123")` succeeds) but doesn't
|
|
88
|
+
* actually return a positive integer (e.g. `z.string()` would
|
|
89
|
+
* accept "123" and return the string "123").
|
|
90
|
+
*/
|
|
91
|
+
for (const tool of ALL_TOOLS) {
|
|
92
|
+
const schema = tool.schema as Record<string, unknown>;
|
|
93
|
+
for (const field of Object.keys(schema)) {
|
|
94
|
+
if (!ID_FIELD_NAMES.has(field)) continue;
|
|
95
|
+
const zSchema = schema[field] as z.ZodTypeAny;
|
|
96
|
+
|
|
97
|
+
it(`${tool.name}.${field}: accepts a positive integer number`, () => {
|
|
98
|
+
const r = zSchema.safeParse(2081);
|
|
99
|
+
expect(r.success).toBe(true);
|
|
100
|
+
if (r.success) expect(r.data).toBe(2081);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it(`${tool.name}.${field}: accepts a digit string and returns a number`, () => {
|
|
104
|
+
const r = zSchema.safeParse("2081");
|
|
105
|
+
expect(r.success).toBe(true);
|
|
106
|
+
if (r.success) {
|
|
107
|
+
expect(typeof r.data).toBe("number");
|
|
108
|
+
expect(Number.isInteger(r.data as number)).toBe(true);
|
|
109
|
+
expect(r.data).toBe(2081);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Reject the inputs that bare `z.coerce.number().int()` was too
|
|
114
|
+
// permissive about — empty/whitespace/null/booleans coerce to
|
|
115
|
+
// 0/0/0/1 and would pass `.int()`. The strict union rejects all.
|
|
116
|
+
const rejectCases: Array<[unknown, string]> = [
|
|
117
|
+
["", "empty string"],
|
|
118
|
+
[" ", "whitespace string"],
|
|
119
|
+
["abc", "non-numeric string"],
|
|
120
|
+
["2081abc", "mixed string"],
|
|
121
|
+
[null, "null"],
|
|
122
|
+
[true, "true"],
|
|
123
|
+
[false, "false"],
|
|
124
|
+
[0, "zero"],
|
|
125
|
+
[-1, "negative integer"],
|
|
126
|
+
[1.5, "non-integer"],
|
|
127
|
+
];
|
|
128
|
+
for (const [bad, label] of rejectCases) {
|
|
129
|
+
it(`${tool.name}.${field}: rejects ${label}`, () => {
|
|
130
|
+
const r = zSchema.safeParse(bad);
|
|
131
|
+
expect(r.success).toBe(false);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
});
|
|
@@ -227,10 +227,9 @@ export async function handleMessage(
|
|
|
227
227
|
// ── Build result ──────────────────────────────────────────────────────────
|
|
228
228
|
|
|
229
229
|
state.allResponseText += state.currentBlockText;
|
|
230
|
-
const
|
|
231
|
-
state.sdkInputTokens + state.sdkCacheRead + state.sdkCacheWrite;
|
|
230
|
+
const cacheTotal = state.sdkInputTokens + state.sdkCacheRead;
|
|
232
231
|
const cacheHitPct =
|
|
233
|
-
|
|
232
|
+
cacheTotal > 0 ? Math.round((state.sdkCacheRead / cacheTotal) * 100) : 0;
|
|
234
233
|
|
|
235
234
|
log(
|
|
236
235
|
"agent",
|
package/src/core/constants.ts
CHANGED
|
@@ -21,6 +21,11 @@ export const DISALLOWED_TOOLS_CORE = [
|
|
|
21
21
|
"TaskOutput",
|
|
22
22
|
"TaskStop",
|
|
23
23
|
"AskUserQuestion",
|
|
24
|
+
// ScheduleWakeup is a /loop-skill-only tool. Calling it outside /loop dynamic
|
|
25
|
+
// mode registers a wakeup the runtime never fires, leaving the dispatcher
|
|
26
|
+
// wedged with the chat lock held until manual restart. Confirmed root cause
|
|
27
|
+
// of a 35-minute hang on 2026-04-27 (talon.log [e2589f7e]).
|
|
28
|
+
"ScheduleWakeup",
|
|
24
29
|
] as const;
|
|
25
30
|
|
|
26
31
|
/** Disallowed tools for background agents — dream and heartbeat (core + Agent). */
|
package/src/core/gateway.ts
CHANGED
|
@@ -267,9 +267,19 @@ export class Gateway {
|
|
|
267
267
|
res.end(json);
|
|
268
268
|
} catch (err) {
|
|
269
269
|
if (res.headersSent) return;
|
|
270
|
-
|
|
270
|
+
// Log full error (incl. stack via logError's structured `stack`
|
|
271
|
+
// field) on the server; return a generic message to the client so
|
|
272
|
+
// we don't leak implementation details.
|
|
273
|
+
// CodeQL: js/stack-trace-exposure (alert #4).
|
|
274
|
+
logError(
|
|
275
|
+
"gateway",
|
|
276
|
+
`Unhandled error on ${req.method} ${req.url}`,
|
|
277
|
+
err,
|
|
278
|
+
);
|
|
271
279
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
272
|
-
res.end(
|
|
280
|
+
res.end(
|
|
281
|
+
JSON.stringify({ ok: false, error: "Internal server error" }),
|
|
282
|
+
);
|
|
273
283
|
}
|
|
274
284
|
},
|
|
275
285
|
);
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import type { ToolDefinition } from "./types.js";
|
|
7
|
+
import { idSchema } from "./schemas.js";
|
|
7
8
|
|
|
8
9
|
export const historyTools: ToolDefinition[] = [
|
|
9
10
|
{
|
|
@@ -19,7 +20,7 @@ export const historyTools: ToolDefinition[] = [
|
|
|
19
20
|
.string()
|
|
20
21
|
.optional()
|
|
21
22
|
.describe("Fetch messages before this date (ISO format)"),
|
|
22
|
-
offset_id:
|
|
23
|
+
offset_id: idSchema.optional().describe("Fetch before this message ID"),
|
|
23
24
|
},
|
|
24
25
|
execute: (params, bridge) =>
|
|
25
26
|
bridge("read_history", {
|
|
@@ -58,7 +59,7 @@ export const historyTools: ToolDefinition[] = [
|
|
|
58
59
|
{
|
|
59
60
|
name: "get_message_by_id",
|
|
60
61
|
description: "Get a specific message by ID.",
|
|
61
|
-
schema: { message_id:
|
|
62
|
+
schema: { message_id: idSchema },
|
|
62
63
|
execute: (params, bridge) => bridge("get_message_by_id", params),
|
|
63
64
|
frontends: ["telegram"],
|
|
64
65
|
tag: "history",
|
|
@@ -69,9 +70,9 @@ export const historyTools: ToolDefinition[] = [
|
|
|
69
70
|
description:
|
|
70
71
|
"Download a photo, document, or other media from a message by its ID. Saves the file to the workspace and returns the file path so you can read/analyze it. Use this when you see a [photo] or [document] in chat history but don't have the file.",
|
|
71
72
|
schema: {
|
|
72
|
-
message_id:
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
message_id: idSchema.describe(
|
|
74
|
+
"Message ID containing the media to download",
|
|
75
|
+
),
|
|
75
76
|
},
|
|
76
77
|
execute: (params, bridge) => bridge("download_media", params),
|
|
77
78
|
frontends: ["telegram"],
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import type { ToolDefinition } from "./types.js";
|
|
7
|
+
import { idSchema } from "./schemas.js";
|
|
7
8
|
|
|
8
9
|
export const memberTools: ToolDefinition[] = [
|
|
9
10
|
{
|
|
@@ -19,7 +20,7 @@ export const memberTools: ToolDefinition[] = [
|
|
|
19
20
|
{
|
|
20
21
|
name: "get_member_info",
|
|
21
22
|
description: "Get detailed info about a user by ID.",
|
|
22
|
-
schema: { user_id:
|
|
23
|
+
schema: { user_id: idSchema },
|
|
23
24
|
execute: (params, bridge) => bridge("get_member_info", params),
|
|
24
25
|
frontends: ["telegram"],
|
|
25
26
|
tag: "members",
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import type { ToolDefinition } from "./types.js";
|
|
7
|
+
import { idSchema } from "./schemas.js";
|
|
7
8
|
|
|
8
9
|
export const messagingTools: ToolDefinition[] = [
|
|
9
10
|
// ── Telegram unified send ─────────────────────────────────────────────
|
|
@@ -43,7 +44,7 @@ Examples:
|
|
|
43
44
|
.string()
|
|
44
45
|
.optional()
|
|
45
46
|
.describe("Message text (for type=text). Supports Markdown."),
|
|
46
|
-
reply_to:
|
|
47
|
+
reply_to: idSchema.optional().describe("Message ID to reply to"),
|
|
47
48
|
file_path: z
|
|
48
49
|
.string()
|
|
49
50
|
.optional()
|
|
@@ -227,7 +228,7 @@ Example: send_message_with_buttons(text="Choose:", rows=[[{"text":"Docs","url":"
|
|
|
227
228
|
description:
|
|
228
229
|
"Add an emoji reaction to a message. Valid: 👍 👎 ❤ 🔥 🥰 👏 😁 🤔 🤯 😱 🤬 😢 🎉 🤩 🤮 💩 🙏 👌 🕊 🤡 🥱 🥴 😍 🐳 ❤🔥 🌚 🌭 💯 🤣 ⚡ 🍌 🏆 💔 🤨 😐 🍓 🍾 💋 🖕 😈 😴 😭 🤓 👻 👨💻 👀 🎃 🙈 😇 😨 🤝 ✍ 🤗 🫡 🎅 🎄 ☃ 💅 🤪 🗿 🆒 💘 🙉 🦄 😘 💊 🙊 😎 👾 🤷 🤷♂ 🤷♀ 😡",
|
|
229
230
|
schema: {
|
|
230
|
-
message_id:
|
|
231
|
+
message_id: idSchema.describe("Message ID"),
|
|
231
232
|
emoji: z.string().describe("Reaction emoji"),
|
|
232
233
|
},
|
|
233
234
|
execute: (params, bridge) => bridge("react", params),
|
|
@@ -239,7 +240,7 @@ Example: send_message_with_buttons(text="Choose:", rows=[[{"text":"Docs","url":"
|
|
|
239
240
|
{
|
|
240
241
|
name: "edit_message",
|
|
241
242
|
description: "Edit a previously sent message.",
|
|
242
|
-
schema: { message_id:
|
|
243
|
+
schema: { message_id: idSchema, text: z.string() },
|
|
243
244
|
execute: (params, bridge) => bridge("edit_message", params),
|
|
244
245
|
frontends: ["telegram"],
|
|
245
246
|
tag: "messaging",
|
|
@@ -249,7 +250,7 @@ Example: send_message_with_buttons(text="Choose:", rows=[[{"text":"Docs","url":"
|
|
|
249
250
|
{
|
|
250
251
|
name: "delete_message",
|
|
251
252
|
description: "Delete a message.",
|
|
252
|
-
schema: { message_id:
|
|
253
|
+
schema: { message_id: idSchema },
|
|
253
254
|
execute: (params, bridge) => bridge("delete_message", params),
|
|
254
255
|
frontends: ["telegram"],
|
|
255
256
|
tag: "messaging",
|
|
@@ -259,7 +260,7 @@ Example: send_message_with_buttons(text="Choose:", rows=[[{"text":"Docs","url":"
|
|
|
259
260
|
{
|
|
260
261
|
name: "forward_message",
|
|
261
262
|
description: "Forward a message within the chat.",
|
|
262
|
-
schema: { message_id:
|
|
263
|
+
schema: { message_id: idSchema },
|
|
263
264
|
execute: (params, bridge) => bridge("forward_message", params),
|
|
264
265
|
frontends: ["telegram"],
|
|
265
266
|
tag: "messaging",
|
|
@@ -269,7 +270,7 @@ Example: send_message_with_buttons(text="Choose:", rows=[[{"text":"Docs","url":"
|
|
|
269
270
|
{
|
|
270
271
|
name: "pin_message",
|
|
271
272
|
description: "Pin a message.",
|
|
272
|
-
schema: { message_id:
|
|
273
|
+
schema: { message_id: idSchema },
|
|
273
274
|
execute: (params, bridge) => bridge("pin_message", params),
|
|
274
275
|
frontends: ["telegram"],
|
|
275
276
|
tag: "messaging",
|
|
@@ -279,7 +280,7 @@ Example: send_message_with_buttons(text="Choose:", rows=[[{"text":"Docs","url":"
|
|
|
279
280
|
{
|
|
280
281
|
name: "unpin_message",
|
|
281
282
|
description: "Unpin a message.",
|
|
282
|
-
schema: { message_id:
|
|
283
|
+
schema: { message_id: idSchema.optional() },
|
|
283
284
|
execute: (params, bridge) => bridge("unpin_message", params),
|
|
284
285
|
frontends: ["telegram"],
|
|
285
286
|
tag: "messaging",
|
|
@@ -291,7 +292,7 @@ Example: send_message_with_buttons(text="Choose:", rows=[[{"text":"Docs","url":"
|
|
|
291
292
|
description:
|
|
292
293
|
"Stop an active poll and get the final results. Returns vote counts for each option.",
|
|
293
294
|
schema: {
|
|
294
|
-
message_id:
|
|
295
|
+
message_id: idSchema.describe("Message ID of the poll to stop"),
|
|
295
296
|
},
|
|
296
297
|
execute: (params, bridge) => bridge("stop_poll", params),
|
|
297
298
|
frontends: ["telegram"],
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared zod schema fragments for tool input definitions.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Telegram-style positive integer ID (message_id, user_id, reply_to,
|
|
9
|
+
* offset_id, etc.).
|
|
10
|
+
*
|
|
11
|
+
* Accepts:
|
|
12
|
+
* - actual numbers that are positive integers (`2081`)
|
|
13
|
+
* - digit-only strings (`"2081"`) which are transformed into numbers
|
|
14
|
+
*
|
|
15
|
+
* Rejects:
|
|
16
|
+
* - non-numeric strings (`"abc"`, `"2081abc"`, `""`, `" "`)
|
|
17
|
+
* - booleans, `null`, `undefined` (would otherwise coerce to 0/1)
|
|
18
|
+
* - non-integer numbers (`1.5`)
|
|
19
|
+
* - zero and negatives
|
|
20
|
+
*
|
|
21
|
+
* Use this instead of `z.number()` or `z.coerce.number()` for any ID
|
|
22
|
+
* field on tool input schemas. The plain coercion path was too lax —
|
|
23
|
+
* `z.coerce.number().int()` happily turns `null`/`""` into `0` and
|
|
24
|
+
* `true` into `1`, both of which the Telegram bot API would then
|
|
25
|
+
* dispatch to. The strict union avoids that.
|
|
26
|
+
*/
|
|
27
|
+
export const idSchema = z.union([
|
|
28
|
+
z.number().int().positive(),
|
|
29
|
+
z
|
|
30
|
+
.string()
|
|
31
|
+
.regex(/^\d+$/, "must be a positive integer")
|
|
32
|
+
.transform((s) => Number(s))
|
|
33
|
+
.pipe(z.number().int().positive()),
|
|
34
|
+
]);
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import type { ToolDefinition } from "./types.js";
|
|
7
|
+
import { idSchema } from "./schemas.js";
|
|
7
8
|
|
|
8
9
|
export const stickerTools: ToolDefinition[] = [
|
|
9
10
|
{
|
|
@@ -56,7 +57,7 @@ The set name will automatically get "_by_<botname>" appended if needed.
|
|
|
56
57
|
|
|
57
58
|
Example: create_sticker_set(user_id=123, name="cool_pack", title="Cool Stickers", file_path="/path/to/sticker.png", emoji_list=["😎"])`,
|
|
58
59
|
schema: {
|
|
59
|
-
user_id:
|
|
60
|
+
user_id: idSchema.describe("Telegram user ID who will own the pack"),
|
|
60
61
|
name: z
|
|
61
62
|
.string()
|
|
62
63
|
.describe(
|
|
@@ -85,7 +86,7 @@ Example: create_sticker_set(user_id=123, name="cool_pack", title="Cool Stickers"
|
|
|
85
86
|
description:
|
|
86
87
|
"Add a new sticker to an existing sticker pack created by the bot.",
|
|
87
88
|
schema: {
|
|
88
|
-
user_id:
|
|
89
|
+
user_id: idSchema.describe("Telegram user ID who owns the pack"),
|
|
89
90
|
name: z.string().describe("Sticker set name (including _by_<botname>)"),
|
|
90
91
|
file_path: z.string().describe("Path to the sticker image file"),
|
|
91
92
|
emoji_list: z
|
|
@@ -529,10 +529,9 @@ export function registerCommands(
|
|
|
529
529
|
"\u2588".repeat(filled) + "\u2591".repeat(barLen - filled);
|
|
530
530
|
const contextWarn = ctxPct >= 80 ? " \u26A0\uFE0F consider /reset" : "";
|
|
531
531
|
|
|
532
|
-
const
|
|
533
|
-
displayInputTokens + displayCacheRead + displayCacheWrite;
|
|
532
|
+
const cacheTotal = displayInputTokens + displayCacheRead;
|
|
534
533
|
const cacheHitPct =
|
|
535
|
-
|
|
534
|
+
cacheTotal > 0 ? Math.round((displayCacheRead / cacheTotal) * 100) : 0;
|
|
536
535
|
|
|
537
536
|
const avgResponseMs =
|
|
538
537
|
info.turns > 0 && u.totalResponseMs
|
|
@@ -8,10 +8,6 @@ import type { TalonConfig } from "../../util/config.js";
|
|
|
8
8
|
import { markdownToTelegramHtml, escapeHtml } from "./formatting.js";
|
|
9
9
|
import { execute } from "../../core/dispatcher.js";
|
|
10
10
|
import { classify, friendlyMessage } from "../../core/errors.js";
|
|
11
|
-
import {
|
|
12
|
-
enrichDMPrompt,
|
|
13
|
-
enrichGroupPrompt,
|
|
14
|
-
} from "../../core/prompt-builder.js";
|
|
15
11
|
import { writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
16
12
|
import { resolve } from "node:path";
|
|
17
13
|
import {
|
|
@@ -759,19 +755,15 @@ async function processAndReply(params: ProcessAndReplyParams): Promise<void> {
|
|
|
759
755
|
stream,
|
|
760
756
|
);
|
|
761
757
|
|
|
762
|
-
//
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
enrichedPrompt = enrichDMPrompt(prompt, senderName, senderUsername);
|
|
766
|
-
if (senderId) trackDmUser(senderId, senderName, senderUsername);
|
|
767
|
-
} else if (isGroup && senderId) {
|
|
768
|
-
enrichedPrompt = enrichGroupPrompt(prompt, String(chatId), senderId);
|
|
758
|
+
// Track first-time DM users for logging (no prompt mutation).
|
|
759
|
+
if (!isGroup && senderName && senderId) {
|
|
760
|
+
trackDmUser(senderId, senderName, senderUsername);
|
|
769
761
|
}
|
|
770
762
|
|
|
771
763
|
const result = await execute({
|
|
772
764
|
chatId: String(chatId),
|
|
773
765
|
numericChatId,
|
|
774
|
-
prompt
|
|
766
|
+
prompt,
|
|
775
767
|
senderName,
|
|
776
768
|
isGroup,
|
|
777
769
|
messageId,
|
package/src/util/log.ts
CHANGED
|
@@ -128,7 +128,10 @@ export function logError(
|
|
|
128
128
|
err?: unknown,
|
|
129
129
|
): void {
|
|
130
130
|
if (err instanceof Error) {
|
|
131
|
-
|
|
131
|
+
// Capture both the concise message (for log consumers that look at `err`)
|
|
132
|
+
// and the full stack (for diagnostics). pino-pretty renders the `stack`
|
|
133
|
+
// field on its own line; JSON consumers can read either field.
|
|
134
|
+
logger.error({ component, err: err.message, stack: err.stack }, message);
|
|
132
135
|
} else if (err !== undefined) {
|
|
133
136
|
logger.error({ component, err: String(err) }, message);
|
|
134
137
|
} else {
|