talon-agent 3.34.1 → 3.36.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.
Files changed (63) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +0 -10
  3. package/package.json +3 -3
  4. package/src/backend/codex/auth.ts +40 -1
  5. package/src/backend/codex/handler/message.ts +16 -1
  6. package/src/backend/codex/plan-usage.ts +27 -2
  7. package/src/backend/kilo/handler/message.ts +2 -0
  8. package/src/backend/kilo/server.ts +1 -0
  9. package/src/backend/opencode/handler/message.ts +2 -0
  10. package/src/backend/opencode/server.ts +1 -0
  11. package/src/backend/remote-server/chat-turn.ts +13 -3
  12. package/src/backend/remote-server/lifecycle.ts +37 -0
  13. package/src/backend/remote-server/server-bindings.ts +12 -0
  14. package/src/backend/remote-server/state.ts +8 -0
  15. package/src/backend/remote-server/turn.ts +93 -23
  16. package/src/bootstrap.ts +24 -10
  17. package/src/core/auth/expiry-monitor.ts +89 -0
  18. package/src/core/auth/login-flow.ts +247 -0
  19. package/src/core/auth/status.ts +193 -0
  20. package/src/core/engine/gateway-actions/history.ts +12 -1
  21. package/src/core/errors.ts +1 -1
  22. package/src/core/tools/history.ts +10 -1
  23. package/src/frontend/native/chat-wire.ts +6 -2
  24. package/src/frontend/native/emit.ts +30 -11
  25. package/src/frontend/native/handlers.ts +62 -13
  26. package/src/frontend/native/history.ts +30 -6
  27. package/src/frontend/native/media.ts +271 -15
  28. package/src/frontend/native/protocol.ts +34 -1
  29. package/src/frontend/native/queue.ts +4 -7
  30. package/src/frontend/native/routes/chats.ts +30 -12
  31. package/src/frontend/native/routes/host.ts +17 -5
  32. package/src/frontend/native/routes/params.ts +21 -0
  33. package/src/frontend/native/runtime.ts +16 -4
  34. package/src/frontend/native/server.ts +2 -35
  35. package/src/frontend/native/status.ts +3 -1
  36. package/src/frontend/native/turn.ts +39 -17
  37. package/src/frontend/shared/model-commands.ts +400 -0
  38. package/src/frontend/telegram/auth-panel.ts +205 -0
  39. package/src/frontend/telegram/callbacks/auth.ts +70 -0
  40. package/src/frontend/telegram/callbacks/index.ts +16 -0
  41. package/src/frontend/telegram/callbacks/whatsapp.ts +50 -0
  42. package/src/frontend/telegram/commands/auth.ts +58 -0
  43. package/src/frontend/telegram/commands/definitions.ts +4 -0
  44. package/src/frontend/telegram/commands/index.ts +10 -4
  45. package/src/frontend/telegram/commands/whatsapp-pairing.ts +121 -79
  46. package/src/frontend/whatsapp/actions/chat-info.ts +2 -36
  47. package/src/frontend/whatsapp/actions/history.ts +100 -0
  48. package/src/frontend/whatsapp/actions/index.ts +4 -1
  49. package/src/frontend/whatsapp/commands.ts +371 -0
  50. package/src/frontend/whatsapp/inbound.ts +24 -46
  51. package/src/frontend/whatsapp/index.ts +12 -1
  52. package/src/frontend/whatsapp/media-store.ts +20 -1
  53. package/src/frontend/whatsapp/message-store.ts +101 -24
  54. package/src/storage/db.ts +7 -1
  55. package/src/storage/history.ts +26 -2
  56. package/src/storage/repositories/history-repo.ts +61 -1
  57. package/src/storage/repositories/whatsapp-messages-repo.ts +101 -0
  58. package/src/storage/sql/db.sql +6 -0
  59. package/src/storage/sql/history.sql +22 -9
  60. package/src/storage/sql/schema.sql +27 -1
  61. package/src/storage/sql/statements.generated.ts +70 -11
  62. package/src/storage/sql/whatsapp-messages.sql +29 -0
  63. package/src/storage/whatsapp-messages.ts +51 -0
@@ -1,6 +1,9 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import type { Readable } from "node:stream";
3
+ import type { AttachmentRef } from "./params.js";
2
4
  import type {
3
5
  BackendOption,
6
+ ClientAttachment,
4
7
  BridgeEvent,
5
8
  BridgeStatus,
6
9
  ClientChat,
@@ -18,7 +21,13 @@ import type {
18
21
  import type { ConfigSnapshot } from "../settings.js";
19
22
 
20
23
  /** Optional attachment references carried alongside a sent message. */
21
- type SendOptions = {
24
+ export type SendOptions = {
25
+ /**
26
+ * Every file attached to the message, as the client's references to its
27
+ * own uploads. `imagePath` and `attachmentPath` below are the single-file
28
+ * shape older clients send; the handler folds them into the same list.
29
+ */
30
+ attachments?: AttachmentRef[];
22
31
  /** Relative bridge path to render inline (e.g. `/media?id=…`). */
23
32
  imagePath?: string;
24
33
  /** Absolute on-disk path handed to the model so it can read the file. */
@@ -41,12 +50,16 @@ export type BridgeServerHandlers = {
41
50
  search(query: string, chatId?: string): SearchResult[];
42
51
  /** Fire-and-forget: streams its results back through `broadcast`. */
43
52
  send(id: string, text: string, opts?: SendOptions): void;
44
- /** Persist an uploaded image and return its render path + on-disk path. */
53
+ /**
54
+ * Stream an uploaded file to disk and return its wire description. The body
55
+ * is consumed as it arrives (never buffered whole), so the size ceiling is
56
+ * disk, not memory.
57
+ */
45
58
  upload(
46
59
  filename: string,
47
60
  contentType: string,
48
- bytes: Buffer,
49
- ): Promise<{ imagePath: string; path: string }>;
61
+ body: Readable,
62
+ ): Promise<ClientAttachment>;
50
63
  listModels(
51
64
  id?: string,
52
65
  ):
@@ -139,7 +152,6 @@ export type RouteHost = {
139
152
  fingerprint: () => string | null;
140
153
  json: (res: ServerResponse, code: number, body: unknown) => void;
141
154
  readJson: (req: IncomingMessage) => Promise<Record<string, unknown>>;
142
- readRaw: (req: IncomingMessage, max: number) => Promise<Buffer>;
143
155
  corsHeaders: () => Record<string, string>;
144
156
  streamFile: (
145
157
  res: ServerResponse,
@@ -27,3 +27,24 @@ export function asPositiveInt(v: string | null): number | undefined {
27
27
  const n = Number(v);
28
28
  return Number.isInteger(n) && n > 0 ? n : undefined;
29
29
  }
30
+
31
+ /** A client's reference to a file it already uploaded. */
32
+ export type AttachmentRef = { url?: string; path?: string };
33
+
34
+ /**
35
+ * Coerce the `attachments` array on a `/send` body to reference shape. Only
36
+ * the identifying fields are read: name, size and type are whatever the
37
+ * daemon recorded when it wrote the file, never what the client claims now.
38
+ */
39
+ export function asAttachmentRefs(v: unknown): AttachmentRef[] {
40
+ if (!Array.isArray(v)) return [];
41
+ const refs: AttachmentRef[] = [];
42
+ for (const item of v) {
43
+ if (typeof item !== "object" || item === null) continue;
44
+ const row = item as Record<string, unknown>;
45
+ const url = asString(row.url);
46
+ const path = asString(row.path);
47
+ if (url || path) refs.push({ url, path });
48
+ }
49
+ return refs;
50
+ }
@@ -12,7 +12,12 @@ import type { TalonConfig } from "../../util/config.js";
12
12
  import type { Gateway } from "../../core/engine/gateway.js";
13
13
  import { getMeshService, type MeshService } from "../../core/mesh/index.js";
14
14
  import { NativeChats } from "./chats.js";
15
- import type { BridgeEvent, ClientToolCall, ContextInfo } from "./protocol.js";
15
+ import type {
16
+ BridgeEvent,
17
+ ClientAttachment,
18
+ ClientToolCall,
19
+ ContextInfo,
20
+ } from "./protocol.js";
16
21
 
17
22
  /** One tool call observed during a running turn, kept for mid-turn replay. */
18
23
  export type LiveToolEntry = {
@@ -21,11 +26,10 @@ export type LiveToolEntry = {
21
26
  done?: boolean;
22
27
  };
23
28
 
24
- /** A chat's single queued follow-up, with its attachment paths intact. */
29
+ /** A chat's single queued follow-up, with its attachments intact. */
25
30
  export type QueuedEntry = {
26
31
  text: string;
27
- imagePath?: string;
28
- attachmentPath?: string;
32
+ attachments: ClientAttachment[];
29
33
  };
30
34
 
31
35
  export type NativeRuntime = {
@@ -51,6 +55,13 @@ export type NativeRuntime = {
51
55
  * frontends behave).
52
56
  */
53
57
  readonly media: Map<string, string>;
58
+ /**
59
+ * Uploads this daemon run has accepted, keyed by their media id. `/send`
60
+ * resolves a client's attachment references through here rather than
61
+ * trusting the paths in the request body, so a message can only ever point
62
+ * the model at a file this daemon itself wrote to the uploads dir.
63
+ */
64
+ readonly uploads: Map<string, ClientAttachment>;
54
65
  /**
55
66
  * Live context-window fill per chat, refreshed at the end of each turn.
56
67
  * Cached (not computed inline) so the sync `toClientChat` projection stays
@@ -99,6 +110,7 @@ export function createNativeRuntime(
99
110
  chats: new NativeChats(),
100
111
  mesh: getMeshService(),
101
112
  media: new Map(),
113
+ uploads: new Map(),
102
114
  contextByChat: new Map(),
103
115
  liveTurns: new Map(),
104
116
  queuedByChat: new Map(),
@@ -24,9 +24,9 @@ import { createServer as createTlsServer } from "node:https";
24
24
  import { createHash, timingSafeEqual } from "node:crypto";
25
25
  import { createReadStream } from "node:fs";
26
26
  import { stat } from "node:fs/promises";
27
- import { extname } from "node:path";
28
27
  import { log, logError, logDebug, logWarn } from "../../util/log.js";
29
28
  import { formatFingerprint, type BridgeTlsIdentity } from "./tls.js";
29
+ import { contentTypeFor } from "./media.js";
30
30
  import { type BridgeEvent } from "./protocol.js";
31
31
  import { buildRoutes } from "./routes/index.js";
32
32
  import type { BridgeServerHandlers, RouteHost } from "./routes/host.js";
@@ -38,7 +38,7 @@ import {
38
38
  type RouteHandler,
39
39
  } from "./routes/table.js";
40
40
 
41
- export type { BridgeServerHandlers } from "./routes/host.js";
41
+ export type { BridgeServerHandlers, SendOptions } from "./routes/host.js";
42
42
  export { BRIDGE_ROUTE_AUTH, type BridgeRouteKey } from "./routes/table.js";
43
43
 
44
44
  const SSE_PING_MS = 25_000;
@@ -352,7 +352,6 @@ export class BridgeServer {
352
352
  fingerprint: () => this.getFingerprint(),
353
353
  json: (res, code, body) => this.json(res, code, body),
354
354
  readJson: (req) => this.readJson(req),
355
- readRaw: (req, max) => this.readRaw(req, max),
356
355
  corsHeaders: () => this.corsHeaders(),
357
356
  streamFile: (res, file) => this.streamFile(res, file),
358
357
  serveMedia: (res, id) => this.serveMedia(res, id),
@@ -591,17 +590,6 @@ export class BridgeServer {
591
590
  }
592
591
 
593
592
  /** Read a raw request body (binary-safe) up to `max` bytes. */
594
- private async readRaw(req: IncomingMessage, max: number): Promise<Buffer> {
595
- const chunks: Buffer[] = [];
596
- let total = 0;
597
- for await (const chunk of req) {
598
- total += (chunk as Buffer).length;
599
- if (total > max) throw new Error("Upload too large");
600
- chunks.push(chunk as Buffer);
601
- }
602
- return Buffer.concat(chunks);
603
- }
604
-
605
593
  private async readJson(
606
594
  req: IncomingMessage,
607
595
  ): Promise<Record<string, unknown>> {
@@ -621,24 +609,3 @@ export class BridgeServer {
621
609
  return parsed as Record<string, unknown>;
622
610
  }
623
611
  }
624
-
625
- /** Minimal image content-type map for the media endpoint. */
626
- function contentTypeFor(filePath: string): string {
627
- switch (extname(filePath).toLowerCase()) {
628
- case ".png":
629
- return "image/png";
630
- case ".jpg":
631
- case ".jpeg":
632
- return "image/jpeg";
633
- case ".gif":
634
- return "image/gif";
635
- case ".webp":
636
- return "image/webp";
637
- case ".bmp":
638
- return "image/bmp";
639
- case ".svg":
640
- return "image/svg+xml";
641
- default:
642
- return "application/octet-stream";
643
- }
644
- }
@@ -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