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.
- package/package.json +5 -6
- package/prompts/dream.md +6 -2
- package/prompts/mempalace.md +57 -0
- package/src/__tests__/cron-store-extended.test.ts +1 -1
- package/src/__tests__/dream.test.ts +118 -1
- package/src/__tests__/fuzz.test.ts +1 -1
- package/src/__tests__/gateway-retry.test.ts +0 -4
- package/src/__tests__/handlers.test.ts +0 -4
- package/src/__tests__/heartbeat.test.ts +3 -0
- package/src/__tests__/mempalace-plugin.test.ts +295 -0
- package/src/__tests__/plugin.test.ts +169 -0
- package/src/__tests__/storage-save-errors.test.ts +1 -1
- package/src/__tests__/time.test.ts +1 -1
- package/src/__tests__/watchdog.test.ts +1 -3
- package/src/__tests__/workspace.test.ts +0 -1
- package/src/bootstrap.ts +72 -7
- package/src/core/dream.ts +40 -6
- package/src/core/plugin.ts +103 -16
- package/src/frontend/telegram/handlers.ts +5 -17
- package/src/plugins/mempalace/index.ts +147 -0
- package/src/util/config.ts +11 -0
- package/src/util/log.ts +2 -1
- package/src/util/paths.ts +9 -0
|
@@ -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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
+
}
|
package/src/util/config.ts
CHANGED
|
@@ -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
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 */
|