talon-agent 1.2.0 → 1.3.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.
@@ -5,11 +5,7 @@
5
5
 
6
6
  import type { Bot, Context } from "grammy";
7
7
  import type { TalonConfig } from "../../util/config.js";
8
- import {
9
- splitMessage,
10
- markdownToTelegramHtml,
11
- escapeHtml,
12
- } from "./formatting.js";
8
+ import { markdownToTelegramHtml, escapeHtml } from "./formatting.js";
13
9
  import { execute } from "../../core/dispatcher.js";
14
10
  import { classify, friendlyMessage } from "../../core/errors.js";
15
11
  import {
@@ -391,7 +387,6 @@ const messageQueues = new Map<
391
387
  messages: QueuedMessage[];
392
388
  timer: ReturnType<typeof setTimeout>;
393
389
  bot: Bot;
394
- config: TalonConfig;
395
390
  numericChatId: number;
396
391
  queuedReactionMsgIds: number[];
397
392
  }
@@ -446,7 +441,6 @@ function isUserRateLimited(senderId: number): boolean {
446
441
  */
447
442
  function enqueueMessage(
448
443
  bot: Bot,
449
- config: TalonConfig,
450
444
  chatId: string,
451
445
  numericChatId: number,
452
446
  msg: QueuedMessage,
@@ -474,7 +468,6 @@ function enqueueMessage(
474
468
  messages: [msg],
475
469
  timer: setTimeout(() => flushQueue(chatId), DEBOUNCE_MS),
476
470
  bot,
477
- config,
478
471
  numericChatId,
479
472
  queuedReactionMsgIds: [] as number[],
480
473
  };
@@ -486,7 +479,7 @@ async function flushQueue(chatId: string): Promise<void> {
486
479
  if (!entry) return;
487
480
  messageQueues.delete(chatId);
488
481
 
489
- const { messages, bot, config, numericChatId, queuedReactionMsgIds } = entry;
482
+ const { messages, bot, numericChatId, queuedReactionMsgIds } = entry;
490
483
 
491
484
  // Clear hourglass reactions on queued messages now that we're processing
492
485
  for (const msgId of queuedReactionMsgIds) {
@@ -516,7 +509,6 @@ async function flushQueue(chatId: string): Promise<void> {
516
509
  try {
517
510
  await processAndReply({
518
511
  bot,
519
- config,
520
512
  chatId,
521
513
  numericChatId,
522
514
  replyToId: last.replyToId,
@@ -550,7 +542,6 @@ async function flushQueue(chatId: string): Promise<void> {
550
542
  await new Promise((r) => setTimeout(r, delayMs));
551
543
  await processAndReply({
552
544
  bot,
553
- config,
554
545
  chatId,
555
546
  numericChatId,
556
547
  replyToId: last.replyToId,
@@ -621,7 +612,6 @@ async function sendHtml(
621
612
  */
622
613
  type ProcessAndReplyParams = {
623
614
  bot: Bot;
624
- config: TalonConfig;
625
615
  chatId: string | number;
626
616
  numericChatId: number;
627
617
  replyToId: number;
@@ -692,7 +682,6 @@ function createStreamCallbacks(
692
682
  async function processAndReply(params: ProcessAndReplyParams): Promise<void> {
693
683
  const {
694
684
  bot,
695
- config,
696
685
  chatId,
697
686
  numericChatId,
698
687
  replyToId,
@@ -864,7 +853,7 @@ async function handleMediaMessage(
864
853
 
865
854
  const prompt = promptParts.join("\n");
866
855
 
867
- enqueueMessage(bot, config, chatId, ctx.chat.id, {
856
+ enqueueMessage(bot, chatId, ctx.chat.id, {
868
857
  prompt,
869
858
  replyToId: ctx.message.message_id,
870
859
  messageId: ctx.message.message_id,
@@ -920,7 +909,7 @@ export async function handleTextMessage(
920
909
  );
921
910
  const prompt = fwdCtx + replyCtx + replyPhotoCtx + (ctx.message.text ?? "");
922
911
 
923
- enqueueMessage(bot, config, chatId, ctx.chat.id, {
912
+ enqueueMessage(bot, chatId, ctx.chat.id, {
924
913
  prompt,
925
914
  replyToId: ctx.message.message_id,
926
915
  messageId: ctx.message.message_id,
@@ -1040,7 +1029,7 @@ export async function handleStickerMessage(
1040
1029
  .filter(Boolean)
1041
1030
  .join("\n");
1042
1031
 
1043
- enqueueMessage(bot, config, chatId, ctx.chat.id, {
1032
+ enqueueMessage(bot, chatId, ctx.chat.id, {
1044
1033
  prompt,
1045
1034
  replyToId: ctx.message.message_id,
1046
1035
  messageId: ctx.message.message_id,
@@ -1185,7 +1174,6 @@ export async function handleCallbackQuery(
1185
1174
 
1186
1175
  await processAndReply({
1187
1176
  bot,
1188
- config,
1189
1177
  chatId,
1190
1178
  numericChatId,
1191
1179
  replyToId,
@@ -0,0 +1,147 @@
1
+ /**
2
+ * MemPalace plugin — structured long-term memory with vector search.
3
+ *
4
+ * Registers the mempalace Python MCP server, giving the agent access to
5
+ * semantic memory search, knowledge graph operations, and diary entries.
6
+ *
7
+ * Configuration in ~/.talon/config.json:
8
+ * "mempalace": {
9
+ * "enabled": true,
10
+ * "palacePath": "/path/to/palace", // optional, defaults to ~/.talon/workspace/palace/
11
+ * "pythonPath": "/path/to/python" // optional, defaults to mempalace venv python (bin/python on Unix, Scripts/python.exe on Windows)
12
+ * }
13
+ */
14
+
15
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
16
+ import { resolve } from "node:path";
17
+ import { execFile as execFileCb, execFileSync } from "node:child_process";
18
+ import { promisify } from "node:util";
19
+ import type { TalonPlugin } from "../../core/plugin.js";
20
+ import { log, logWarn } from "../../util/log.js";
21
+ import { dirs } from "../../util/paths.js";
22
+
23
+ const execFile = promisify(execFileCb);
24
+
25
+ /** Load from ~/.talon/prompts/ (user-customisable, seeded on first run) */
26
+ const PROMPT_PATH = resolve(dirs.prompts, "mempalace.md");
27
+
28
+ /**
29
+ * Create a mempalace plugin instance with resolved paths.
30
+ * Uses a factory because MCP server command/args depend on runtime config.
31
+ */
32
+ export function createMempalacePlugin(config: {
33
+ pythonPath: string;
34
+ palacePath: string;
35
+ }): TalonPlugin {
36
+ const { pythonPath, palacePath } = config;
37
+
38
+ return {
39
+ name: "mempalace",
40
+ description:
41
+ "Memory palace — structured long-term memory with vector search",
42
+ version: "1.0.0",
43
+
44
+ mcpServer: {
45
+ command: pythonPath,
46
+ args: ["-m", "mempalace.mcp_server", "--palace", palacePath],
47
+ },
48
+
49
+ validateConfig() {
50
+ const errors: string[] = [];
51
+ if (!existsSync(pythonPath)) {
52
+ errors.push(
53
+ `Python binary not found at ${pythonPath}. Create or select a Python environment, set "pythonPath" to that interpreter, then run: ${pythonPath} -m pip install mempalace`,
54
+ );
55
+ return errors;
56
+ }
57
+
58
+ // Verify mempalace.mcp_server is importable (the actual module spawned by MCP)
59
+ try {
60
+ execFileSync(pythonPath, ["-c", "import mempalace.mcp_server"], {
61
+ timeout: 15_000,
62
+ stdio: "pipe",
63
+ });
64
+ } catch (err: unknown) {
65
+ const execErr =
66
+ err && typeof err === "object"
67
+ ? (err as {
68
+ code?: string;
69
+ signal?: string;
70
+ killed?: boolean;
71
+ stderr?: string | Buffer;
72
+ })
73
+ : undefined;
74
+ const code = execErr?.code;
75
+ if (code === "ENOENT" || code === "EACCES" || code === "EPERM") {
76
+ errors.push(
77
+ `Cannot execute Python at ${pythonPath} (${code}). Check that the path is correct and the binary is executable.`,
78
+ );
79
+ } else if (code === "ETIMEDOUT" || execErr?.killed || execErr?.signal) {
80
+ errors.push(
81
+ `Python import check timed out or was killed. The interpreter at ${pythonPath} may be unresponsive.`,
82
+ );
83
+ } else {
84
+ const stderr =
85
+ typeof execErr?.stderr === "string"
86
+ ? execErr.stderr.trim()
87
+ : Buffer.isBuffer(execErr?.stderr)
88
+ ? execErr.stderr.toString("utf-8").trim()
89
+ : "";
90
+ errors.push(
91
+ `mempalace package not installed or mcp_server submodule missing. Run: ${pythonPath} -m pip install mempalace${stderr ? `. Details: ${stderr}` : ""}`,
92
+ );
93
+ }
94
+ }
95
+
96
+ return errors.length > 0 ? errors : undefined;
97
+ },
98
+
99
+ async init() {
100
+ // Ensure palace directory exists
101
+ if (!existsSync(palacePath)) {
102
+ mkdirSync(palacePath, { recursive: true });
103
+ log("mempalace", `Created palace directory: ${palacePath}`);
104
+ }
105
+
106
+ // Quick smoke test — verify mempalace can import and access the palace path
107
+ try {
108
+ const { stdout } = await execFile(
109
+ pythonPath,
110
+ [
111
+ "-c",
112
+ `import mempalace; print(f"mempalace {mempalace.__version__}" if hasattr(mempalace, "__version__") else "mempalace ok")`,
113
+ ],
114
+ { timeout: 15_000 },
115
+ );
116
+ log("mempalace", stdout.trim() || "Module verified");
117
+ } catch {
118
+ // Non-fatal — MCP server handles lazy init
119
+ log(
120
+ "mempalace",
121
+ "Module import check skipped — MCP server will initialize on first use",
122
+ );
123
+ }
124
+
125
+ log("mempalace", `Ready (palace: ${palacePath})`);
126
+ },
127
+
128
+ getEnvVars() {
129
+ return {
130
+ MEMPALACE_PALACE_PATH: palacePath,
131
+ };
132
+ },
133
+
134
+ getSystemPromptAddition() {
135
+ try {
136
+ const template = readFileSync(PROMPT_PATH, "utf-8");
137
+ return template.replace(/\{\{palacePath\}\}/g, palacePath);
138
+ } catch (err) {
139
+ logWarn(
140
+ "mempalace",
141
+ `Failed to load prompt from ${PROMPT_PATH}: ${err instanceof Error ? err.message : err}`,
142
+ );
143
+ return `## MemPalace — Long-term Memory\n\nPalace location: \`${palacePath}\``;
144
+ }
145
+ },
146
+ };
147
+ }
@@ -44,6 +44,17 @@ const configSchema = z.object({
44
44
  timezone: z.string().optional(),
45
45
  plugins: z.array(pluginEntrySchema).default([]),
46
46
 
47
+ // MemPalace — structured long-term memory with vector search
48
+ mempalace: z
49
+ .object({
50
+ enabled: z.boolean().default(false),
51
+ /** Palace directory path (default: ~/.talon/workspace/palace/) */
52
+ palacePath: z.string().min(1).optional(),
53
+ /** Python binary path (default: ~/.talon/mempalace-venv/bin/python) */
54
+ pythonPath: z.string().min(1).optional(),
55
+ })
56
+ .optional(),
57
+
47
58
  // Display name shown in terminal UI (defaults to "Talon")
48
59
  botDisplayName: z.string().default("Talon"),
49
60
 
package/src/util/log.ts CHANGED
@@ -41,7 +41,8 @@ export type LogComponent =
41
41
  | "plugin"
42
42
  | "teams"
43
43
  | "config"
44
- | "access";
44
+ | "access"
45
+ | "mempalace";
45
46
 
46
47
  const LOG_FILE = files.log;
47
48
 
package/src/util/paths.ts CHANGED
@@ -51,6 +51,8 @@ export const dirs = {
51
51
  prompts: resolve(TALON_ROOT, "prompts"),
52
52
  /** Per-chat message traces: ~/.talon/data/traces/ */
53
53
  traces: resolve(TALON_ROOT, "data", "traces"),
54
+ /** MemPalace palace: ~/.talon/workspace/palace/ */
55
+ palace: resolve(TALON_ROOT, "workspace", "palace"),
54
56
  } as const;
55
57
 
56
58
  // ── Files ──────────────────────────────────────────────────────────────────
@@ -78,6 +80,13 @@ export const files = {
78
80
  userSession: resolve(TALON_ROOT, ".user-session"),
79
81
  /** PID file for daemon mode: ~/.talon/talon.pid */
80
82
  pid: resolve(TALON_ROOT, "talon.pid"),
83
+ /** MemPalace venv python binary (platform-dependent: bin/python on Unix, Scripts/python.exe on Windows) */
84
+ mempalacePython: resolve(
85
+ TALON_ROOT,
86
+ "mempalace-venv",
87
+ process.platform === "win32" ? "Scripts" : "bin",
88
+ process.platform === "win32" ? "python.exe" : "python",
89
+ ),
81
90
  /** Dream mode state: ~/.talon/workspace/memory/dream_state.json */
82
91
  dreamState: resolve(TALON_ROOT, "workspace", "memory", "dream_state.json"),
83
92
  /** Heartbeat state: ~/.talon/workspace/memory/heartbeat_state.json */