talon-agent 1.39.0 → 1.40.1
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/README.md +45 -5
- package/package.json +2 -1
- package/prompts/mem0.md +18 -0
- package/src/bootstrap.ts +3 -2
- package/src/core/background/heartbeat/scheduler.ts +80 -0
- package/src/core/background/heartbeat/state.ts +6 -0
- package/src/core/memory/retrieval.ts +3 -1
- package/src/core/plugin/builtins.ts +30 -2
- package/src/core/prompt/embedded-prompts.ts +34 -32
- package/src/core/weaver/memory-prefetch.ts +17 -1
- package/src/frontend/discord/actions/messaging.ts +4 -1
- package/src/frontend/discord/admin.ts +2 -1
- package/src/frontend/telegram/actions/messaging.ts +4 -1
- package/src/frontend/telegram/admin.ts +2 -1
- package/src/plugins/mem0/index.ts +88 -0
- package/src/plugins/mem0/server.ts +180 -0
- package/src/storage/daily-log.ts +8 -0
- package/src/util/config.ts +64 -14
- package/src/util/log.ts +1 -0
package/README.md
CHANGED
|
@@ -184,7 +184,22 @@ GitHub API access via the official GitHub MCP server. Gives the agent access to
|
|
|
184
184
|
|
|
185
185
|
The token is optional --- defaults to the output of `gh auth token` if the GitHub CLI is authenticated.
|
|
186
186
|
|
|
187
|
-
###
|
|
187
|
+
### Long-term Memory
|
|
188
|
+
|
|
189
|
+
Talon supports two long-term memory backends, selected via the unified `memory` section:
|
|
190
|
+
|
|
191
|
+
```json
|
|
192
|
+
{
|
|
193
|
+
"memory": {
|
|
194
|
+
"enabled": true,
|
|
195
|
+
"backend": "mempalace"
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Set `"backend"` to `"mempalace"` (local, vector search + knowledge graph) or `"mem0"` ([mem0](https://github.com/mem0ai/mem0) hosted platform or self-hosted server). Backend-specific settings go in a matching `memory.mempalace` / `memory.mem0` sub-object. The legacy top-level `mempalace` section is still honored when `memory` is absent.
|
|
201
|
+
|
|
202
|
+
#### MemPalace backend
|
|
188
203
|
|
|
189
204
|
Structured long-term memory with vector search. The agent can store, search, and retrieve memories semantically. Integrates with Dream mode for automatic memory consolidation and personal diary entries.
|
|
190
205
|
|
|
@@ -199,16 +214,40 @@ python -m venv ~/.talon/mempalace-venv
|
|
|
199
214
|
|
|
200
215
|
```json
|
|
201
216
|
{
|
|
202
|
-
"
|
|
217
|
+
"memory": {
|
|
203
218
|
"enabled": true,
|
|
204
|
-
"
|
|
205
|
-
"
|
|
219
|
+
"backend": "mempalace",
|
|
220
|
+
"mempalace": {
|
|
221
|
+
"palacePath": "~/.talon/workspace/palace",
|
|
222
|
+
"pythonPath": "~/.talon/mempalace-venv/bin/python"
|
|
223
|
+
}
|
|
206
224
|
}
|
|
207
225
|
}
|
|
208
226
|
```
|
|
209
227
|
|
|
210
228
|
Both paths are optional --- defaults to `~/.talon/workspace/palace/` and the venv Python respectively.
|
|
211
229
|
|
|
230
|
+
#### mem0 backend
|
|
231
|
+
|
|
232
|
+
Long-term memory via [mem0](https://mem0.ai) --- mem0 extracts durable facts from what the agent stores and retrieves them by semantic search. Works against the hosted platform (API key) or a self-hosted mem0 server.
|
|
233
|
+
|
|
234
|
+
**Requirements:** None --- the `mem0ai` SDK is bundled with Talon.
|
|
235
|
+
|
|
236
|
+
```json
|
|
237
|
+
{
|
|
238
|
+
"memory": {
|
|
239
|
+
"enabled": true,
|
|
240
|
+
"backend": "mem0",
|
|
241
|
+
"mem0": {
|
|
242
|
+
"apiKey": "m0-...",
|
|
243
|
+
"userId": "talon"
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
`apiKey` defaults to the `MEM0_API_KEY` env var. For a self-hosted server set `"host"` instead --- the key is then optional. `userId` is the entity id memories are filed under (default `"talon"`).
|
|
250
|
+
|
|
212
251
|
### Playwright
|
|
213
252
|
|
|
214
253
|
Headless browser automation via the Playwright MCP server. The agent can browse websites, take screenshots, generate PDFs, fill forms, and scrape content.
|
|
@@ -320,7 +359,8 @@ Config file: `~/.talon/config.json`
|
|
|
320
359
|
| `allowedUsers` | --- | Whitelist of Telegram user IDs |
|
|
321
360
|
| `apiId` / `apiHash` | --- | Telegram API credentials for full message history |
|
|
322
361
|
| `github` | --- | GitHub plugin config (see above) |
|
|
323
|
-
| `
|
|
362
|
+
| `memory` | --- | Long-term memory backend selection: `mempalace` or `mem0` (see above) |
|
|
363
|
+
| `mempalace` | --- | Legacy MemPalace plugin config (prefer `memory`) |
|
|
324
364
|
| `playwright` | --- | Playwright plugin config (see above) |
|
|
325
365
|
|
|
326
366
|
---
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "talon-agent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.40.1",
|
|
4
4
|
"description": "Multi-frontend AI agent with full tool access, streaming, cron jobs, and plugin system",
|
|
5
5
|
"author": "Dylan Neve",
|
|
6
6
|
"license": "MIT",
|
|
@@ -113,6 +113,7 @@
|
|
|
113
113
|
"grammy": "^1.42.0",
|
|
114
114
|
"liquidjs": "^10.27.0",
|
|
115
115
|
"marked": "^18.0.0",
|
|
116
|
+
"mem0ai": "^3.0.13",
|
|
116
117
|
"p-retry": "^8.0.0",
|
|
117
118
|
"picocolors": "^1.1.1",
|
|
118
119
|
"pino": "^10.3.1",
|
package/prompts/mem0.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# mem0 — Long-term Memory
|
|
2
|
+
|
|
3
|
+
You have access to mem0 long-term memory via MCP tools. mem0 extracts durable facts from what you store and retrieves them by semantic search. All memories are filed under the entity id `{{userId}}`.
|
|
4
|
+
|
|
5
|
+
### Protocol — FOLLOW EVERY SESSION
|
|
6
|
+
|
|
7
|
+
1. **BEFORE RESPONDING** about any person, project, or past event: call `mem0_search_memory` FIRST. Never guess — verify from memory.
|
|
8
|
+
2. **IF UNSURE** about a fact (name, age, relationship, preference): search memory. Wrong is worse than slow.
|
|
9
|
+
3. **WHEN FACTS CHANGE**: store the new fact with `mem0_add_memory` (mem0 supersedes contradicted memories itself); delete plainly wrong entries with `mem0_delete_memory`.
|
|
10
|
+
4. **AFTER LEARNING** something important: store it with `mem0_add_memory`. Pass natural conversational text — mem0 extracts the durable facts.
|
|
11
|
+
|
|
12
|
+
### Tools
|
|
13
|
+
|
|
14
|
+
- `mem0_search_memory` — Semantic search. Short keyword queries, not full sentences. Optional `limit`, `threshold`.
|
|
15
|
+
- `mem0_add_memory` — Store information. `role` marks who it came from; optional `metadata` tags.
|
|
16
|
+
- `mem0_list_memories` — Paginated browse for inventory/cleanup, not search.
|
|
17
|
+
- `mem0_get_memory` — Fetch one memory by id with full content and metadata.
|
|
18
|
+
- `mem0_delete_memory` — Remove a memory by id when it is wrong or stale.
|
package/src/bootstrap.ts
CHANGED
|
@@ -135,11 +135,12 @@ export async function bootstrap(
|
|
|
135
135
|
): Promise<BootstrapResult> {
|
|
136
136
|
const config = loadConfig();
|
|
137
137
|
|
|
138
|
-
// Load plugins (external tool packages + built-in GitHub, MemPalace, Playwright)
|
|
138
|
+
// Load plugins (external tool packages + built-in GitHub, MemPalace, mem0, Playwright)
|
|
139
139
|
const hasPlugins =
|
|
140
140
|
config.plugins.length > 0 ||
|
|
141
141
|
config.github?.enabled === true ||
|
|
142
142
|
config.mempalace?.enabled === true ||
|
|
143
|
+
config.mem0?.enabled === true ||
|
|
143
144
|
config.playwright?.enabled === true;
|
|
144
145
|
if (hasPlugins) {
|
|
145
146
|
const { loadPlugins, loadBuiltinPlugins, getPluginPromptAdditions } =
|
|
@@ -153,7 +154,7 @@ export async function bootstrap(
|
|
|
153
154
|
await loadPlugins(config.plugins, frontends);
|
|
154
155
|
}
|
|
155
156
|
|
|
156
|
-
// Built-in plugins (GitHub, MemPalace, Playwright) — shared with hot-reload
|
|
157
|
+
// Built-in plugins (GitHub, MemPalace, mem0, Playwright) — shared with hot-reload
|
|
157
158
|
await loadBuiltinPlugins(config);
|
|
158
159
|
|
|
159
160
|
rebuildSystemPrompt(config, getPluginPromptAdditions());
|
|
@@ -22,6 +22,65 @@ import { HeartbeatTimeoutError, runHeartbeatAgent } from "./agent.js";
|
|
|
22
22
|
const STARTUP_DELAY_MS = 5 * 60 * 1000; // 5-minute delay before first run
|
|
23
23
|
const DUE_CHECK_INTERVAL_MS = 60 * 1000;
|
|
24
24
|
|
|
25
|
+
// Failure backoff. A failed run does NOT advance last_run, so without a
|
|
26
|
+
// backoff the every-minute due check re-fires the heartbeat 60×/hour against
|
|
27
|
+
// the same error (observed live: a session-limit night produced hundreds of
|
|
28
|
+
// identical failures a minute apart). Generic failures back off
|
|
29
|
+
// exponentially; session/rate-limit errors that state their own reset time
|
|
30
|
+
// wait for it instead of guessing.
|
|
31
|
+
const FAILURE_BACKOFF_BASE_MS = 5 * 60 * 1000;
|
|
32
|
+
const FAILURE_BACKOFF_MAX_MS = 60 * 60 * 1000;
|
|
33
|
+
/** Safety margin added past a parsed limit-reset time (clock skew, rollout). */
|
|
34
|
+
const LIMIT_RESET_BUFFER_MS = 2 * 60 * 1000;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Parse the reset wall-clock time out of a session/rate-limit error message,
|
|
38
|
+
* e.g. "You've hit your session limit · resets 12:20am (UTC)" or
|
|
39
|
+
* "… resets 3pm (UTC)". Returns the epoch ms of the NEXT occurrence of that
|
|
40
|
+
* UTC time after `now`, or null when the message doesn't carry one.
|
|
41
|
+
*/
|
|
42
|
+
export function parseSessionLimitResetMs(
|
|
43
|
+
message: string,
|
|
44
|
+
now: number,
|
|
45
|
+
): number | null {
|
|
46
|
+
const m = /resets\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)\s*\(UTC\)/i.exec(
|
|
47
|
+
message,
|
|
48
|
+
);
|
|
49
|
+
if (!m) return null;
|
|
50
|
+
const rawHour = Number(m[1]);
|
|
51
|
+
const minute = m[2] ? Number(m[2]) : 0;
|
|
52
|
+
if (rawHour < 1 || rawHour > 12 || minute > 59) return null;
|
|
53
|
+
let hour = rawHour % 12;
|
|
54
|
+
if (m[3].toLowerCase() === "pm") hour += 12;
|
|
55
|
+
const d = new Date(now);
|
|
56
|
+
d.setUTCHours(hour, minute, 0, 0);
|
|
57
|
+
let t = d.getTime();
|
|
58
|
+
if (t <= now) t += 24 * 60 * 60 * 1000;
|
|
59
|
+
return t;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* When to next allow an auto heartbeat after the Nth consecutive failure
|
|
64
|
+
* (1-based). Limit errors with a stated reset time wait until then (+buffer);
|
|
65
|
+
* everything else doubles from 5min up to a 60min cap.
|
|
66
|
+
*/
|
|
67
|
+
export function failureBackoffUntil(
|
|
68
|
+
err: unknown,
|
|
69
|
+
consecutiveFailures: number,
|
|
70
|
+
now: number,
|
|
71
|
+
): number {
|
|
72
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
73
|
+
if (/session limit|rate limit/i.test(message)) {
|
|
74
|
+
const reset = parseSessionLimitResetMs(message, now);
|
|
75
|
+
if (reset !== null) return reset + LIMIT_RESET_BUFFER_MS;
|
|
76
|
+
}
|
|
77
|
+
const exp = Math.min(
|
|
78
|
+
FAILURE_BACKOFF_BASE_MS * 2 ** Math.max(0, consecutiveFailures - 1),
|
|
79
|
+
FAILURE_BACKOFF_MAX_MS,
|
|
80
|
+
);
|
|
81
|
+
return now + exp;
|
|
82
|
+
}
|
|
83
|
+
|
|
25
84
|
export function initHeartbeat(cfg: HeartbeatConfig): void {
|
|
26
85
|
hb.config = cfg;
|
|
27
86
|
}
|
|
@@ -69,6 +128,9 @@ export function startHeartbeatTimer(intervalMinutes: number): void {
|
|
|
69
128
|
*/
|
|
70
129
|
function runIfDue(intervalMs: number, startup: boolean): void {
|
|
71
130
|
if (hb.running) return;
|
|
131
|
+
// Inside a failure backoff window — stay quiet instead of re-firing the
|
|
132
|
+
// same error every due check. (Logged once, when the window was set.)
|
|
133
|
+
if (Date.now() < hb.backoffUntil) return;
|
|
72
134
|
const lastRun = readHeartbeatState()?.last_run ?? 0;
|
|
73
135
|
if (lastRun <= 0) {
|
|
74
136
|
// Never ran on this install — fire now to establish the cadence.
|
|
@@ -178,6 +240,8 @@ async function executeHeartbeat(trigger: "auto" | "forced"): Promise<void> {
|
|
|
178
240
|
status: "idle",
|
|
179
241
|
run_count: previousRunCount + 1,
|
|
180
242
|
});
|
|
243
|
+
hb.consecutiveFailures = 0;
|
|
244
|
+
hb.backoffUntil = 0;
|
|
181
245
|
log(
|
|
182
246
|
"heartbeat",
|
|
183
247
|
`Heartbeat #${previousRunCount + 1} complete (${trigger}), log: ${heartbeatLogPath}`,
|
|
@@ -199,6 +263,22 @@ async function executeHeartbeat(trigger: "auto" | "forced"): Promise<void> {
|
|
|
199
263
|
status: "idle",
|
|
200
264
|
run_count: isTimeout ? previousRunCount + 1 : previousRunCount,
|
|
201
265
|
});
|
|
266
|
+
// Timeouts advance last_run (budget consumed), so the cadence itself
|
|
267
|
+
// spaces the next attempt. Every other failure retries against the same
|
|
268
|
+
// last_run — back off so the due check doesn't hammer it every minute.
|
|
269
|
+
if (!isTimeout) {
|
|
270
|
+
hb.consecutiveFailures += 1;
|
|
271
|
+
hb.backoffUntil = failureBackoffUntil(
|
|
272
|
+
err,
|
|
273
|
+
hb.consecutiveFailures,
|
|
274
|
+
Date.now(),
|
|
275
|
+
);
|
|
276
|
+
logWarn(
|
|
277
|
+
"heartbeat",
|
|
278
|
+
`Backing off until ${new Date(hb.backoffUntil).toISOString()} ` +
|
|
279
|
+
`after ${hb.consecutiveFailures} consecutive failure(s)`,
|
|
280
|
+
);
|
|
281
|
+
}
|
|
202
282
|
if (trigger === "forced") throw err;
|
|
203
283
|
} finally {
|
|
204
284
|
hb.running = false;
|
|
@@ -71,6 +71,10 @@ export const hb: {
|
|
|
71
71
|
intervalMinutesRef: number;
|
|
72
72
|
config: HeartbeatConfig | null;
|
|
73
73
|
logFileSequence: number;
|
|
74
|
+
/** Consecutive failed runs — drives the failure backoff curve. */
|
|
75
|
+
consecutiveFailures: number;
|
|
76
|
+
/** Epoch ms before which auto runs are suppressed after a failure. */
|
|
77
|
+
backoffUntil: number;
|
|
74
78
|
} = {
|
|
75
79
|
running: false,
|
|
76
80
|
currentRunPromise: null,
|
|
@@ -79,6 +83,8 @@ export const hb: {
|
|
|
79
83
|
intervalMinutesRef: 60,
|
|
80
84
|
config: null,
|
|
81
85
|
logFileSequence: 0,
|
|
86
|
+
consecutiveFailures: 0,
|
|
87
|
+
backoffUntil: 0,
|
|
82
88
|
};
|
|
83
89
|
|
|
84
90
|
// ── State-file I/O ───────────────────────────────────────────────────────────
|
|
@@ -73,7 +73,9 @@ export function isAutoInjectTrusted(
|
|
|
73
73
|
/**
|
|
74
74
|
* Enforce the #373 trust policy on a retrieval result: drop every item that
|
|
75
75
|
* is not explicitly auto-inject trusted. Adapters should call this as their
|
|
76
|
-
* last step
|
|
76
|
+
* last step, and the Weaver's `prefetchMemory` applies it again to whatever
|
|
77
|
+
* a retriever returns — a buggy adapter cannot leak low-trust items into
|
|
78
|
+
* the prompt.
|
|
77
79
|
*/
|
|
78
80
|
export function filterAutoInjectable(
|
|
79
81
|
memory: RetrievedMemory | undefined,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Built-in plugin loading (GitHub / MemPalace / Playwright) + the hot-reload
|
|
2
|
+
* Built-in plugin loading (GitHub / MemPalace / mem0 / Playwright) + the hot-reload
|
|
3
3
|
* path that re-reads config, tears down, and re-loads everything.
|
|
4
4
|
*/
|
|
5
5
|
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from "./loader.js";
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* Load built-in plugins (GitHub, MemPalace, Playwright) based on config flags.
|
|
16
|
+
* Load built-in plugins (GitHub, MemPalace, mem0, Playwright) based on config flags.
|
|
17
17
|
* Shared by both bootstrap and hot-reload to avoid duplication.
|
|
18
18
|
*/
|
|
19
19
|
export async function loadBuiltinPlugins(config: TalonConfig): Promise<void> {
|
|
@@ -75,6 +75,34 @@ export async function loadBuiltinPlugins(config: TalonConfig): Promise<void> {
|
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
const mem0 = config.mem0;
|
|
79
|
+
if (mem0?.enabled) {
|
|
80
|
+
try {
|
|
81
|
+
const { createMem0Plugin } = await import("../../plugins/mem0/index.js");
|
|
82
|
+
const m0 = createMem0Plugin({
|
|
83
|
+
apiKey: mem0.apiKey,
|
|
84
|
+
host: mem0.host,
|
|
85
|
+
userId: mem0.userId,
|
|
86
|
+
});
|
|
87
|
+
const m0Config = mem0 as unknown as Record<string, unknown>;
|
|
88
|
+
const loaded = registerPlugin(m0, m0Config);
|
|
89
|
+
if (loaded) {
|
|
90
|
+
await initPluginWithTimeout(
|
|
91
|
+
loaded.plugin,
|
|
92
|
+
loaded.config,
|
|
93
|
+
15_000,
|
|
94
|
+
"mem0 init",
|
|
95
|
+
"mem0 init",
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
} catch (err) {
|
|
99
|
+
logError(
|
|
100
|
+
"plugin",
|
|
101
|
+
`mem0 init: ${err instanceof Error ? err.message : err}`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
78
106
|
const playwright = config.playwright;
|
|
79
107
|
if (playwright?.enabled) {
|
|
80
108
|
try {
|
|
@@ -16,22 +16,23 @@ import asset2 from "../../../prompts/discord.md" with { type: "file" };
|
|
|
16
16
|
import asset3 from "../../../prompts/dream.md" with { type: "file" };
|
|
17
17
|
import asset4 from "../../../prompts/heartbeat.md" with { type: "file" };
|
|
18
18
|
import asset5 from "../../../prompts/identity.md" with { type: "file" };
|
|
19
|
-
import asset6 from "../../../prompts/
|
|
20
|
-
import asset7 from "../../../prompts/
|
|
21
|
-
import asset8 from "../../../prompts/
|
|
22
|
-
import asset9 from "../../../prompts/system/contract-text-
|
|
23
|
-
import asset10 from "../../../prompts/system/contract-
|
|
24
|
-
import asset11 from "../../../prompts/system/
|
|
25
|
-
import asset12 from "../../../prompts/system/
|
|
26
|
-
import asset13 from "../../../prompts/system/
|
|
27
|
-
import asset14 from "../../../prompts/system/
|
|
28
|
-
import asset15 from "../../../prompts/system/
|
|
29
|
-
import asset16 from "../../../prompts/system/
|
|
30
|
-
import asset17 from "../../../prompts/system/
|
|
31
|
-
import asset18 from "../../../prompts/system/
|
|
32
|
-
import asset19 from "../../../prompts/
|
|
33
|
-
import asset20 from "../../../prompts/
|
|
34
|
-
import asset21 from "../../../prompts/
|
|
19
|
+
import asset6 from "../../../prompts/mem0.md" with { type: "file" };
|
|
20
|
+
import asset7 from "../../../prompts/mempalace.md" with { type: "file" };
|
|
21
|
+
import asset8 from "../../../prompts/native.md" with { type: "file" };
|
|
22
|
+
import asset9 from "../../../prompts/system/contract-text-or-tools.md" with { type: "file" };
|
|
23
|
+
import asset10 from "../../../prompts/system/contract-text-preferred.md" with { type: "file" };
|
|
24
|
+
import asset11 from "../../../prompts/system/contract-tool-only.md" with { type: "file" };
|
|
25
|
+
import asset12 from "../../../prompts/system/cron.md" with { type: "file" };
|
|
26
|
+
import asset13 from "../../../prompts/system/daily-memory.md" with { type: "file" };
|
|
27
|
+
import asset14 from "../../../prompts/system/goals.md" with { type: "file" };
|
|
28
|
+
import asset15 from "../../../prompts/system/heartbeat-agent.md" with { type: "file" };
|
|
29
|
+
import asset16 from "../../../prompts/system/persistent-memory.md" with { type: "file" };
|
|
30
|
+
import asset17 from "../../../prompts/system/skills.md" with { type: "file" };
|
|
31
|
+
import asset18 from "../../../prompts/system/triggers.md" with { type: "file" };
|
|
32
|
+
import asset19 from "../../../prompts/system/workspace.md" with { type: "file" };
|
|
33
|
+
import asset20 from "../../../prompts/teams.md" with { type: "file" };
|
|
34
|
+
import asset21 from "../../../prompts/telegram.md" with { type: "file" };
|
|
35
|
+
import asset22 from "../../../prompts/terminal.md" with { type: "file" };
|
|
35
36
|
|
|
36
37
|
/** rel path (posix, under prompts/) → embedded file path (/$bunfs/… when compiled). */
|
|
37
38
|
const ASSETS: Record<string, string> = {
|
|
@@ -41,22 +42,23 @@ const ASSETS: Record<string, string> = {
|
|
|
41
42
|
"dream.md": asset3,
|
|
42
43
|
"heartbeat.md": asset4,
|
|
43
44
|
"identity.md": asset5,
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"system/contract-text-
|
|
48
|
-
"system/contract-
|
|
49
|
-
"system/
|
|
50
|
-
"system/
|
|
51
|
-
"system/
|
|
52
|
-
"system/
|
|
53
|
-
"system/
|
|
54
|
-
"system/
|
|
55
|
-
"system/
|
|
56
|
-
"system/
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
45
|
+
"mem0.md": asset6,
|
|
46
|
+
"mempalace.md": asset7,
|
|
47
|
+
"native.md": asset8,
|
|
48
|
+
"system/contract-text-or-tools.md": asset9,
|
|
49
|
+
"system/contract-text-preferred.md": asset10,
|
|
50
|
+
"system/contract-tool-only.md": asset11,
|
|
51
|
+
"system/cron.md": asset12,
|
|
52
|
+
"system/daily-memory.md": asset13,
|
|
53
|
+
"system/goals.md": asset14,
|
|
54
|
+
"system/heartbeat-agent.md": asset15,
|
|
55
|
+
"system/persistent-memory.md": asset16,
|
|
56
|
+
"system/skills.md": asset17,
|
|
57
|
+
"system/triggers.md": asset18,
|
|
58
|
+
"system/workspace.md": asset19,
|
|
59
|
+
"teams.md": asset20,
|
|
60
|
+
"telegram.md": asset21,
|
|
61
|
+
"terminal.md": asset22,
|
|
60
62
|
};
|
|
61
63
|
|
|
62
64
|
/** Read an embedded prompt by its rel path (e.g. "system/cron.md"). */
|
|
@@ -3,10 +3,16 @@
|
|
|
3
3
|
* for live user messages, strictly fail-closed: a broken palace must
|
|
4
4
|
* never block chat delivery. The result is dynamic turn context passed
|
|
5
5
|
* through ChatRunParams; it never touches the frozen prompt.
|
|
6
|
+
*
|
|
7
|
+
* The #373 trust policy is enforced HERE, not just in adapters: whatever a
|
|
8
|
+
* retriever returns is passed through `filterAutoInjectable` before it can
|
|
9
|
+
* reach the prompt, so a buggy or future adapter cannot leak `user_claim` /
|
|
10
|
+
* `group_chat` items into auto-injection.
|
|
6
11
|
*/
|
|
7
12
|
|
|
8
13
|
import type { RetrievedMemory } from "../agent-runtime/capabilities.js";
|
|
9
14
|
import type { MemoryRetriever } from "../memory/retrieval.js";
|
|
15
|
+
import { filterAutoInjectable } from "../memory/retrieval.js";
|
|
10
16
|
import { logDebug, logWarn } from "../../util/log.js";
|
|
11
17
|
|
|
12
18
|
export type PrefetchMemoryInput = {
|
|
@@ -23,13 +29,23 @@ export async function prefetchMemory(
|
|
|
23
29
|
input: PrefetchMemoryInput,
|
|
24
30
|
): Promise<RetrievedMemory | undefined> {
|
|
25
31
|
try {
|
|
26
|
-
const
|
|
32
|
+
const raw = await retrieve({
|
|
27
33
|
runKind: "chat",
|
|
28
34
|
chatId: input.chatId,
|
|
29
35
|
text: input.text,
|
|
30
36
|
senderName: input.senderName,
|
|
31
37
|
isGroup: input.isGroup,
|
|
32
38
|
});
|
|
39
|
+
const retrieved = filterAutoInjectable(raw);
|
|
40
|
+
const droppedCount =
|
|
41
|
+
(raw?.items.length ?? 0) - (retrieved?.items.length ?? 0);
|
|
42
|
+
if (droppedCount > 0) {
|
|
43
|
+
logWarn(
|
|
44
|
+
"dispatcher",
|
|
45
|
+
`[${input.reqId}] memory pre-retrieval: dropped ${droppedCount} ` +
|
|
46
|
+
`low-trust item(s) the retriever failed to filter (#373)`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
33
49
|
if (retrieved) {
|
|
34
50
|
logDebug(
|
|
35
51
|
"dispatcher",
|
|
@@ -208,9 +208,12 @@ export const messagingHandlers: DiscordActionHandlers = {
|
|
|
208
208
|
|
|
209
209
|
schedule_message: (body, chatId, { client, scheduledMessages }) => {
|
|
210
210
|
const text = String(body.text ?? "");
|
|
211
|
+
// NaN (e.g. delay_seconds: "5m") must fall back to the default, not
|
|
212
|
+
// propagate: setTimeout(fn, NaN) fires immediately.
|
|
213
|
+
const requested = Number(body.delay_seconds ?? 60);
|
|
211
214
|
const delaySec = Math.max(
|
|
212
215
|
1,
|
|
213
|
-
Math.min(MAX_DELAY_SEC, Number(
|
|
216
|
+
Math.min(MAX_DELAY_SEC, Number.isFinite(requested) ? requested : 60),
|
|
214
217
|
);
|
|
215
218
|
const entry: ScheduledMessage = {
|
|
216
219
|
id: `sched_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
@@ -13,6 +13,7 @@ import type { TalonConfig } from "../../util/config.js";
|
|
|
13
13
|
import type { Gateway } from "../../core/engine/gateway.js";
|
|
14
14
|
import { resetSession, getAllSessions } from "../../storage/sessions.js";
|
|
15
15
|
import { clearHistory } from "../../storage/history.js";
|
|
16
|
+
import { todayLogDate } from "../../storage/daily-log.js";
|
|
16
17
|
import { getChatSettings } from "../../storage/chat-settings.js";
|
|
17
18
|
import {
|
|
18
19
|
getAllCronJobs,
|
|
@@ -165,7 +166,7 @@ export async function handleAdminSubcommand(
|
|
|
165
166
|
}
|
|
166
167
|
|
|
167
168
|
case "daily": {
|
|
168
|
-
const today =
|
|
169
|
+
const today = todayLogDate();
|
|
169
170
|
const logPath = `${dirs.logs}/${today}.md`;
|
|
170
171
|
try {
|
|
171
172
|
const content = readFileSync(logPath, "utf-8");
|
|
@@ -243,9 +243,12 @@ export const messagingHandlers: TelegramActionHandlers = {
|
|
|
243
243
|
const text = String(body.text ?? "");
|
|
244
244
|
const replyTo = toPositiveId(body.reply_to_message_id);
|
|
245
245
|
const rows = body.rows as ScheduledMessage["rows"];
|
|
246
|
+
// NaN (e.g. delay_seconds: "5m") must fall back to the default, not
|
|
247
|
+
// propagate: setTimeout(fn, NaN) fires immediately.
|
|
248
|
+
const requested = Number(body.delay_seconds ?? 60);
|
|
246
249
|
const delaySec = Math.max(
|
|
247
250
|
1,
|
|
248
|
-
Math.min(MAX_DELAY_SEC, Number(
|
|
251
|
+
Math.min(MAX_DELAY_SEC, Number.isFinite(requested) ? requested : 60),
|
|
249
252
|
);
|
|
250
253
|
const entry: ScheduledMessage = {
|
|
251
254
|
id: `sched_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
@@ -10,6 +10,7 @@ import { tailFile } from "../../util/tail-file.js";
|
|
|
10
10
|
import { escapeHtml } from "./formatting.js";
|
|
11
11
|
import { resetSession, getAllSessions } from "../../storage/sessions.js";
|
|
12
12
|
import { clearHistory } from "../../storage/history.js";
|
|
13
|
+
import { todayLogDate } from "../../storage/daily-log.js";
|
|
13
14
|
import { getChatSettings } from "../../storage/chat-settings.js";
|
|
14
15
|
import {
|
|
15
16
|
getAllCronJobs,
|
|
@@ -221,7 +222,7 @@ export async function handleAdminCommand(
|
|
|
221
222
|
}
|
|
222
223
|
|
|
223
224
|
case "daily": {
|
|
224
|
-
const today =
|
|
225
|
+
const today = todayLogDate();
|
|
225
226
|
const logPath = `${dirs.logs}/${today}.md`;
|
|
226
227
|
try {
|
|
227
228
|
const content = readFileSync(logPath, "utf-8");
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mem0 plugin — long-term memory via the mem0 platform (https://mem0.ai).
|
|
3
|
+
*
|
|
4
|
+
* Registers a stdio MCP server (server.ts) bridging the `mem0ai` SDK,
|
|
5
|
+
* giving the agent semantic memory add/search/list/get/delete. Works
|
|
6
|
+
* against the hosted platform (API key) or a self-hosted mem0 server
|
|
7
|
+
* (host URL).
|
|
8
|
+
*
|
|
9
|
+
* Preferred configuration in ~/.talon/config.json:
|
|
10
|
+
* "memory": {
|
|
11
|
+
* "enabled": true,
|
|
12
|
+
* "backend": "mem0",
|
|
13
|
+
* "mem0": {
|
|
14
|
+
* "apiKey": "m0-...", // default: MEM0_API_KEY env var
|
|
15
|
+
* "host": "http://localhost:8888", // optional self-hosted server
|
|
16
|
+
* "userId": "talon" // optional entity id (default "talon")
|
|
17
|
+
* }
|
|
18
|
+
* }
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { readFileSync } from "node:fs";
|
|
22
|
+
import { resolve } from "node:path";
|
|
23
|
+
import { fileURLToPath } from "node:url";
|
|
24
|
+
import type { TalonPlugin } from "../../core/plugin/index.js";
|
|
25
|
+
import { log, logWarn } from "../../util/log.js";
|
|
26
|
+
import { dirs } from "../../util/paths.js";
|
|
27
|
+
|
|
28
|
+
/** Load from ~/.talon/prompts/ (user-customisable, seeded on first run) */
|
|
29
|
+
const PROMPT_PATH = resolve(dirs.prompts, "mem0.md");
|
|
30
|
+
|
|
31
|
+
const SERVER_PATH = fileURLToPath(new URL("./server.ts", import.meta.url));
|
|
32
|
+
|
|
33
|
+
export function createMem0Plugin(config: {
|
|
34
|
+
/** Platform API key. Falls back to the MEM0_API_KEY env var. */
|
|
35
|
+
apiKey?: string;
|
|
36
|
+
/** Self-hosted mem0 server URL; when set, apiKey may be omitted. */
|
|
37
|
+
host?: string;
|
|
38
|
+
/** Entity id memories are filed under (default "talon"). */
|
|
39
|
+
userId?: string;
|
|
40
|
+
}): TalonPlugin {
|
|
41
|
+
const apiKey = config.apiKey?.trim() || process.env.MEM0_API_KEY?.trim();
|
|
42
|
+
const host = config.host?.trim();
|
|
43
|
+
const userId = config.userId?.trim() || "talon";
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
name: "mem0",
|
|
47
|
+
description: "mem0 — long-term memory layer (hosted or self-hosted)",
|
|
48
|
+
version: "1.0.0",
|
|
49
|
+
|
|
50
|
+
mcpServerPath: SERVER_PATH,
|
|
51
|
+
|
|
52
|
+
validateConfig() {
|
|
53
|
+
if (!apiKey && !host) {
|
|
54
|
+
return [
|
|
55
|
+
'mem0 requires an API key ("memory.mem0.apiKey" or the MEM0_API_KEY env var) or a self-hosted "memory.mem0.host" URL.',
|
|
56
|
+
];
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
init() {
|
|
62
|
+
log(
|
|
63
|
+
"mem0",
|
|
64
|
+
`Ready (${host ? `host: ${host}` : "hosted platform"}, user: ${userId})`,
|
|
65
|
+
);
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
getEnvVars() {
|
|
69
|
+
const env: Record<string, string> = { MEM0_USER_ID: userId };
|
|
70
|
+
if (apiKey) env.MEM0_API_KEY = apiKey;
|
|
71
|
+
if (host) env.MEM0_HOST = host;
|
|
72
|
+
return env;
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
getSystemPromptAddition() {
|
|
76
|
+
try {
|
|
77
|
+
const template = readFileSync(PROMPT_PATH, "utf-8");
|
|
78
|
+
return template.replace(/\{\{userId\}\}/g, userId);
|
|
79
|
+
} catch (err) {
|
|
80
|
+
logWarn(
|
|
81
|
+
"mem0",
|
|
82
|
+
`Failed to load prompt from ${PROMPT_PATH}: ${err instanceof Error ? err.message : err}`,
|
|
83
|
+
);
|
|
84
|
+
return `## mem0 — Long-term Memory\n\nMemory entity id: \`${userId}\``;
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mem0 MCP server — stdio subprocess spawned by the plugin loader.
|
|
3
|
+
*
|
|
4
|
+
* Thin bridge over the `mem0ai` MemoryClient (hosted platform or a
|
|
5
|
+
* self-hosted server via MEM0_HOST). All memories are scoped to one
|
|
6
|
+
* entity (MEM0_USER_ID) so the palace of a single Talon deployment
|
|
7
|
+
* stays isolated inside a shared mem0 project.
|
|
8
|
+
*
|
|
9
|
+
* Env contract (set by src/plugins/mem0/index.ts getEnvVars):
|
|
10
|
+
* MEM0_API_KEY — platform API key (required unless MEM0_HOST is set)
|
|
11
|
+
* MEM0_HOST — optional self-hosted API base URL
|
|
12
|
+
* MEM0_USER_ID — entity id memories are filed under (default "talon")
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
16
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
17
|
+
import { z } from "zod";
|
|
18
|
+
import MemoryClient from "mem0ai";
|
|
19
|
+
|
|
20
|
+
const apiKey = process.env.MEM0_API_KEY ?? "";
|
|
21
|
+
const host = process.env.MEM0_HOST;
|
|
22
|
+
const userId = process.env.MEM0_USER_ID || "talon";
|
|
23
|
+
|
|
24
|
+
const client = new MemoryClient({ apiKey, ...(host ? { host } : {}) });
|
|
25
|
+
|
|
26
|
+
/** v3 search/list scope every call to this deployment's entity. */
|
|
27
|
+
const entityFilter = { AND: [{ user_id: userId }] };
|
|
28
|
+
|
|
29
|
+
type ToolResult = {
|
|
30
|
+
content: Array<{ type: "text"; text: string }>;
|
|
31
|
+
isError?: boolean;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function textResult(value: unknown): ToolResult {
|
|
35
|
+
return {
|
|
36
|
+
content: [
|
|
37
|
+
{
|
|
38
|
+
type: "text",
|
|
39
|
+
text: typeof value === "string" ? value : JSON.stringify(value),
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function errorResult(err: unknown): ToolResult {
|
|
46
|
+
return {
|
|
47
|
+
content: [
|
|
48
|
+
{
|
|
49
|
+
type: "text",
|
|
50
|
+
text: `mem0 error: ${err instanceof Error ? err.message : String(err)}`,
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
isError: true,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const server = new McpServer({ name: "mem0-tools", version: "1.0.0" });
|
|
58
|
+
|
|
59
|
+
server.tool(
|
|
60
|
+
"mem0_add_memory",
|
|
61
|
+
"Store new information in mem0 long-term memory. Pass conversational text; mem0 extracts and files the durable facts itself.",
|
|
62
|
+
{
|
|
63
|
+
text: z.string().min(1).describe("The information to remember"),
|
|
64
|
+
role: z
|
|
65
|
+
.enum(["user", "assistant"])
|
|
66
|
+
.optional()
|
|
67
|
+
.describe("Who the information came from (default user)"),
|
|
68
|
+
metadata: z
|
|
69
|
+
.record(z.string(), z.string())
|
|
70
|
+
.optional()
|
|
71
|
+
.describe("Optional key/value tags stored with the memory"),
|
|
72
|
+
},
|
|
73
|
+
async ({ text, role, metadata }) => {
|
|
74
|
+
try {
|
|
75
|
+
const memories = await client.add(
|
|
76
|
+
[{ role: role ?? "user", content: text }],
|
|
77
|
+
{
|
|
78
|
+
userId,
|
|
79
|
+
...(metadata ? { metadata } : {}),
|
|
80
|
+
},
|
|
81
|
+
);
|
|
82
|
+
return textResult({ stored: memories.length, memories });
|
|
83
|
+
} catch (err) {
|
|
84
|
+
return errorResult(err);
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
server.tool(
|
|
90
|
+
"mem0_search_memory",
|
|
91
|
+
"Semantic search over mem0 memories. Use short keyword queries, not full sentences.",
|
|
92
|
+
{
|
|
93
|
+
query: z.string().min(1).describe("Search query"),
|
|
94
|
+
limit: z
|
|
95
|
+
.number()
|
|
96
|
+
.int()
|
|
97
|
+
.min(1)
|
|
98
|
+
.max(50)
|
|
99
|
+
.optional()
|
|
100
|
+
.describe("Max results (default 10)"),
|
|
101
|
+
threshold: z
|
|
102
|
+
.number()
|
|
103
|
+
.min(0)
|
|
104
|
+
.max(1)
|
|
105
|
+
.optional()
|
|
106
|
+
.describe("Minimum relevance score, 0-1"),
|
|
107
|
+
},
|
|
108
|
+
async ({ query, limit, threshold }) => {
|
|
109
|
+
try {
|
|
110
|
+
const { results } = await client.search(query, {
|
|
111
|
+
filters: entityFilter,
|
|
112
|
+
topK: limit ?? 10,
|
|
113
|
+
...(threshold !== undefined ? { threshold } : {}),
|
|
114
|
+
});
|
|
115
|
+
return textResult({ count: results.length, results });
|
|
116
|
+
} catch (err) {
|
|
117
|
+
return errorResult(err);
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
server.tool(
|
|
123
|
+
"mem0_list_memories",
|
|
124
|
+
"Browse stored mem0 memories with pagination. Use for inventory/cleanup, not search.",
|
|
125
|
+
{
|
|
126
|
+
page: z
|
|
127
|
+
.number()
|
|
128
|
+
.int()
|
|
129
|
+
.min(1)
|
|
130
|
+
.optional()
|
|
131
|
+
.describe("Page number (default 1)"),
|
|
132
|
+
page_size: z
|
|
133
|
+
.number()
|
|
134
|
+
.int()
|
|
135
|
+
.min(1)
|
|
136
|
+
.max(100)
|
|
137
|
+
.optional()
|
|
138
|
+
.describe("Results per page (default 25)"),
|
|
139
|
+
},
|
|
140
|
+
async ({ page, page_size }) => {
|
|
141
|
+
try {
|
|
142
|
+
const result = await client.getAll({
|
|
143
|
+
filters: entityFilter,
|
|
144
|
+
page: page ?? 1,
|
|
145
|
+
pageSize: page_size ?? 25,
|
|
146
|
+
});
|
|
147
|
+
return textResult(result);
|
|
148
|
+
} catch (err) {
|
|
149
|
+
return errorResult(err);
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
server.tool(
|
|
155
|
+
"mem0_get_memory",
|
|
156
|
+
"Fetch a single mem0 memory by id, with full content and metadata.",
|
|
157
|
+
{ memory_id: z.string().min(1).describe("Memory id from search/list") },
|
|
158
|
+
async ({ memory_id }) => {
|
|
159
|
+
try {
|
|
160
|
+
return textResult(await client.get(memory_id));
|
|
161
|
+
} catch (err) {
|
|
162
|
+
return errorResult(err);
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
server.tool(
|
|
168
|
+
"mem0_delete_memory",
|
|
169
|
+
"Delete a mem0 memory by id when it is wrong or stale.",
|
|
170
|
+
{ memory_id: z.string().min(1).describe("Memory id from search/list") },
|
|
171
|
+
async ({ memory_id }) => {
|
|
172
|
+
try {
|
|
173
|
+
return textResult(await client.delete(memory_id));
|
|
174
|
+
} catch (err) {
|
|
175
|
+
return errorResult(err);
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
await server.connect(new StdioServerTransport());
|
package/src/storage/daily-log.ts
CHANGED
|
@@ -104,6 +104,14 @@ export function getLogsDir(): string {
|
|
|
104
104
|
return LOGS_DIR;
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Today's log-file date key. Readers must use this (not the UTC date from
|
|
109
|
+
* `toISOString`) or they look for the wrong file whenever local date ≠ UTC.
|
|
110
|
+
*/
|
|
111
|
+
export function todayLogDate(): string {
|
|
112
|
+
return localDateKey(new Date());
|
|
113
|
+
}
|
|
114
|
+
|
|
107
115
|
/** Matches YYYY-MM-DD.md filenames strictly. */
|
|
108
116
|
const DAILY_FILE_RE = /^\d{4}-\d{2}-\d{2}\.md$/;
|
|
109
117
|
|
package/src/util/config.ts
CHANGED
|
@@ -195,6 +195,32 @@ const playwrightConfigSchema = z.object({
|
|
|
195
195
|
endpointFile: z.string().optional(),
|
|
196
196
|
});
|
|
197
197
|
|
|
198
|
+
/** MemPalace backend settings, shared by `memory.mempalace` and the legacy top-level `mempalace` section. */
|
|
199
|
+
const mempalaceSettingsSchema = z.object({
|
|
200
|
+
/** Palace directory path (default: ~/.talon/workspace/palace/) */
|
|
201
|
+
palacePath: z.string().min(1).optional(),
|
|
202
|
+
/** Python binary path (default: ~/.talon/mempalace-venv/bin/python) */
|
|
203
|
+
pythonPath: z.string().min(1).optional(),
|
|
204
|
+
/**
|
|
205
|
+
* BCP 47 language codes for entity detection (mempalace >= 3.3).
|
|
206
|
+
* Supported: en, es, fr, de, ja, ko, zh-CN, zh-TW, pt-br, ru, it, hi, id.
|
|
207
|
+
* Sets MEMPALACE_ENTITY_LANGUAGES for the MCP server.
|
|
208
|
+
*/
|
|
209
|
+
entityLanguages: z.array(z.string().min(2)).nonempty().optional(),
|
|
210
|
+
/** Enable mempalace diagnostic diaries (sets MEMPAL_VERBOSE=1). */
|
|
211
|
+
verbose: z.boolean().optional(),
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
/** mem0 backend settings, shared by `memory.mem0` and the top-level `mem0` section. */
|
|
215
|
+
const mem0SettingsSchema = z.object({
|
|
216
|
+
/** Platform API key (default: MEM0_API_KEY env var). */
|
|
217
|
+
apiKey: z.string().min(1).optional(),
|
|
218
|
+
/** Self-hosted mem0 server URL; when set, apiKey may be omitted. */
|
|
219
|
+
host: z.string().min(1).optional(),
|
|
220
|
+
/** Entity id memories are filed under (default "talon"). */
|
|
221
|
+
userId: z.string().min(1).optional(),
|
|
222
|
+
});
|
|
223
|
+
|
|
198
224
|
/**
|
|
199
225
|
* Memory pre-retrieval (Phase B) — automatic per-turn palace retrieval.
|
|
200
226
|
* Ships INERT: the flag gates wiring a real retriever into the dispatcher.
|
|
@@ -383,25 +409,30 @@ const configSchema = z.object({
|
|
|
383
409
|
})
|
|
384
410
|
.optional(),
|
|
385
411
|
|
|
386
|
-
//
|
|
387
|
-
mempalace
|
|
412
|
+
// Long-term memory — unified backend selection. Preferred over the
|
|
413
|
+
// legacy top-level "mempalace"/"mem0" sections; when enabled it wins
|
|
414
|
+
// over them (loadConfig mirrors it onto the section the loaders read).
|
|
415
|
+
memory: z
|
|
388
416
|
.object({
|
|
389
417
|
enabled: z.boolean().default(false),
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
pythonPath: z.string().min(1).optional(),
|
|
394
|
-
/**
|
|
395
|
-
* BCP 47 language codes for entity detection (mempalace >= 3.3).
|
|
396
|
-
* Supported: en, es, fr, de, ja, ko, zh-CN, zh-TW, pt-br, ru, it, hi, id.
|
|
397
|
-
* Sets MEMPALACE_ENTITY_LANGUAGES for the MCP server.
|
|
398
|
-
*/
|
|
399
|
-
entityLanguages: z.array(z.string().min(2)).nonempty().optional(),
|
|
400
|
-
/** Enable mempalace diagnostic diaries (sets MEMPAL_VERBOSE=1). */
|
|
401
|
-
verbose: z.boolean().optional(),
|
|
418
|
+
backend: z.enum(["mempalace", "mem0"]).default("mempalace"),
|
|
419
|
+
mempalace: mempalaceSettingsSchema.optional(),
|
|
420
|
+
mem0: mem0SettingsSchema.optional(),
|
|
402
421
|
})
|
|
403
422
|
.optional(),
|
|
404
423
|
|
|
424
|
+
// MemPalace — structured long-term memory with vector search
|
|
425
|
+
// (legacy section; prefer "memory": { "backend": "mempalace", ... })
|
|
426
|
+
mempalace: mempalaceSettingsSchema
|
|
427
|
+
.extend({ enabled: z.boolean().default(false) })
|
|
428
|
+
.optional(),
|
|
429
|
+
|
|
430
|
+
// mem0 — long-term memory layer, hosted platform or self-hosted
|
|
431
|
+
// (legacy-style section; prefer "memory": { "backend": "mem0", ... })
|
|
432
|
+
mem0: mem0SettingsSchema
|
|
433
|
+
.extend({ enabled: z.boolean().default(false) })
|
|
434
|
+
.optional(),
|
|
435
|
+
|
|
405
436
|
// Playwright — headless browser automation via MCP
|
|
406
437
|
playwright: playwrightConfigSchema.optional(),
|
|
407
438
|
|
|
@@ -577,6 +608,25 @@ export function loadConfig(): TalonConfig {
|
|
|
577
608
|
|
|
578
609
|
const parsed = configSchema.parse(fileConfig);
|
|
579
610
|
|
|
611
|
+
// Unified memory section: mirror the selected backend onto the per-plugin
|
|
612
|
+
// section the loaders consume (builtins.ts reads config.mempalace /
|
|
613
|
+
// config.mem0). The memory section wins over a legacy section when both
|
|
614
|
+
// are present; the unselected backend is disabled so exactly one memory
|
|
615
|
+
// plugin loads.
|
|
616
|
+
if (parsed.memory?.enabled) {
|
|
617
|
+
if (parsed.memory.backend === "mempalace") {
|
|
618
|
+
parsed.mempalace = {
|
|
619
|
+
...parsed.mempalace,
|
|
620
|
+
...parsed.memory.mempalace,
|
|
621
|
+
enabled: true,
|
|
622
|
+
};
|
|
623
|
+
if (parsed.mem0) parsed.mem0.enabled = false;
|
|
624
|
+
} else {
|
|
625
|
+
parsed.mem0 = { ...parsed.mem0, ...parsed.memory.mem0, enabled: true };
|
|
626
|
+
if (parsed.mempalace) parsed.mempalace.enabled = false;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
580
630
|
// Apply timezone globally before building the system prompt
|
|
581
631
|
setTimezone(parsed.timezone);
|
|
582
632
|
|