talon-agent 3.35.0 → 3.37.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.
@@ -12,7 +12,9 @@ export function bridgeStatus(runtime: NativeRuntime): BridgeStatus {
12
12
  return {
13
13
  app: "talon-bridge",
14
14
  protocol: BRIDGE_PROTOCOL_VERSION,
15
- capabilities: ["mesh", "mesh-commands", "plugins-skills"],
15
+ // "attachments": /upload streams any file type and /send takes an
16
+ // `attachments` list, so a client can stage several files per message.
17
+ capabilities: ["mesh", "mesh-commands", "plugins-skills", "attachments"],
16
18
  botName: runtime.botName,
17
19
  backend: config.backend,
18
20
  model: resolveModel(config.model)?.displayName ?? config.model,
@@ -16,7 +16,7 @@ import { recordTurnMeta } from "./turn-meta.js";
16
16
  import type { ChatEntry } from "./chats.js";
17
17
  import { refreshContext } from "./context.js";
18
18
  import { emitAssistant, emitUser } from "./emit.js";
19
- import type { BridgeEvent } from "./protocol.js";
19
+ import type { BridgeEvent, ClientAttachment } from "./protocol.js";
20
20
  import { takeQueued } from "./queue.js";
21
21
  import type { LiveToolEntry, NativeRuntime } from "./runtime.js";
22
22
  import { summarizeToolResult } from "./tool-result.js";
@@ -24,7 +24,38 @@ import { summarizeToolResult } from "./tool-result.js";
24
24
  /** The tool calls of one running turn, in call order. */
25
25
  type TurnTools = Map<string, LiveToolEntry>;
26
26
 
27
- export type StartTurnOptions = { imagePath?: string; attachmentPath?: string };
27
+ export type StartTurnOptions = { attachments?: ClientAttachment[] };
28
+
29
+ /** Human-readable size for the attachment lines handed to the model. */
30
+ function formatSize(bytes: number): string {
31
+ if (bytes < 1024) return `${bytes} B`;
32
+ const units = ["KB", "MB", "GB", "TB"];
33
+ let value = bytes / 1024;
34
+ let unit = 0;
35
+ while (value >= 1024 && unit < units.length - 1) {
36
+ value /= 1024;
37
+ unit += 1;
38
+ }
39
+ return `${value < 10 ? value.toFixed(1) : Math.round(value)} ${units[unit]}`;
40
+ }
41
+
42
+ /**
43
+ * Point the model at the files the user attached. Each gets its own line with
44
+ * an absolute path so the model can read it with its own tools; images keep
45
+ * the `[Attached image: …]` wording backends already recognise.
46
+ */
47
+ export function attachmentPrompt(
48
+ text: string,
49
+ attachments: ClientAttachment[],
50
+ ): string {
51
+ if (!attachments.length) return text;
52
+ const lines = attachments.map((a) =>
53
+ a.image
54
+ ? `[Attached image: ${a.path}]`
55
+ : `[Attached file: ${a.name} (${a.mimeType}, ${formatSize(a.size)}) at ${a.path}]`,
56
+ );
57
+ return `${text ? `${text}\n\n` : ""}${lines.join("\n")}`;
58
+ }
28
59
 
29
60
  /** True when a turn is currently running for a chat (used to decide queue). */
30
61
  export function isBusy(runtime: NativeRuntime, chatId: string): boolean {
@@ -247,7 +278,7 @@ async function runTurn(
247
278
  entry: ChatEntry,
248
279
  text: string,
249
280
  messageId: number,
250
- attachmentPath?: string,
281
+ attachments: ClientAttachment[],
251
282
  ): Promise<void> {
252
283
  const start = Date.now();
253
284
  // Tool calls observed during this turn, in call order — recorded into the
@@ -256,10 +287,7 @@ async function runTurn(
256
287
  // the activity so far (cleared in the `finally`).
257
288
  const turnTools: TurnTools = new Map();
258
289
  runtime.liveTurns.set(entry.id, turnTools);
259
- // Point the model at an attached image so it can read the file itself.
260
- const prompt = attachmentPath
261
- ? `${text ? `${text}\n\n` : ""}[Attached image: ${attachmentPath}]`
262
- : text;
290
+ const prompt = attachmentPrompt(text, attachments);
263
291
  try {
264
292
  const result = await execute({
265
293
  chatId: entry.id,
@@ -287,8 +315,7 @@ async function runTurn(
287
315
  if (queued) {
288
316
  setImmediate(() =>
289
317
  startTurn(runtime, entry, queued.text, {
290
- imagePath: queued.imagePath,
291
- attachmentPath: queued.attachmentPath,
318
+ attachments: queued.attachments,
292
319
  }),
293
320
  );
294
321
  }
@@ -303,16 +330,11 @@ export function startTurn(
303
330
  text: string,
304
331
  options?: StartTurnOptions,
305
332
  ): void {
306
- const messageId = emitUser(
307
- runtime,
308
- entry,
309
- text,
310
- options?.imagePath,
311
- options?.attachmentPath,
312
- );
333
+ const attachments = options?.attachments ?? [];
334
+ const messageId = emitUser(runtime, entry, text, attachments);
313
335
  runtime.broadcast({ kind: "turn_start", chatId: entry.id });
314
336
  runtime.broadcast({ kind: "typing", chatId: entry.id, on: true });
315
- void runTurn(runtime, entry, text, messageId, options?.attachmentPath);
337
+ void runTurn(runtime, entry, text, messageId, attachments);
316
338
  }
317
339
 
318
340
  /** Best-effort interrupt of a chat's in-flight turn. `true` if one was
@@ -30,10 +30,10 @@
30
30
  * other.
31
31
  */
32
32
 
33
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
34
- import { join, resolve } from "node:path";
35
- import { tmpdir } from "node:os";
33
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
34
+ import { dirname, resolve } from "node:path";
36
35
  import type { TalonPlugin } from "../../core/plugin/types.js";
36
+ import { files } from "../../util/paths.js";
37
37
  import { log } from "../../util/log.js";
38
38
 
39
39
  export function createPlaywrightPlugin(config: {
@@ -66,6 +66,11 @@ export function createPlaywrightPlugin(config: {
66
66
 
67
67
  const args: string[] = [];
68
68
 
69
+ // Endpoint mode writes a config file the MCP child reads at every spawn;
70
+ // `writeMcpConfig` is re-run before each spawn via `prepareMcpSpawn` so a
71
+ // deleted config heals instead of failing every browser tool until restart.
72
+ let writeMcpConfig: (() => void) | undefined;
73
+
69
74
  if (endpoint) {
70
75
  // Connect to the existing browser (e.g. the Camoufox websocket server)
71
76
  // via a generated MCP config file: `browser.remoteEndpoint` is the
@@ -79,11 +84,12 @@ export function createPlaywrightPlugin(config: {
79
84
  remoteEndpoint: endpoint,
80
85
  },
81
86
  };
82
- const configPath = join(
83
- tmpdir(),
84
- `talon-playwright-mcp-${process.pid}.json`,
85
- );
86
- writeFileSync(configPath, JSON.stringify(mcpConfig));
87
+ const configPath = files.playwrightMcpConfig;
88
+ writeMcpConfig = () => {
89
+ mkdirSync(dirname(configPath), { recursive: true });
90
+ writeFileSync(configPath, JSON.stringify(mcpConfig));
91
+ };
92
+ writeMcpConfig();
87
93
  args.push("--config", configPath);
88
94
  } else {
89
95
  args.push("--no-sandbox");
@@ -107,6 +113,10 @@ export function createPlaywrightPlugin(config: {
107
113
  args: [mcpBin, ...args],
108
114
  },
109
115
 
116
+ prepareMcpSpawn() {
117
+ writeMcpConfig?.();
118
+ },
119
+
110
120
  validateConfig() {
111
121
  const errors: string[] = [];
112
122
 
package/src/storage/db.ts CHANGED
@@ -68,7 +68,8 @@ let db: SqlDatabase | null = null;
68
68
  *
69
69
  * Column reconciliation runs first: `ALTER TABLE … ADD COLUMN` has no
70
70
  * IF NOT EXISTS form, so columns added to already-shipped tables
71
- * (media_index.content_hash, sessions.metrics) are ensured by attempting the ALTER and
71
+ * (media_index.content_hash, sessions.metrics,
72
+ * history_messages.attachments) are ensured by attempting the ALTER and
72
73
  * swallowing the two expected failures — "duplicate column name"
73
74
  * (column already there) and "no such table" (fresh database; the
74
75
  * CREATE TABLE in schema.sql includes the column).
@@ -94,6 +95,11 @@ function ensureSchema(database: SqlDatabase): void {
94
95
  } catch {
95
96
  /* duplicate column or no such table — both mean nothing to do */
96
97
  }
98
+ try {
99
+ database.exec(dbSql.addHistoryAttachmentsColumn);
100
+ } catch {
101
+ /* duplicate column or no such table — both mean nothing to do */
102
+ }
97
103
  database.exec("BEGIN");
98
104
  try {
99
105
  database.exec(SCHEMA);
@@ -26,7 +26,10 @@ import { formatSmartTimestamp, formatRelativeAge } from "../util/time.js";
26
26
  import { importLegacyJson } from "./legacy-import.js";
27
27
  import * as repo from "./repositories/history-repo.js";
28
28
 
29
- export type { HistoryMessage } from "./repositories/history-repo.js";
29
+ export type {
30
+ HistoryMessage,
31
+ MessageAttachment,
32
+ } from "./repositories/history-repo.js";
30
33
  import type { HistoryMessage } from "./repositories/history-repo.js";
31
34
 
32
35
  // ── Persistence lifecycle ───────────────────────────────────────────────────
@@ -9,6 +9,24 @@ import { escapeLike } from "../../native/sqlguard.js";
9
9
  import { getDatabase, inTransaction } from "../db.js";
10
10
  import { historySql } from "../sql/statements.generated.js";
11
11
  /** One chat message as the domain sees it — the shape the rows map to. */
12
+ /**
13
+ * A file attached to a message, persisted with it. The bridge's own
14
+ * `ClientAttachment` is this plus a per-run `/media` URL — storage keeps only
15
+ * what survives a restart, and the URL is minted fresh on read.
16
+ */
17
+ export type MessageAttachment = {
18
+ /** Absolute path on this host. */
19
+ path: string;
20
+ /** Original file name, for display. */
21
+ name: string;
22
+ /** Size in bytes. */
23
+ size: number;
24
+ /** Best-effort MIME type. */
25
+ mimeType: string;
26
+ /** True when clients render it inline as an image. */
27
+ image: boolean;
28
+ };
29
+
12
30
  export type HistoryMessage = {
13
31
  msgId: number;
14
32
  senderId: number;
@@ -25,8 +43,11 @@ export type HistoryMessage = {
25
43
  mediaType?:
26
44
  "photo" | "document" | "voice" | "sticker" | "video" | "animation";
27
45
  stickerFileId?: string;
28
- /** Saved file path for downloaded media. */
46
+ /** Saved file path for downloaded media (the first attachment's, when
47
+ * a message carries several). */
29
48
  filePath?: string;
49
+ /** Every file attached to this message, oldest row shape holds none. */
50
+ attachments?: MessageAttachment[];
30
51
  };
31
52
 
32
53
  type Row = {
@@ -40,8 +61,31 @@ type Row = {
40
61
  media_type: string | null;
41
62
  sticker_file_id: string | null;
42
63
  file_path: string | null;
64
+ attachments: string | null;
43
65
  };
44
66
 
67
+ /**
68
+ * Parse the persisted attachments column. Anything unreadable (hand-edited
69
+ * row, a shape from a future version) degrades to "no attachments" rather
70
+ * than failing the whole history read.
71
+ */
72
+ function parseAttachments(raw: string | null): MessageAttachment[] | undefined {
73
+ if (!raw) return undefined;
74
+ try {
75
+ const parsed: unknown = JSON.parse(raw);
76
+ if (!Array.isArray(parsed)) return undefined;
77
+ const rows = parsed.filter(
78
+ (a): a is MessageAttachment =>
79
+ typeof a === "object" &&
80
+ a !== null &&
81
+ typeof (a as MessageAttachment).path === "string",
82
+ );
83
+ return rows.length ? rows : undefined;
84
+ } catch {
85
+ return undefined;
86
+ }
87
+ }
88
+
45
89
  function rowToMessage(row: Row): HistoryMessage {
46
90
  return {
47
91
  msgId: row.msg_id,
@@ -54,6 +98,7 @@ function rowToMessage(row: Row): HistoryMessage {
54
98
  mediaType: (row.media_type ?? undefined) as HistoryMessage["mediaType"],
55
99
  stickerFileId: row.sticker_file_id ?? undefined,
56
100
  filePath: row.file_path ?? undefined,
101
+ attachments: parseAttachments(row.attachments),
57
102
  };
58
103
  }
59
104
 
@@ -72,6 +117,7 @@ export function insert(chatId: string, msg: HistoryMessage): void {
72
117
  msg.mediaType ?? null,
73
118
  msg.stickerFileId ?? null,
74
119
  msg.filePath ?? null,
120
+ msg.attachments?.length ? JSON.stringify(msg.attachments) : null,
75
121
  );
76
122
  }
77
123
 
@@ -19,3 +19,9 @@ ALTER TABLE history_messages ADD COLUMN sender_handle TEXT
19
19
  -- Column reconciliation for databases that shipped before per-session
20
20
  -- metrics existed. Fresh databases get the column via schema.sql.
21
21
  ALTER TABLE sessions ADD COLUMN metrics TEXT NOT NULL DEFAULT '{"lifetime":{"counters":{"queries":0,"toolCalls":0,"turnsWithTools":0,"apiCalls":0,"inputTokens":0,"outputTokens":0,"cacheReadTokens":0,"cacheWriteTokens":0,"failedTurns":0,"flowViolationRetries":0,"flowViolationCapExhausted":0,"trailingTextDropped":0},"latency":{"count":0,"sumMs":0,"minMs":null,"maxMs":0},"toolCallsByName":{},"backend":{},"cacheHitPercent":{"count":0,"sumMs":0,"minMs":null,"maxMs":0},"toolCallsPerTurn":{"count":0,"sumMs":0,"minMs":null,"maxMs":0},"apiCallsPerTurn":{"count":0,"sumMs":0,"minMs":null,"maxMs":0}},"buckets":{}}'
22
+
23
+ -- name: addHistoryAttachmentsColumn
24
+ -- Column reconciliation for databases that shipped before a message could
25
+ -- carry more than one attachment. Fresh databases get the column via
26
+ -- schema.sql.
27
+ ALTER TABLE history_messages ADD COLUMN attachments TEXT
@@ -4,12 +4,13 @@
4
4
  -- name: insert
5
5
  INSERT OR IGNORE INTO history_messages
6
6
  (chat_id, msg_id, sender_id, sender_name, sender_handle, text,
7
- reply_to_msg_id, timestamp, media_type, sticker_file_id, file_path)
8
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
7
+ reply_to_msg_id, timestamp, media_type, sticker_file_id, file_path,
8
+ attachments)
9
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
9
10
 
10
11
  -- name: recent
11
12
  SELECT msg_id, sender_id, sender_name, sender_handle, text, reply_to_msg_id,
12
- timestamp, media_type, sticker_file_id, file_path
13
+ timestamp, media_type, sticker_file_id, file_path, attachments
13
14
  FROM history_messages
14
15
  WHERE chat_id = ? ORDER BY id DESC LIMIT ?
15
16
 
@@ -17,7 +18,7 @@ WHERE chat_id = ? ORDER BY id DESC LIMIT ?
17
18
  -- Scroll-back pagination: the window of messages strictly older than a
18
19
  -- given msg_id, newest-first (the repository reverses to chronological).
19
20
  SELECT msg_id, sender_id, sender_name, sender_handle, text, reply_to_msg_id,
20
- timestamp, media_type, sticker_file_id, file_path
21
+ timestamp, media_type, sticker_file_id, file_path, attachments
21
22
  FROM history_messages
22
23
  WHERE chat_id = ? AND msg_id < ? ORDER BY id DESC LIMIT ?
23
24
 
@@ -25,7 +26,7 @@ WHERE chat_id = ? AND msg_id < ? ORDER BY id DESC LIMIT ?
25
26
  -- Time-cursor variant of recentBefore for the read_history `before` date
26
27
  -- parameter: the newest `limit` messages strictly older than a timestamp.
27
28
  SELECT msg_id, sender_id, sender_name, sender_handle, text, reply_to_msg_id,
28
- timestamp, media_type, sticker_file_id, file_path
29
+ timestamp, media_type, sticker_file_id, file_path, attachments
29
30
  FROM history_messages
30
31
  WHERE chat_id = ? AND timestamp < ? ORDER BY id DESC LIMIT ?
31
32
 
@@ -39,7 +40,7 @@ DELETE FROM history_messages WHERE chat_id = ?
39
40
  -- The match param must already be a valid FTS5 expression
40
41
  -- (see history.ts ftsQuery).
41
42
  SELECT msg_id, sender_id, sender_name, sender_handle, text, reply_to_msg_id,
42
- timestamp, media_type, sticker_file_id, file_path
43
+ timestamp, media_type, sticker_file_id, file_path, attachments
43
44
  FROM history_messages
44
45
  WHERE chat_id = ?
45
46
  AND id IN (SELECT rowid FROM history_fts WHERE history_fts MATCH ?)
@@ -50,7 +51,7 @@ ORDER BY id DESC LIMIT ?
50
51
  -- search_history `after` / `before` date parameters. Either bound may be
51
52
  -- the open end of the range (0 / a far-future value).
52
53
  SELECT msg_id, sender_id, sender_name, sender_handle, text, reply_to_msg_id,
53
- timestamp, media_type, sticker_file_id, file_path
54
+ timestamp, media_type, sticker_file_id, file_path, attachments
54
55
  FROM history_messages
55
56
  WHERE chat_id = ?
56
57
  AND id IN (SELECT rowid FROM history_fts WHERE history_fts MATCH ?)
@@ -60,20 +61,20 @@ ORDER BY id DESC LIMIT ?
60
61
  -- name: bySenderName
61
62
  -- The fragment param is LIKE-escaped by the repository (backslash escape).
62
63
  SELECT msg_id, sender_id, sender_name, sender_handle, text, reply_to_msg_id,
63
- timestamp, media_type, sticker_file_id, file_path
64
+ timestamp, media_type, sticker_file_id, file_path, attachments
64
65
  FROM history_messages
65
66
  WHERE chat_id = ? AND lower(sender_name) LIKE ? ESCAPE '\'
66
67
  ORDER BY id DESC LIMIT ?
67
68
 
68
69
  -- name: byMsgId
69
70
  SELECT msg_id, sender_id, sender_name, sender_handle, text, reply_to_msg_id,
70
- timestamp, media_type, sticker_file_id, file_path
71
+ timestamp, media_type, sticker_file_id, file_path, attachments
71
72
  FROM history_messages
72
73
  WHERE chat_id = ? AND msg_id = ? ORDER BY id DESC LIMIT 1
73
74
 
74
75
  -- name: bySenderId
75
76
  SELECT msg_id, sender_id, sender_name, sender_handle, text, reply_to_msg_id,
76
- timestamp, media_type, sticker_file_id, file_path
77
+ timestamp, media_type, sticker_file_id, file_path, attachments
77
78
  FROM history_messages
78
79
  WHERE chat_id = ? AND sender_id = ? ORDER BY id DESC LIMIT ?
79
80
 
@@ -20,7 +20,12 @@ CREATE TABLE IF NOT EXISTS history_messages (
20
20
  timestamp INTEGER NOT NULL,
21
21
  media_type TEXT,
22
22
  sticker_file_id TEXT,
23
- file_path TEXT
23
+ file_path TEXT,
24
+ -- Files attached to this message, as a JSON array of
25
+ -- {path,name,size,mimeType,image}. A message can carry several (the
26
+ -- companion's composer stages any number), so file_path above holds
27
+ -- only the first for the pre-multi-file row shape. Null when none.
28
+ attachments TEXT
24
29
  );
25
30
  CREATE INDEX IF NOT EXISTS idx_history_chat ON history_messages(chat_id, id);
26
31
  CREATE UNIQUE INDEX IF NOT EXISTS idx_history_chat_msg ON history_messages(chat_id, msg_id);
@@ -25,7 +25,12 @@ CREATE TABLE IF NOT EXISTS history_messages (
25
25
  timestamp INTEGER NOT NULL,
26
26
  media_type TEXT,
27
27
  sticker_file_id TEXT,
28
- file_path TEXT
28
+ file_path TEXT,
29
+ -- Files attached to this message, as a JSON array of
30
+ -- {path,name,size,mimeType,image}. A message can carry several (the
31
+ -- companion's composer stages any number), so file_path above holds
32
+ -- only the first for the pre-multi-file row shape. Null when none.
33
+ attachments TEXT
29
34
  );
30
35
  CREATE INDEX IF NOT EXISTS idx_history_chat ON history_messages(chat_id, id);
31
36
  CREATE UNIQUE INDEX IF NOT EXISTS idx_history_chat_msg ON history_messages(chat_id, msg_id);
@@ -324,6 +329,10 @@ ALTER TABLE history_messages ADD COLUMN sender_handle TEXT`,
324
329
  addSessionsMetricsColumn: `-- Column reconciliation for databases that shipped before per-session
325
330
  -- metrics existed. Fresh databases get the column via schema.sql.
326
331
  ALTER TABLE sessions ADD COLUMN metrics TEXT NOT NULL DEFAULT '{"lifetime":{"counters":{"queries":0,"toolCalls":0,"turnsWithTools":0,"apiCalls":0,"inputTokens":0,"outputTokens":0,"cacheReadTokens":0,"cacheWriteTokens":0,"failedTurns":0,"flowViolationRetries":0,"flowViolationCapExhausted":0,"trailingTextDropped":0},"latency":{"count":0,"sumMs":0,"minMs":null,"maxMs":0},"toolCallsByName":{},"backend":{},"cacheHitPercent":{"count":0,"sumMs":0,"minMs":null,"maxMs":0},"toolCallsPerTurn":{"count":0,"sumMs":0,"minMs":null,"maxMs":0},"apiCallsPerTurn":{"count":0,"sumMs":0,"minMs":null,"maxMs":0}},"buckets":{}}'`,
332
+ addHistoryAttachmentsColumn: `-- Column reconciliation for databases that shipped before a message could
333
+ -- carry more than one attachment. Fresh databases get the column via
334
+ -- schema.sql.
335
+ ALTER TABLE history_messages ADD COLUMN attachments TEXT`,
327
336
  } as const;
328
337
 
329
338
  export const goalsSql = {
@@ -355,22 +364,23 @@ WHERE chat_id = ? AND status IN (/* statuses */)`,
355
364
  export const historySql = {
356
365
  insert: `INSERT OR IGNORE INTO history_messages
357
366
  (chat_id, msg_id, sender_id, sender_name, sender_handle, text,
358
- reply_to_msg_id, timestamp, media_type, sticker_file_id, file_path)
359
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
367
+ reply_to_msg_id, timestamp, media_type, sticker_file_id, file_path,
368
+ attachments)
369
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
360
370
  recent: `SELECT msg_id, sender_id, sender_name, sender_handle, text, reply_to_msg_id,
361
- timestamp, media_type, sticker_file_id, file_path
371
+ timestamp, media_type, sticker_file_id, file_path, attachments
362
372
  FROM history_messages
363
373
  WHERE chat_id = ? ORDER BY id DESC LIMIT ?`,
364
374
  recentBefore: `-- Scroll-back pagination: the window of messages strictly older than a
365
375
  -- given msg_id, newest-first (the repository reverses to chronological).
366
376
  SELECT msg_id, sender_id, sender_name, sender_handle, text, reply_to_msg_id,
367
- timestamp, media_type, sticker_file_id, file_path
377
+ timestamp, media_type, sticker_file_id, file_path, attachments
368
378
  FROM history_messages
369
379
  WHERE chat_id = ? AND msg_id < ? ORDER BY id DESC LIMIT ?`,
370
380
  recentBeforeTime: `-- Time-cursor variant of recentBefore for the read_history \`before\` date
371
381
  -- parameter: the newest \`limit\` messages strictly older than a timestamp.
372
382
  SELECT msg_id, sender_id, sender_name, sender_handle, text, reply_to_msg_id,
373
- timestamp, media_type, sticker_file_id, file_path
383
+ timestamp, media_type, sticker_file_id, file_path, attachments
374
384
  FROM history_messages
375
385
  WHERE chat_id = ? AND timestamp < ? ORDER BY id DESC LIMIT ?`,
376
386
  setFilePath: `UPDATE history_messages SET file_path = ? WHERE chat_id = ? AND msg_id = ?`,
@@ -378,7 +388,7 @@ WHERE chat_id = ? AND timestamp < ? ORDER BY id DESC LIMIT ?`,
378
388
  searchFts: `-- The match param must already be a valid FTS5 expression
379
389
  -- (see history.ts ftsQuery).
380
390
  SELECT msg_id, sender_id, sender_name, sender_handle, text, reply_to_msg_id,
381
- timestamp, media_type, sticker_file_id, file_path
391
+ timestamp, media_type, sticker_file_id, file_path, attachments
382
392
  FROM history_messages
383
393
  WHERE chat_id = ?
384
394
  AND id IN (SELECT rowid FROM history_fts WHERE history_fts MATCH ?)
@@ -387,7 +397,7 @@ ORDER BY id DESC LIMIT ?`,
387
397
  -- search_history \`after\` / \`before\` date parameters. Either bound may be
388
398
  -- the open end of the range (0 / a far-future value).
389
399
  SELECT msg_id, sender_id, sender_name, sender_handle, text, reply_to_msg_id,
390
- timestamp, media_type, sticker_file_id, file_path
400
+ timestamp, media_type, sticker_file_id, file_path, attachments
391
401
  FROM history_messages
392
402
  WHERE chat_id = ?
393
403
  AND id IN (SELECT rowid FROM history_fts WHERE history_fts MATCH ?)
@@ -395,16 +405,16 @@ WHERE chat_id = ?
395
405
  ORDER BY id DESC LIMIT ?`,
396
406
  bySenderName: `-- The fragment param is LIKE-escaped by the repository (backslash escape).
397
407
  SELECT msg_id, sender_id, sender_name, sender_handle, text, reply_to_msg_id,
398
- timestamp, media_type, sticker_file_id, file_path
408
+ timestamp, media_type, sticker_file_id, file_path, attachments
399
409
  FROM history_messages
400
410
  WHERE chat_id = ? AND lower(sender_name) LIKE ? ESCAPE '\\'
401
411
  ORDER BY id DESC LIMIT ?`,
402
412
  byMsgId: `SELECT msg_id, sender_id, sender_name, sender_handle, text, reply_to_msg_id,
403
- timestamp, media_type, sticker_file_id, file_path
413
+ timestamp, media_type, sticker_file_id, file_path, attachments
404
414
  FROM history_messages
405
415
  WHERE chat_id = ? AND msg_id = ? ORDER BY id DESC LIMIT 1`,
406
416
  bySenderId: `SELECT msg_id, sender_id, sender_name, sender_handle, text, reply_to_msg_id,
407
- timestamp, media_type, sticker_file_id, file_path
417
+ timestamp, media_type, sticker_file_id, file_path, attachments
408
418
  FROM history_messages
409
419
  WHERE chat_id = ? AND sender_id = ? ORDER BY id DESC LIMIT ?`,
410
420
  latestMsgId: `SELECT msg_id FROM history_messages WHERE chat_id = ? ORDER BY id DESC LIMIT 1`,
package/src/util/paths.ts CHANGED
@@ -162,4 +162,15 @@ export const files = {
162
162
  * for what the data means.
163
163
  */
164
164
  codexOauthIncompat: resolve(TALON_ROOT, "data", "codex-oauth-incompat.json"),
165
+ /**
166
+ * Generated @playwright/mcp config (endpoint mode): ~/.talon/data/playwright-mcp.json
167
+ *
168
+ * Deliberately NOT in os.tmpdir(): the MCP child re-reads this file on every
169
+ * spawn, hours or days after the daemon wrote it, and a /tmp sweeper
170
+ * deleting it takes every browser tool down until the daemon restarts
171
+ * (2026-09-15: an hourly disk cleaner removed 676 `/tmp/talon-*` entries,
172
+ * and browser tools failed with ENOENT for the next 24h). The name is
173
+ * pid-free so each run overwrites the last instead of orphaning a file.
174
+ */
175
+ playwrightMcpConfig: resolve(TALON_ROOT, "data", "playwright-mcp.json"),
165
176
  } as const;