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.
- package/package.json +1 -1
- package/src/core/plugin/mcp.ts +13 -0
- package/src/core/plugin/registry.ts +12 -1
- package/src/core/plugin/types.ts +12 -0
- package/src/core/weaver/typing-loop.ts +39 -8
- package/src/frontend/native/chat-wire.ts +6 -2
- package/src/frontend/native/emit.ts +30 -11
- package/src/frontend/native/handlers.ts +79 -13
- package/src/frontend/native/history.ts +30 -6
- package/src/frontend/native/media.ts +271 -15
- package/src/frontend/native/protocol.ts +34 -1
- package/src/frontend/native/queue.ts +4 -7
- package/src/frontend/native/routes/chats.ts +45 -12
- package/src/frontend/native/routes/host.ts +17 -5
- package/src/frontend/native/routes/params.ts +21 -0
- package/src/frontend/native/runtime.ts +16 -4
- package/src/frontend/native/server.ts +2 -35
- package/src/frontend/native/status.ts +3 -1
- package/src/frontend/native/turn.ts +39 -17
- package/src/plugins/playwright/index.ts +18 -8
- package/src/storage/db.ts +7 -1
- package/src/storage/history.ts +4 -1
- package/src/storage/repositories/history-repo.ts +47 -1
- package/src/storage/sql/db.sql +6 -0
- package/src/storage/sql/history.sql +11 -10
- package/src/storage/sql/schema.sql +6 -1
- package/src/storage/sql/statements.generated.ts +21 -11
- package/src/util/paths.ts +11 -0
|
@@ -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
|
-
|
|
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 = {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
307
|
-
|
|
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,
|
|
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 {
|
|
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 =
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
|
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);
|
package/src/storage/history.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
package/src/storage/sql/db.sql
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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;
|