talon-agent 1.9.0 → 1.9.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "talon-agent",
3
- "version": "1.9.0",
3
+ "version": "1.9.1",
4
4
  "description": "Multi-frontend AI agent with full tool access, streaming, cron jobs, and plugin system",
5
5
  "author": "Dylan Neve",
6
6
  "license": "MIT",
@@ -0,0 +1,64 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import {
4
+ DISALLOWED_TOOLS_BACKGROUND,
5
+ DISALLOWED_TOOLS_CORE,
6
+ } from "../core/constants.js";
7
+ import { DISALLOWED_TOOLS_CHAT } from "../backend/claude-sdk/constants.js";
8
+
9
+ describe("disallowed tool lists", () => {
10
+ describe("DISALLOWED_TOOLS_CORE", () => {
11
+ it("blocks interactive/planning tools that are nonsensical in headless contexts", () => {
12
+ const expected = [
13
+ "EnterPlanMode",
14
+ "ExitPlanMode",
15
+ "EnterWorktree",
16
+ "ExitWorktree",
17
+ "TodoWrite",
18
+ "TodoRead",
19
+ "TaskCreate",
20
+ "TaskUpdate",
21
+ "TaskGet",
22
+ "TaskList",
23
+ "TaskOutput",
24
+ "TaskStop",
25
+ "AskUserQuestion",
26
+ ];
27
+ for (const tool of expected) {
28
+ expect(DISALLOWED_TOOLS_CORE).toContain(tool);
29
+ }
30
+ });
31
+
32
+ it("blocks ScheduleWakeup — /loop-skill-only tool that wedges the dispatcher when called outside /loop mode", () => {
33
+ // Confirmed root cause of a 35-minute hang on 2026-04-27.
34
+ // ScheduleWakeup registers a wakeup the runtime never fires, so the
35
+ // chat lock is held indefinitely until manual restart.
36
+ expect(DISALLOWED_TOOLS_CORE).toContain("ScheduleWakeup");
37
+ });
38
+ });
39
+
40
+ describe("DISALLOWED_TOOLS_BACKGROUND", () => {
41
+ it("inherits everything from CORE", () => {
42
+ for (const tool of DISALLOWED_TOOLS_CORE) {
43
+ expect(DISALLOWED_TOOLS_BACKGROUND).toContain(tool);
44
+ }
45
+ });
46
+
47
+ it("additionally blocks Agent (no nested agents in dream/heartbeat)", () => {
48
+ expect(DISALLOWED_TOOLS_BACKGROUND).toContain("Agent");
49
+ });
50
+ });
51
+
52
+ describe("DISALLOWED_TOOLS_CHAT", () => {
53
+ it("inherits everything from CORE", () => {
54
+ for (const tool of DISALLOWED_TOOLS_CORE) {
55
+ expect(DISALLOWED_TOOLS_CHAT).toContain(tool);
56
+ }
57
+ });
58
+
59
+ it("additionally blocks Claude's built-in web tools (replaced by Brave MCP)", () => {
60
+ expect(DISALLOWED_TOOLS_CHAT).toContain("WebSearch");
61
+ expect(DISALLOWED_TOOLS_CHAT).toContain("WebFetch");
62
+ });
63
+ });
64
+ });
@@ -29,11 +29,6 @@ vi.mock("../core/errors.js", () => ({
29
29
  TalonError: class TalonError extends Error {},
30
30
  }));
31
31
 
32
- vi.mock("../core/prompt-builder.js", () => ({
33
- enrichDMPrompt: vi.fn((p: string) => p),
34
- enrichGroupPrompt: vi.fn((p: string) => p),
35
- }));
36
-
37
32
  vi.mock("../storage/daily-log.js", () => ({
38
33
  appendDailyLog: vi.fn(),
39
34
  appendDailyLogResponse: vi.fn(),
@@ -5,10 +5,6 @@ const executeMock = vi.hoisted(() => vi.fn());
5
5
  vi.mock("../core/dispatcher.js", () => ({
6
6
  execute: executeMock,
7
7
  }));
8
- vi.mock("../core/prompt-builder.js", () => ({
9
- enrichDMPrompt: vi.fn((p: string) => p),
10
- enrichGroupPrompt: vi.fn((p: string) => p),
11
- }));
12
8
  vi.mock("../storage/daily-log.js", () => ({
13
9
  appendDailyLog: vi.fn(),
14
10
  appendDailyLogResponse: vi.fn(),
@@ -2641,11 +2637,8 @@ describe("handleStickerMessage — video sticker branch (L835 TRUE)", () => {
2641
2637
  }, 3000);
2642
2638
  });
2643
2639
 
2644
- describe("processAndReply — group message without senderId (L552 FALSE branch)", () => {
2645
- it("skips enrichGroupPrompt when senderId is undefined in group", async () => {
2646
- const { enrichGroupPrompt } = await import("../core/prompt-builder.js");
2647
- (enrichGroupPrompt as ReturnType<typeof vi.fn>).mockClear();
2648
-
2640
+ describe("processAndReply — group message without senderId", () => {
2641
+ it("processes anonymous group messages without mutating the prompt", async () => {
2649
2642
  executeMock.mockResolvedValueOnce({
2650
2643
  text: "",
2651
2644
  durationMs: 10,
@@ -2672,13 +2665,14 @@ describe("processAndReply — group message without senderId (L552 FALSE branch)
2672
2665
  await handleTextMessage(ctx, mockBot, mockConfig);
2673
2666
  await new Promise((r) => setTimeout(r, 700));
2674
2667
 
2675
- // enrichGroupPrompt should NOT have been called (senderId falsy)
2676
- expect(enrichGroupPrompt).not.toHaveBeenCalled();
2677
- // But message was still processed
2668
+ // Message was still processed, and the prompt is passed through verbatim.
2678
2669
  const calls = executeMock.mock.calls
2679
2670
  .slice(before)
2680
2671
  .filter((c) => (c[0] as { chatId: string }).chatId === String(chatId));
2681
2672
  expect(calls.length).toBe(1);
2673
+ expect((calls[0][0] as { prompt: string }).prompt).toBe(
2674
+ "@testbot anonymous message",
2675
+ );
2682
2676
  }, 3000);
2683
2677
  });
2684
2678
 
@@ -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). */
@@ -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
- // Enrich prompt with sender context
763
- let enrichedPrompt = prompt;
764
- if (!isGroup && senderName) {
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: enrichedPrompt,
766
+ prompt,
775
767
  senderName,
776
768
  isGroup,
777
769
  messageId,
@@ -1,296 +0,0 @@
1
- /**
2
- * Extended prompt-builder tests — covers branches and edge cases not exercised
3
- * by the existing prompt-builder.test.ts.
4
- *
5
- * enrichDMPrompt edge cases:
6
- * - Empty-string username (falsy → no @tag appended)
7
- * - Whitespace-only name
8
- * - Multi-line prompt body preserved exactly
9
- * - Special characters in name / username
10
- *
11
- * enrichGroupPrompt branches:
12
- * - Exactly 2 messages (boundary: 1 prior — triggers the formatting path)
13
- * - 6 messages (5 prior + 1 current — tests full-width context window)
14
- * - Line 33 defensive branch: priorMsgs.length === 0 cannot be reached via
15
- * recentMsgs.length > 1 guard, but the behaviour is confirmed by the
16
- * single-message case already in base tests; we verify the slice is correct.
17
- * - senderName is taken from priorMsgs[0], not the current message
18
- * - Timestamp formatting via formatSmartTimestamp is invoked (output contains
19
- * a formatted timestamp string within the context block)
20
- * - Text truncation at exactly 200 characters (boundary check)
21
- * - Text shorter than 200 characters is not padded
22
- * - getRecentBySenderId is called with the correct chatId and senderId
23
- * - Output structure: context block precedes the prompt, separated by blank line
24
- */
25
-
26
- import { describe, it, expect, vi, beforeEach } from "vitest";
27
-
28
- // ── Mocks ─────────────────────────────────────────────────────────────────────
29
-
30
- vi.mock("../storage/history.js", () => ({
31
- getRecentBySenderId: vi.fn(() => []),
32
- }));
33
-
34
- // We do NOT mock ../util/time.js — formatSmartTimestamp is a pure function
35
- // that we want exercised for real so that timestamp output is visible in results.
36
-
37
- // ── Dynamic imports (after mocks) ────────────────────────────────────────────
38
-
39
- const { getRecentBySenderId } = await import("../storage/history.js");
40
- const { enrichDMPrompt, enrichGroupPrompt } =
41
- await import("../core/prompt-builder.js");
42
-
43
- // ── Helpers ──────────────────────────────────────────────────────────────────
44
-
45
- /** Build a minimal history message object. */
46
- function msg(
47
- msgId: number,
48
- senderId: number,
49
- senderName: string,
50
- text: string,
51
- timestamp = Date.now(),
52
- ) {
53
- return { msgId, senderId, senderName, text, timestamp };
54
- }
55
-
56
- // ── enrichDMPrompt ────────────────────────────────────────────────────────────
57
-
58
- describe("enrichDMPrompt — extended edge cases", () => {
59
- it("omits the @tag when username is an empty string (falsy branch)", () => {
60
- // senderUsername = "" → `${senderUsername ? ` (@${senderUsername})` : ""}` → ""
61
- const result = enrichDMPrompt("hello", "Alice", "");
62
- expect(result).toBe("[DM from Alice]\nhello");
63
- expect(result).not.toContain("@");
64
- });
65
-
66
- it("preserves a multi-line prompt body verbatim", () => {
67
- const multiLine = "line one\nline two\nline three";
68
- const result = enrichDMPrompt(multiLine, "Bob");
69
- expect(result).toBe(`[DM from Bob]\n${multiLine}`);
70
- });
71
-
72
- it("handles special characters in sender name", () => {
73
- const result = enrichDMPrompt("hi", "O'Brien & Co.", "obrien");
74
- expect(result).toBe("[DM from O'Brien & Co. (@obrien)]\nhi");
75
- });
76
-
77
- it("handles special characters in username", () => {
78
- const result = enrichDMPrompt("hi", "User", "user.name_123");
79
- expect(result).toBe("[DM from User (@user.name_123)]\nhi");
80
- });
81
-
82
- it("handles an empty prompt string", () => {
83
- const result = enrichDMPrompt("", "Carol");
84
- expect(result).toBe("[DM from Carol]\n");
85
- });
86
-
87
- it("handles a whitespace-only sender name", () => {
88
- // No special handling expected — just passed through
89
- const result = enrichDMPrompt("msg", " ");
90
- expect(result).toBe("[DM from ]\nmsg");
91
- });
92
-
93
- it("format is always [DM from NAME] newline PROMPT", () => {
94
- const name = "TestUser";
95
- const username = "tuser";
96
- const prompt = "test prompt";
97
- const result = enrichDMPrompt(prompt, name, username);
98
- expect(result.startsWith("[DM from TestUser (@tuser)]\n")).toBe(true);
99
- expect(result.endsWith(prompt)).toBe(true);
100
- });
101
- });
102
-
103
- // ── enrichGroupPrompt ─────────────────────────────────────────────────────────
104
-
105
- describe("enrichGroupPrompt — extended branch coverage", () => {
106
- beforeEach(() => {
107
- vi.mocked(getRecentBySenderId).mockReset();
108
- });
109
-
110
- // ── boundary: exactly 2 messages (1 prior + 1 current) ──────────────────
111
-
112
- it("includes context when there are exactly 2 messages (minimum enrichment case)", () => {
113
- vi.mocked(getRecentBySenderId).mockReturnValue([
114
- msg(
115
- 1,
116
- 10,
117
- "Alice",
118
- "prior message",
119
- new Date("2025-06-01T10:00:00Z").getTime(),
120
- ),
121
- msg(
122
- 2,
123
- 10,
124
- "Alice",
125
- "current message",
126
- new Date("2025-06-01T10:01:00Z").getTime(),
127
- ),
128
- ]);
129
- const result = enrichGroupPrompt("current message", "chat-x", 10);
130
- expect(result).toContain("Alice's recent messages in this group:");
131
- expect(result).toContain("prior message");
132
- expect(result).toContain("current message"); // original prompt preserved
133
- // Structure: context block then blank line then prompt
134
- expect(result).toMatch(/\]\n\ncurrent message$/);
135
- });
136
-
137
- // ── senderName from priorMsgs[0], not the current message ───────────────
138
-
139
- it("uses the senderName from the first prior message (priorMsgs[0])", () => {
140
- vi.mocked(getRecentBySenderId).mockReturnValue([
141
- msg(1, 20, "FirstSender", "msg a", Date.now() - 5000),
142
- msg(2, 20, "SecondSender", "msg b", Date.now() - 3000), // prior[1]
143
- msg(3, 20, "ThirdSender", "current msg", Date.now()), // current (slice off)
144
- ]);
145
- const result = enrichGroupPrompt("current msg", "chat-y", 20);
146
- // senderName header should be from index 0 of priorMsgs
147
- expect(result).toContain("FirstSender's recent messages in this group:");
148
- });
149
-
150
- // ── 6 messages: 5 prior + 1 current ─────────────────────────────────────
151
-
152
- it("includes all 5 prior messages when 6 total are returned", () => {
153
- const now = Date.now();
154
- vi.mocked(getRecentBySenderId).mockReturnValue([
155
- msg(1, 30, "Dave", "msg 1", now - 50000),
156
- msg(2, 30, "Dave", "msg 2", now - 40000),
157
- msg(3, 30, "Dave", "msg 3", now - 30000),
158
- msg(4, 30, "Dave", "msg 4", now - 20000),
159
- msg(5, 30, "Dave", "msg 5", now - 10000),
160
- msg(6, 30, "Dave", "current", now),
161
- ]);
162
- const result = enrichGroupPrompt("current", "chat-z", 30);
163
- expect(result).toContain("Dave's recent messages in this group:");
164
- for (let i = 1; i <= 5; i++) {
165
- expect(result).toContain(`msg ${i}`);
166
- }
167
- expect(result).toContain("current"); // original prompt
168
- // The current message's text should NOT appear inside the context block
169
- const contextBlock = result.split("]\n\n")[0];
170
- expect(contextBlock).not.toContain("current");
171
- });
172
-
173
- // ── text truncation at exactly 200 chars ─────────────────────────────────
174
-
175
- it("truncates messages at exactly 200 characters (slice boundary)", () => {
176
- const exactly200 = "a".repeat(200);
177
- const longText = "a".repeat(250);
178
- vi.mocked(getRecentBySenderId).mockReturnValue([
179
- msg(1, 40, "Eve", longText, Date.now() - 2000),
180
- msg(2, 40, "Eve", "current", Date.now()),
181
- ]);
182
- const result = enrichGroupPrompt("current", "chat-trunc", 40);
183
- expect(result).toContain(exactly200);
184
- expect(result).not.toContain("a".repeat(201));
185
- });
186
-
187
- it("short messages (< 200 chars) are not truncated or padded", () => {
188
- const shortText = "short message";
189
- vi.mocked(getRecentBySenderId).mockReturnValue([
190
- msg(1, 41, "Frank", shortText, Date.now() - 1000),
191
- msg(2, 41, "Frank", "current", Date.now()),
192
- ]);
193
- const result = enrichGroupPrompt("current", "chat-short", 41);
194
- expect(result).toContain(shortText);
195
- // The full text appears — no additional characters appended to the message body.
196
- // Note: the outer context bracket `]` is appended after the last context line
197
- // in the format `[header:\n lines]`, so we check containment, not endsWith.
198
- const lines = result.split("\n");
199
- const priorLine = lines.find((l) => l.includes(shortText));
200
- expect(priorLine).toBeDefined();
201
- // Line format: ` [timestamp] short message` optionally followed by `]` (closing bracket)
202
- // Verify the message text appears after the timestamp bracket and is not padded
203
- expect(priorLine).toMatch(new RegExp(`\\] ${shortText}\\]?$`));
204
- });
205
-
206
- // ── getRecentBySenderId called with correct args ──────────────────────────
207
-
208
- it("calls getRecentBySenderId with the provided chatId and senderId", () => {
209
- vi.mocked(getRecentBySenderId).mockReturnValue([]);
210
- enrichGroupPrompt("hi", "specific-chat-id", 99);
211
- expect(getRecentBySenderId).toHaveBeenCalledWith("specific-chat-id", 99, 5);
212
- });
213
-
214
- it("requests exactly 5 recent messages from history", () => {
215
- vi.mocked(getRecentBySenderId).mockReturnValue([]);
216
- enrichGroupPrompt("hi", "chat-count", 55);
217
- const [, , limit] = vi.mocked(getRecentBySenderId).mock.calls[0];
218
- expect(limit).toBe(5);
219
- });
220
-
221
- // ── output structure ──────────────────────────────────────────────────────
222
-
223
- it("context block is wrapped with [ ... ] and separated from prompt by blank line", () => {
224
- vi.mocked(getRecentBySenderId).mockReturnValue([
225
- msg(1, 50, "Gina", "past msg", Date.now() - 3000),
226
- msg(2, 50, "Gina", "now", Date.now()),
227
- ]);
228
- const result = enrichGroupPrompt("now", "chat-struct", 50);
229
- // Should start with '[' (the context header)
230
- expect(result.startsWith("[")).toBe(true);
231
- // Blank line (\n\n) separates context block from the original prompt
232
- expect(result).toContain("]\n\nnow");
233
- });
234
-
235
- it("each prior message line is indented with two spaces", () => {
236
- vi.mocked(getRecentBySenderId).mockReturnValue([
237
- msg(1, 60, "Hank", "indented message", Date.now() - 5000),
238
- msg(2, 60, "Hank", "current", Date.now()),
239
- ]);
240
- const result = enrichGroupPrompt("current", "chat-indent", 60);
241
- // The format is ` [timestamp] message text`
242
- const lines = result.split("\n");
243
- const priorLine = lines.find((l) => l.includes("indented message"));
244
- expect(priorLine).toBeDefined();
245
- expect(priorLine!.startsWith(" [")).toBe(true);
246
- });
247
-
248
- // ── timestamp is formatted and embedded ──────────────────────────────────
249
-
250
- it("includes a formatted timestamp in each prior message line", () => {
251
- const ts = new Date("2020-03-15T12:34:00Z").getTime(); // past year → full date format
252
- vi.mocked(getRecentBySenderId).mockReturnValue([
253
- msg(1, 70, "Iris", "timestamped msg", ts),
254
- msg(2, 70, "Iris", "current", Date.now()),
255
- ]);
256
- const result = enrichGroupPrompt("current", "chat-ts", 70);
257
- // The context block line should contain a bracket-wrapped timestamp
258
- const lines = result.split("\n");
259
- const priorLine = lines.find((l) => l.includes("timestamped msg"));
260
- expect(priorLine).toBeDefined();
261
- // Format: ` [<timestamp>] text` — there should be a timestamp inside brackets
262
- expect(priorLine).toMatch(/\[.+\]/);
263
- });
264
-
265
- // ── returns prompt unchanged for 0 and 1 message (re-confirmed at boundary)
266
-
267
- it("returns the prompt unchanged when getRecentBySenderId returns empty array", () => {
268
- vi.mocked(getRecentBySenderId).mockReturnValue([]);
269
- const result = enrichGroupPrompt("only message", "chat-empty", 1);
270
- expect(result).toBe("only message");
271
- });
272
-
273
- it("returns the prompt unchanged when exactly 1 message is returned (the current)", () => {
274
- vi.mocked(getRecentBySenderId).mockReturnValue([
275
- msg(1, 80, "Jan", "the only message", Date.now()),
276
- ]);
277
- const result = enrichGroupPrompt("the only message", "chat-one", 80);
278
- expect(result).toBe("the only message");
279
- });
280
-
281
- // ── multi-line prompt body is preserved ───────────────────────────────────
282
-
283
- it("preserves a multi-line original prompt after the context block", () => {
284
- vi.mocked(getRecentBySenderId).mockReturnValue([
285
- msg(1, 90, "Karl", "prior", Date.now() - 1000),
286
- msg(2, 90, "Karl", "current line 1\ncurrent line 2", Date.now()),
287
- ]);
288
- const result = enrichGroupPrompt(
289
- "current line 1\ncurrent line 2",
290
- "chat-ml",
291
- 90,
292
- );
293
- expect(result).toContain("current line 1\ncurrent line 2");
294
- expect(result.endsWith("current line 1\ncurrent line 2")).toBe(true);
295
- });
296
- });
@@ -1,106 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from "vitest";
2
-
3
- vi.mock("../storage/history.js", () => ({
4
- getRecentBySenderId: vi.fn(() => []),
5
- }));
6
-
7
- const { getRecentBySenderId } = await import("../storage/history.js");
8
- const { enrichDMPrompt, enrichGroupPrompt } =
9
- await import("../core/prompt-builder.js");
10
-
11
- describe("enrichDMPrompt", () => {
12
- it("prepends DM metadata", () => {
13
- const result = enrichDMPrompt("hello", "Alice");
14
- expect(result).toBe("[DM from Alice]\nhello");
15
- });
16
-
17
- it("includes username when provided", () => {
18
- const result = enrichDMPrompt("hello", "Alice", "alice42");
19
- expect(result).toBe("[DM from Alice (@alice42)]\nhello");
20
- });
21
-
22
- it("works without username", () => {
23
- const result = enrichDMPrompt("hello", "Bob", undefined);
24
- expect(result).toBe("[DM from Bob]\nhello");
25
- });
26
- });
27
-
28
- describe("enrichGroupPrompt", () => {
29
- beforeEach(() => {
30
- vi.mocked(getRecentBySenderId).mockReset();
31
- });
32
-
33
- it("returns prompt unchanged when no prior messages", () => {
34
- vi.mocked(getRecentBySenderId).mockReturnValue([]);
35
- const result = enrichGroupPrompt("hello", "chat1", 42);
36
- expect(result).toBe("hello");
37
- });
38
-
39
- it("returns prompt unchanged when only one message (current)", () => {
40
- vi.mocked(getRecentBySenderId).mockReturnValue([
41
- {
42
- msgId: 1,
43
- senderId: 42,
44
- senderName: "Alice",
45
- text: "hello",
46
- timestamp: Date.now(),
47
- },
48
- ]);
49
- const result = enrichGroupPrompt("hello", "chat1", 42);
50
- expect(result).toBe("hello");
51
- });
52
-
53
- it("prepends prior messages for threading context", () => {
54
- vi.mocked(getRecentBySenderId).mockReturnValue([
55
- {
56
- msgId: 1,
57
- senderId: 42,
58
- senderName: "Alice",
59
- text: "first message",
60
- timestamp: new Date("2025-01-01T10:00:00Z").getTime(),
61
- },
62
- {
63
- msgId: 2,
64
- senderId: 42,
65
- senderName: "Alice",
66
- text: "second message",
67
- timestamp: new Date("2025-01-01T10:01:00Z").getTime(),
68
- },
69
- {
70
- msgId: 3,
71
- senderId: 42,
72
- senderName: "Alice",
73
- text: "current",
74
- timestamp: new Date("2025-01-01T10:02:00Z").getTime(),
75
- },
76
- ]);
77
- const result = enrichGroupPrompt("current", "chat1", 42);
78
- expect(result).toContain("Alice's recent messages");
79
- expect(result).toContain("first message");
80
- expect(result).toContain("second message");
81
- expect(result).toContain("current"); // the original prompt
82
- });
83
-
84
- it("truncates long messages to 200 chars", () => {
85
- const longText = "x".repeat(300);
86
- vi.mocked(getRecentBySenderId).mockReturnValue([
87
- {
88
- msgId: 1,
89
- senderId: 42,
90
- senderName: "Bob",
91
- text: longText,
92
- timestamp: Date.now(),
93
- },
94
- {
95
- msgId: 2,
96
- senderId: 42,
97
- senderName: "Bob",
98
- text: "current",
99
- timestamp: Date.now(),
100
- },
101
- ]);
102
- const result = enrichGroupPrompt("current", "chat1", 42);
103
- expect(result).not.toContain(longText); // full text shouldn't appear
104
- expect(result).toContain("x".repeat(200)); // truncated version should
105
- });
106
- });
@@ -1,40 +0,0 @@
1
- /**
2
- * Prompt enrichment — adds context to raw user messages before sending to the AI.
3
- * Platform-agnostic; works with any messaging frontend.
4
- */
5
-
6
- import { getRecentBySenderId } from "../storage/history.js";
7
- import { formatSmartTimestamp } from "../util/time.js";
8
-
9
- /**
10
- * Enrich a DM prompt with sender metadata.
11
- */
12
- export function enrichDMPrompt(
13
- prompt: string,
14
- senderName: string,
15
- senderUsername?: string,
16
- ): string {
17
- const userTag = senderUsername ? ` (@${senderUsername})` : "";
18
- return `[DM from ${senderName}${userTag}]\n${prompt}`;
19
- }
20
-
21
- /**
22
- * Enrich a group prompt with the sender's recent messages for threading context.
23
- */
24
- export function enrichGroupPrompt(
25
- prompt: string,
26
- chatId: string,
27
- senderId: number,
28
- ): string {
29
- const recentMsgs = getRecentBySenderId(chatId, senderId, 5);
30
- if (recentMsgs.length <= 1) return prompt;
31
-
32
- const priorMsgs = recentMsgs.slice(0, -1);
33
- const senderName = priorMsgs[0].senderName;
34
- const contextLines = priorMsgs
35
- .map(
36
- (m) => ` [${formatSmartTimestamp(m.timestamp)}] ${m.text.slice(0, 200)}`,
37
- )
38
- .join("\n");
39
- return `[${senderName}'s recent messages in this group:\n${contextLines}]\n\n${prompt}`;
40
- }