talon-agent 1.3.0 → 1.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.
@@ -125,9 +125,6 @@ describe("gateway shared actions", () => {
125
125
  originalFetch = globalThis.fetch;
126
126
  originalEnv = { ...process.env };
127
127
  vi.clearAllMocks();
128
- // Reset env vars used by web_search
129
- delete process.env.TALON_BRAVE_API_KEY;
130
- delete process.env.TALON_SEARXNG_URL;
131
128
  });
132
129
 
133
130
  afterEach(() => {
@@ -276,377 +273,6 @@ describe("gateway shared actions", () => {
276
273
  });
277
274
  });
278
275
 
279
- // ════════════════════════════════════════════════════════════════════════
280
- // web_search
281
- // ════════════════════════════════════════════════════════════════════════
282
-
283
- describe("web_search", () => {
284
- it("returns error for missing query", async () => {
285
- const result = await handleSharedAction({ action: "web_search" }, 123);
286
- expect(result).toEqual({ ok: false, error: "Missing query" });
287
- });
288
-
289
- it("returns error for empty query string", async () => {
290
- const result = await handleSharedAction(
291
- { action: "web_search", query: "" },
292
- 123,
293
- );
294
- expect(result).toEqual({ ok: false, error: "Missing query" });
295
- });
296
-
297
- it("uses Brave API when key is configured", async () => {
298
- process.env.TALON_BRAVE_API_KEY = "test-brave-key";
299
- const mockFetch = vi.fn().mockResolvedValueOnce(
300
- mockResponse({
301
- ok: true,
302
- contentType: "application/json",
303
- json: {
304
- web: {
305
- results: [
306
- {
307
- title: "Result 1",
308
- url: "https://example.com/1",
309
- description: "Description 1",
310
- },
311
- {
312
- title: "Result 2",
313
- url: "https://example.com/2",
314
- description: "Description 2",
315
- },
316
- ],
317
- },
318
- },
319
- }),
320
- );
321
- vi.stubGlobal("fetch", mockFetch);
322
-
323
- const result = await handleSharedAction(
324
- { action: "web_search", query: "test query" },
325
- 123,
326
- );
327
-
328
- expect(result?.ok).toBe(true);
329
- expect(result?.text).toContain("via Brave");
330
- expect(result?.text).toContain("Result 1");
331
- expect(result?.text).toContain("https://example.com/1");
332
- expect(result?.text).toContain("Description 1");
333
- expect(result?.text).toContain("Result 2");
334
-
335
- // Verify Brave API was called correctly
336
- expect(mockFetch).toHaveBeenCalledTimes(1);
337
- const [url, opts] = mockFetch.mock.calls[0];
338
- expect(url).toContain("api.search.brave.com");
339
- expect(url).toContain("q=test%20query");
340
- expect(url).toContain("count=5");
341
- expect(opts.headers["X-Subscription-Token"]).toBe("test-brave-key");
342
- });
343
-
344
- it("respects custom limit for Brave API", async () => {
345
- process.env.TALON_BRAVE_API_KEY = "test-brave-key";
346
- const mockFetch = vi.fn().mockResolvedValueOnce(
347
- mockResponse({
348
- ok: true,
349
- json: {
350
- web: {
351
- results: [{ title: "R", url: "https://r.com", description: "d" }],
352
- },
353
- },
354
- }),
355
- );
356
- vi.stubGlobal("fetch", mockFetch);
357
-
358
- await handleSharedAction(
359
- { action: "web_search", query: "test", limit: 8 },
360
- 123,
361
- );
362
- const [url] = mockFetch.mock.calls[0];
363
- expect(url).toContain("count=8");
364
- });
365
-
366
- it("clamps search limit to 10", async () => {
367
- process.env.TALON_BRAVE_API_KEY = "test-brave-key";
368
- const mockFetch = vi.fn().mockResolvedValueOnce(
369
- mockResponse({
370
- ok: true,
371
- json: {
372
- web: {
373
- results: [{ title: "R", url: "https://r.com", description: "d" }],
374
- },
375
- },
376
- }),
377
- );
378
- vi.stubGlobal("fetch", mockFetch);
379
-
380
- await handleSharedAction(
381
- { action: "web_search", query: "test", limit: 50 },
382
- 123,
383
- );
384
- const [url] = mockFetch.mock.calls[0];
385
- expect(url).toContain("count=10");
386
- });
387
-
388
- it("falls back to SearXNG when Brave returns non-ok", async () => {
389
- process.env.TALON_BRAVE_API_KEY = "test-brave-key";
390
- const mockFetch = vi
391
- .fn()
392
- .mockResolvedValueOnce(mockResponse({ ok: false, status: 429 })) // Brave fails
393
- .mockResolvedValueOnce(
394
- mockResponse({
395
- ok: true,
396
- json: {
397
- results: [
398
- {
399
- title: "SearX Result",
400
- url: "https://searx.example.com",
401
- content: "SearX snippet",
402
- },
403
- ],
404
- },
405
- }),
406
- );
407
- vi.stubGlobal("fetch", mockFetch);
408
-
409
- const result = await handleSharedAction(
410
- { action: "web_search", query: "fallback test" },
411
- 123,
412
- );
413
-
414
- expect(mockFetch).toHaveBeenCalledTimes(2);
415
- expect(result?.ok).toBe(true);
416
- expect(result?.text).toContain("via SearXNG");
417
- expect(result?.text).toContain("SearX Result");
418
- });
419
-
420
- it("falls back to SearXNG when Brave throws an error", async () => {
421
- process.env.TALON_BRAVE_API_KEY = "test-brave-key";
422
- const mockFetch = vi
423
- .fn()
424
- .mockRejectedValueOnce(new Error("network error")) // Brave throws
425
- .mockResolvedValueOnce(
426
- mockResponse({
427
- ok: true,
428
- json: {
429
- results: [
430
- {
431
- title: "Fallback",
432
- url: "https://fb.com",
433
- content: "snippet",
434
- },
435
- ],
436
- },
437
- }),
438
- );
439
- vi.stubGlobal("fetch", mockFetch);
440
-
441
- const result = await handleSharedAction(
442
- { action: "web_search", query: "test" },
443
- 123,
444
- );
445
-
446
- expect(result?.ok).toBe(true);
447
- expect(result?.text).toContain("via SearXNG");
448
- });
449
-
450
- it("uses SearXNG directly when no Brave key", async () => {
451
- // No TALON_BRAVE_API_KEY set
452
- const mockFetch = vi.fn().mockResolvedValueOnce(
453
- mockResponse({
454
- ok: true,
455
- json: {
456
- results: [
457
- {
458
- title: "Direct SearX",
459
- url: "https://searx.com/r",
460
- content: "content here",
461
- },
462
- ],
463
- },
464
- }),
465
- );
466
- vi.stubGlobal("fetch", mockFetch);
467
-
468
- const result = await handleSharedAction(
469
- { action: "web_search", query: "direct" },
470
- 123,
471
- );
472
-
473
- expect(mockFetch).toHaveBeenCalledTimes(1);
474
- const [url] = mockFetch.mock.calls[0];
475
- expect(url).toContain("localhost:8080");
476
- expect(url).toContain("format=json");
477
- expect(result?.ok).toBe(true);
478
- expect(result?.text).toContain("via SearXNG");
479
- });
480
-
481
- it("uses custom SearXNG URL from env", async () => {
482
- process.env.TALON_SEARXNG_URL = "http://my-searx:9090";
483
- const mockFetch = vi.fn().mockResolvedValueOnce(
484
- mockResponse({
485
- ok: true,
486
- json: {
487
- results: [{ title: "T", url: "https://t.com", content: "c" }],
488
- },
489
- }),
490
- );
491
- vi.stubGlobal("fetch", mockFetch);
492
-
493
- await handleSharedAction({ action: "web_search", query: "custom" }, 123);
494
-
495
- const [url] = mockFetch.mock.calls[0];
496
- expect(url).toContain("my-searx:9090");
497
- });
498
-
499
- it("returns 'no results' when both providers fail", async () => {
500
- process.env.TALON_BRAVE_API_KEY = "test-key";
501
- const mockFetch = vi
502
- .fn()
503
- .mockRejectedValueOnce(new Error("brave fail"))
504
- .mockRejectedValueOnce(new Error("searx fail"));
505
- vi.stubGlobal("fetch", mockFetch);
506
-
507
- const result = await handleSharedAction(
508
- { action: "web_search", query: "nothing" },
509
- 123,
510
- );
511
-
512
- expect(result?.ok).toBe(true);
513
- expect(result?.text).toBe('No results for "nothing".');
514
- });
515
-
516
- it("returns 'no results' when both return non-ok", async () => {
517
- process.env.TALON_BRAVE_API_KEY = "test-key";
518
- const mockFetch = vi
519
- .fn()
520
- .mockResolvedValueOnce(mockResponse({ ok: false, status: 500 }))
521
- .mockResolvedValueOnce(mockResponse({ ok: false, status: 503 }));
522
- vi.stubGlobal("fetch", mockFetch);
523
-
524
- const result = await handleSharedAction(
525
- { action: "web_search", query: "failing" },
526
- 123,
527
- );
528
-
529
- expect(result?.ok).toBe(true);
530
- expect(result?.text).toBe('No results for "failing".');
531
- });
532
-
533
- it("returns 'no results' when Brave returns empty results array", async () => {
534
- process.env.TALON_BRAVE_API_KEY = "test-key";
535
- const mockFetch = vi
536
- .fn()
537
- .mockResolvedValueOnce(
538
- mockResponse({ ok: true, json: { web: { results: [] } } }),
539
- )
540
- .mockResolvedValueOnce(
541
- mockResponse({ ok: true, json: { results: [] } }),
542
- );
543
- vi.stubGlobal("fetch", mockFetch);
544
-
545
- const result = await handleSharedAction(
546
- { action: "web_search", query: "empty" },
547
- 123,
548
- );
549
-
550
- expect(result?.ok).toBe(true);
551
- expect(result?.text).toBe('No results for "empty".');
552
- });
553
-
554
- it("handles Brave response with missing web field", async () => {
555
- process.env.TALON_BRAVE_API_KEY = "test-key";
556
- const mockFetch = vi
557
- .fn()
558
- .mockResolvedValueOnce(mockResponse({ ok: true, json: {} })) // no web field
559
- .mockResolvedValueOnce(
560
- mockResponse({
561
- ok: true,
562
- json: {
563
- results: [
564
- { title: "FallbackR", url: "https://f.com", content: "fb" },
565
- ],
566
- },
567
- }),
568
- );
569
- vi.stubGlobal("fetch", mockFetch);
570
-
571
- const result = await handleSharedAction(
572
- { action: "web_search", query: "test" },
573
- 123,
574
- );
575
- expect(result?.text).toContain("via SearXNG");
576
- });
577
-
578
- it("truncates long snippets to 200 chars", async () => {
579
- process.env.TALON_BRAVE_API_KEY = "test-key";
580
- const longDesc = "A".repeat(500);
581
- const mockFetch = vi.fn().mockResolvedValueOnce(
582
- mockResponse({
583
- ok: true,
584
- json: {
585
- web: {
586
- results: [
587
- { title: "Long", url: "https://l.com", description: longDesc },
588
- ],
589
- },
590
- },
591
- }),
592
- );
593
- vi.stubGlobal("fetch", mockFetch);
594
-
595
- const result = await handleSharedAction(
596
- { action: "web_search", query: "long" },
597
- 123,
598
- );
599
- // The snippet should be sliced to 200 chars
600
- expect(result?.text).not.toContain("A".repeat(201));
601
- expect(result?.text).toContain("A".repeat(200));
602
- });
603
-
604
- it("handles missing description in Brave results", async () => {
605
- process.env.TALON_BRAVE_API_KEY = "test-key";
606
- const mockFetch = vi.fn().mockResolvedValueOnce(
607
- mockResponse({
608
- ok: true,
609
- json: {
610
- web: { results: [{ title: "NoDesc", url: "https://nd.com" }] },
611
- },
612
- }),
613
- );
614
- vi.stubGlobal("fetch", mockFetch);
615
-
616
- const result = await handleSharedAction(
617
- { action: "web_search", query: "nodesc" },
618
- 123,
619
- );
620
- expect(result?.ok).toBe(true);
621
- expect(result?.text).toContain("NoDesc");
622
- });
623
-
624
- it("slices SearXNG results to limit", async () => {
625
- const mockFetch = vi.fn().mockResolvedValueOnce(
626
- mockResponse({
627
- ok: true,
628
- json: {
629
- results: Array.from({ length: 20 }, (_, i) => ({
630
- title: `R${i}`,
631
- url: `https://r${i}.com`,
632
- content: `c${i}`,
633
- })),
634
- },
635
- }),
636
- );
637
- vi.stubGlobal("fetch", mockFetch);
638
-
639
- const result = await handleSharedAction(
640
- { action: "web_search", query: "many", limit: 3 },
641
- 123,
642
- );
643
- // Should only contain 3 results (numbered 1-3)
644
- expect(result?.text).toContain("1. R0");
645
- expect(result?.text).toContain("3. R2");
646
- expect(result?.text).not.toContain("4. R3");
647
- });
648
- });
649
-
650
276
  // ════════════════════════════════════════════════════════════════════════
651
277
  // fetch_url
652
278
  // ════════════════════════════════════════════════════════════════════════
@@ -2082,14 +1708,13 @@ describe("gateway shared actions", () => {
2082
1708
  });
2083
1709
  });
2084
1710
 
2085
- // ── Additional branch coverage for fetch_url and web_search ──────────────
1711
+ // ── Additional branch coverage for fetch_url ──────────────────────────────
2086
1712
 
2087
1713
  describe("gateway-actions — additional branch coverage", () => {
2088
1714
  let originalFetch: typeof globalThis.fetch;
2089
1715
 
2090
1716
  beforeEach(() => {
2091
1717
  originalFetch = globalThis.fetch;
2092
- delete process.env.TALON_BRAVE_API_KEY;
2093
1718
  });
2094
1719
 
2095
1720
  afterEach(() => {
@@ -2144,51 +1769,4 @@ describe("gateway-actions — additional branch coverage", () => {
2144
1769
  // Should succeed (downloaded as bin), covering ct ?? "" right side and ct.split("/")[1] ?? "file" right side
2145
1770
  expect(result?.ok).toBe(true);
2146
1771
  });
2147
-
2148
- it("handles search result with missing content field (line 113 r.content ?? '' branch)", async () => {
2149
- const mockFetch = vi.fn().mockResolvedValueOnce({
2150
- ok: true,
2151
- status: 200,
2152
- headers: new Headers(),
2153
- json: async () => ({
2154
- results: [
2155
- { title: "Result", url: "https://example.com", content: undefined },
2156
- ],
2157
- }),
2158
- text: async () => "",
2159
- arrayBuffer: async () => new ArrayBuffer(0),
2160
- } as unknown as Response);
2161
- vi.stubGlobal("fetch", mockFetch);
2162
- process.env.TALON_SEARXNG_URL = "http://localhost:8080";
2163
-
2164
- const result = await handleSharedAction(
2165
- { action: "web_search", query: "test" },
2166
- 123,
2167
- );
2168
- expect(result?.ok).toBe(true);
2169
- // snippet should be "" (from ?? "")
2170
- expect(result?.text).toBeDefined();
2171
- });
2172
-
2173
- it("handles search response with no results array (line 113 data.results ?? [] branch)", async () => {
2174
- const mockFetch = vi.fn().mockResolvedValueOnce({
2175
- ok: true,
2176
- status: 200,
2177
- headers: new Headers(),
2178
- json: async () => ({
2179
- /* no results property */
2180
- }),
2181
- text: async () => "",
2182
- arrayBuffer: async () => new ArrayBuffer(0),
2183
- } as unknown as Response);
2184
- vi.stubGlobal("fetch", mockFetch);
2185
- process.env.TALON_SEARXNG_URL = "http://localhost:8080";
2186
-
2187
- const result = await handleSharedAction(
2188
- { action: "web_search", query: "empty" },
2189
- 123,
2190
- );
2191
- expect(result?.ok).toBe(true);
2192
- expect(result?.text).toContain("No results for");
2193
- });
2194
1772
  });
@@ -9,7 +9,6 @@ import {
9
9
  setSessionName,
10
10
  } from "../../storage/sessions.js";
11
11
  import { getChatSettings, setChatModel } from "../../storage/chat-settings.js";
12
- import { getRecentHistory } from "../../storage/history.js";
13
12
  import { resolve } from "node:path";
14
13
  import { classify } from "../../core/errors.js";
15
14
  import {
@@ -19,7 +18,7 @@ import {
19
18
  import { rebuildSystemPrompt } from "../../util/config.js";
20
19
  import { log, logError, logWarn } from "../../util/log.js";
21
20
  import { traceMessage } from "../../util/trace.js";
22
- import { formatSmartTimestamp, formatFullDatetime } from "../../util/time.js";
21
+ import { formatFullDatetime } from "../../util/time.js";
23
22
 
24
23
  import type { QueryParams, QueryResult } from "../../core/types.js";
25
24
 
@@ -115,66 +114,68 @@ export async function handleMessage(
115
114
  "TaskOutput",
116
115
  "TaskStop",
117
116
  "AskUserQuestion",
117
+ // Always disable Claude Code built-in web tools — fetch_url is always
118
+ // available, and Brave Search MCP replaces WebSearch when configured.
119
+ "WebSearch",
120
+ "WebFetch",
118
121
  ],
119
122
  ...thinkingConfig,
120
123
  mcpServers: {
121
- // Register frontend-specific MCP tools based on active frontend
124
+ // Register unified MCP tools server one per messaging frontend.
125
+ // Terminal frontend relies on Claude Code built-in tools (Read, Write,
126
+ // Bash, etc.) and doesn't need a custom MCP tools server.
122
127
  ...(() => {
123
- const frontends = Array.isArray(config.frontend)
128
+ const allFrontends = Array.isArray(config.frontend)
124
129
  ? config.frontend
125
130
  : [config.frontend];
131
+ const frontends = allFrontends.filter((f) => f !== "terminal");
126
132
  const bridgeUrl = `http://127.0.0.1:${bridgePortFn()}`;
127
- const mcpEnv = { TALON_BRIDGE_URL: bridgeUrl, TALON_CHAT_ID: chatId };
128
133
  const servers: Record<
129
134
  string,
130
135
  { command: string; args: string[]; env: Record<string, string> }
131
136
  > = {};
132
- // Resolve tsx from Talon's node_modules (cwd may be ~/.talon/workspace/ which has no node_modules)
133
137
  // Resolve tsx from the package root (3 levels up from src/backend/claude-sdk/)
134
138
  const tsxImport = resolve(
135
139
  import.meta.dirname ?? ".",
136
140
  "../../../node_modules/tsx/dist/esm/index.mjs",
137
141
  );
142
+ // Unified MCP server in core/tools/
143
+ const mcpServerPath = resolve(
144
+ import.meta.dirname ?? ".",
145
+ "../../core/tools/mcp-server.ts",
146
+ );
138
147
 
139
- if (frontends.includes("telegram")) {
140
- servers["telegram-tools"] = {
141
- command: process.platform === "win32" ? "npx" : "node",
142
- args:
143
- process.platform === "win32"
144
- ? ["tsx", resolve(import.meta.dirname ?? ".", "tools.ts")]
145
- : [
146
- "--import",
147
- tsxImport,
148
- resolve(import.meta.dirname ?? ".", "tools.ts"),
149
- ],
150
- env: mcpEnv,
148
+ for (const frontend of frontends) {
149
+ const serverName = `${frontend}-tools`;
150
+ const mcpEnv = {
151
+ TALON_BRIDGE_URL: bridgeUrl,
152
+ TALON_CHAT_ID: chatId,
153
+ TALON_FRONTEND: frontend,
151
154
  };
152
- }
153
- if (frontends.includes("teams")) {
154
- servers["teams-tools"] = {
155
+ servers[serverName] = {
155
156
  command: process.platform === "win32" ? "npx" : "node",
156
157
  args:
157
158
  process.platform === "win32"
158
- ? [
159
- "tsx",
160
- resolve(
161
- import.meta.dirname ?? ".",
162
- "../../frontend/teams/tools.ts",
163
- ),
164
- ]
165
- : [
166
- "--import",
167
- tsxImport,
168
- resolve(
169
- import.meta.dirname ?? ".",
170
- "../../frontend/teams/tools.ts",
171
- ),
172
- ],
159
+ ? ["tsx", mcpServerPath]
160
+ : ["--import", tsxImport, mcpServerPath],
173
161
  env: mcpEnv,
174
162
  };
175
163
  }
176
164
  return servers;
177
165
  })(),
166
+ // Brave Search MCP server — provides brave_web_search and brave_local_search
167
+ ...(config.braveApiKey
168
+ ? {
169
+ "brave-search": {
170
+ command: resolve(
171
+ import.meta.dirname ?? ".",
172
+ "../../../node_modules/.bin/brave-search-mcp-server",
173
+ ),
174
+ args: [],
175
+ env: { BRAVE_API_KEY: config.braveApiKey },
176
+ },
177
+ }
178
+ : {}),
178
179
  ...getPluginMcpServers(`http://127.0.0.1:${bridgePortFn()}`, chatId),
179
180
  },
180
181
  ...(session.sessionId ? { resume: session.sessionId } : {}),
@@ -183,25 +184,9 @@ export async function handleMessage(
183
184
  const msgIdHint = params.messageId ? ` [msg_id:${params.messageId}]` : "";
184
185
  const nowTag = `[${formatFullDatetime()}]`;
185
186
 
186
- // Session continuity: when resuming a session that has history but no active
187
- // SDK session (after restart or /resume), prepend recent messages for context.
188
- let continuityPrefix = "";
189
- if (!session.sessionId && session.turns > 0) {
190
- const recentMsgs = getRecentHistory(chatId, 10);
191
- if (recentMsgs.length > 0) {
192
- const contextLines = recentMsgs
193
- .map((m) => {
194
- const time = formatSmartTimestamp(m.timestamp);
195
- return `[${time}] ${m.senderName}: ${m.text.slice(0, 300)}`;
196
- })
197
- .join("\n");
198
- continuityPrefix = `[Session resumed — recent conversation context:\n${contextLines}]\n\n`;
199
- }
200
- }
201
-
202
187
  const prompt = isGroup
203
- ? `${continuityPrefix}${nowTag} [${senderName}]${msgIdHint}: ${text}`
204
- : `${continuityPrefix}${nowTag}${msgIdHint} ${text}`;
188
+ ? `${nowTag} [${senderName}]${msgIdHint}: ${text}`
189
+ : `${nowTag}${msgIdHint} ${text}`;
205
190
  log("agent", `[${chatId}] <- (${text.length} chars)`);
206
191
  traceMessage(chatId, "in", text, { senderName, isGroup });
207
192
 
@@ -17,7 +17,6 @@ import {
17
17
  resetSession,
18
18
  } from "../../storage/sessions.js";
19
19
  import { getChatSettings } from "../../storage/chat-settings.js";
20
- import { getRecentHistory } from "../../storage/history.js";
21
20
  import { classify } from "../../core/errors.js";
22
21
  import { log, logError, logWarn } from "../../util/log.js";
23
22
  import { traceMessage } from "../../util/trace.js";
@@ -53,7 +52,7 @@ async function ensureServer(): Promise<OpencodeClient> {
53
52
 
54
53
  // Register our MCP tools server with OpenCode
55
54
  try {
56
- const toolsPath = new URL("../claude-sdk/tools.ts", import.meta.url)
55
+ const toolsPath = new URL("../../core/tools/mcp-server.ts", import.meta.url)
57
56
  .pathname;
58
57
  await client.mcp.add({
59
58
  body: {
@@ -63,6 +62,7 @@ async function ensureServer(): Promise<OpencodeClient> {
63
62
  command: ["node", "--import", "tsx", toolsPath],
64
63
  environment: {
65
64
  TALON_BRIDGE_URL: `http://127.0.0.1:${gatewayPortFn()}`,
65
+ TALON_FRONTEND: "telegram",
66
66
  },
67
67
  },
68
68
  },
@@ -149,24 +149,9 @@ export async function handleMessage(
149
149
 
150
150
  // Build prompt with group context
151
151
  const msgIdHint = params.messageId ? ` [msg_id:${params.messageId}]` : "";
152
- let continuityPrefix = "";
153
- const session = getSession(chatId);
154
- if (!session.sessionId && session.turns > 0) {
155
- const recent = getRecentHistory(chatId, 3);
156
- if (recent.length > 0) {
157
- const ctx = recent
158
- .map(
159
- (m) =>
160
- `[${new Date(m.timestamp).toISOString().slice(11, 16)}] ${m.senderName}: ${m.text.slice(0, 300)}`,
161
- )
162
- .join("\n");
163
- continuityPrefix = `[Session resumed — recent context:\n${ctx}]\n\n`;
164
- }
165
- }
166
-
167
152
  const prompt = isGroup
168
- ? `${continuityPrefix}[${senderName}]${msgIdHint}: ${text}`
169
- : `${continuityPrefix}${text}${msgIdHint}`;
153
+ ? `[${senderName}]${msgIdHint}: ${text}`
154
+ : `${text}${msgIdHint}`;
170
155
 
171
156
  log("agent", `[${chatId}] <- (${text.length} chars)`);
172
157
  traceMessage(chatId, "in", text, { senderName, isGroup });
@@ -211,7 +196,7 @@ export async function handleMessage(
211
196
  model: activeModel,
212
197
  });
213
198
 
214
- if (session.turns === 0 && text) {
199
+ if (getSession(chatId).turns === 0 && text) {
215
200
  const cleanText = text
216
201
  .replace(/^\[.*?\]\s*/g, "")
217
202
  .replace(/\[msg_id:\d+\]\s*/g, "")