talon-agent 1.9.0 → 1.9.2
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 +4 -3
- package/prompts/mempalace.md +24 -4
- package/src/__tests__/disallowed-tools.test.ts +64 -0
- package/src/__tests__/handlers-stream.test.ts +0 -5
- package/src/__tests__/handlers.test.ts +6 -12
- package/src/__tests__/log.test.ts +4 -3
- package/src/__tests__/tool-functional.test.ts +615 -0
- package/src/__tests__/tool-id-coercion.test.ts +136 -0
- package/src/backend/claude-sdk/handler.ts +2 -3
- package/src/core/constants.ts +5 -0
- package/src/core/gateway.ts +12 -2
- package/src/core/tools/history.ts +6 -5
- package/src/core/tools/members.ts +2 -1
- package/src/core/tools/messaging.ts +9 -8
- package/src/core/tools/schemas.ts +34 -0
- package/src/core/tools/stickers.ts +3 -2
- package/src/frontend/telegram/commands.ts +2 -3
- package/src/frontend/telegram/handlers.ts +4 -12
- package/src/util/log.ts +4 -1
- package/src/__tests__/prompt-builder-extended.test.ts +0 -296
- package/src/__tests__/prompt-builder.test.ts +0 -106
- package/src/core/prompt-builder.ts +0 -40
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "talon-agent",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.2",
|
|
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",
|
|
@@ -51,13 +51,14 @@
|
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"@anthropic-ai/claude-agent-sdk": "^0.2.108",
|
|
54
|
+
"@anthropic-ai/sdk": "^0.95.0",
|
|
54
55
|
"@brave/brave-search-mcp-server": "^2.0.75",
|
|
55
56
|
"@clack/prompts": "^1.2.0",
|
|
56
57
|
"@grammyjs/auto-retry": "^2.0.2",
|
|
57
58
|
"@grammyjs/transformer-throttler": "^1.2.1",
|
|
58
59
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
59
60
|
"@opencode-ai/sdk": "^1.4.0",
|
|
60
|
-
"@playwright/mcp": "^0.0.
|
|
61
|
+
"@playwright/mcp": "^0.0.74",
|
|
61
62
|
"big-integer": "^1.6.52",
|
|
62
63
|
"cheerio": "^1.2.0",
|
|
63
64
|
"croner": "^10.0.1",
|
|
@@ -85,6 +86,6 @@
|
|
|
85
86
|
"vitest": "^4.1.3"
|
|
86
87
|
},
|
|
87
88
|
"overrides": {
|
|
88
|
-
"@anthropic-ai/sdk": "^0.
|
|
89
|
+
"@anthropic-ai/sdk": "^0.95.0"
|
|
89
90
|
}
|
|
90
91
|
}
|
package/prompts/mempalace.md
CHANGED
|
@@ -7,6 +7,7 @@ You have access to a local memory palace via MCP tools. The palace stores verbat
|
|
|
7
7
|
- **Wings** = top-level categories (people, projects, topics)
|
|
8
8
|
- **Rooms** = specific subjects within a wing
|
|
9
9
|
- **Drawers** = individual memory chunks (verbatim text)
|
|
10
|
+
- **Tunnels** = cross-wing links between related rooms (auto-created in mempalace 3.3.4+ when topics overlap, plus manual)
|
|
10
11
|
- **Knowledge Graph** = entity-relationship facts with temporal validity
|
|
11
12
|
|
|
12
13
|
### Protocol — FOLLOW EVERY SESSION
|
|
@@ -25,6 +26,7 @@ You have access to a local memory palace via MCP tools. The palace stores verbat
|
|
|
25
26
|
- `mempalace_status` — Palace overview: total drawers, wings, rooms.
|
|
26
27
|
- `mempalace_list_wings` / `mempalace_list_rooms` — Browse structure.
|
|
27
28
|
- `mempalace_get_taxonomy` — Full wing/room/count tree.
|
|
29
|
+
- `mempalace_get_aaak_spec` — Get the AAAK closet/compression spec. Only needed when reading/writing AAAK-compressed memories directly.
|
|
28
30
|
|
|
29
31
|
**Knowledge Graph (Temporal Facts):**
|
|
30
32
|
|
|
@@ -34,24 +36,42 @@ You have access to a local memory palace via MCP tools. The palace stores verbat
|
|
|
34
36
|
- `mempalace_kg_timeline` — Chronological story of an entity.
|
|
35
37
|
- `mempalace_kg_stats` — Graph overview: entities, triples, relationship types.
|
|
36
38
|
|
|
37
|
-
**Palace Graph
|
|
39
|
+
**Palace Graph & Cross-Wing Tunnels:**
|
|
38
40
|
|
|
39
41
|
- `mempalace_traverse` — Walk from a room, find connected ideas across wings.
|
|
40
42
|
- `mempalace_find_tunnels` — Find rooms that bridge two wings.
|
|
43
|
+
- `mempalace_follow_tunnels` — From a specific (wing, room) pair, walk the outbound tunnels and see connected rooms with drawer previews.
|
|
44
|
+
- `mempalace_create_tunnel` — Manually create a cross-wing tunnel between two (wing, room) pairs. Use when you spot a connection the auto-detector missed.
|
|
45
|
+
- `mempalace_list_tunnels` — List tunnels (optionally filtered by wing).
|
|
46
|
+
- `mempalace_delete_tunnel` — Remove a tunnel by ID when it's wrong or noisy.
|
|
41
47
|
- `mempalace_graph_stats` — Graph connectivity overview.
|
|
42
48
|
|
|
43
|
-
**
|
|
49
|
+
**Drawers:**
|
|
44
50
|
|
|
45
51
|
- `mempalace_add_drawer` — Store verbatim content into a wing/room. Auto-checks duplicates.
|
|
52
|
+
- `mempalace_get_drawer` — Fetch a single drawer by ID. Returns full content + metadata. Use after a search hit when you need the verbatim text.
|
|
53
|
+
- `mempalace_list_drawers` — Browse drawers in a wing/room with pagination (`limit`, `offset`). Use for inventory/cleanup, not search.
|
|
54
|
+
- `mempalace_update_drawer` — Edit an existing drawer's content, wing, or room in place. Use to refine misfiled or stale entries instead of delete + re-add.
|
|
46
55
|
- `mempalace_delete_drawer` — Remove a drawer by ID.
|
|
47
|
-
|
|
48
|
-
|
|
56
|
+
|
|
57
|
+
**Diary:**
|
|
58
|
+
|
|
59
|
+
- `mempalace_diary_write` — Write a session diary entry (`agent_name`, `entry`, `topic`, optional `wing`).
|
|
60
|
+
- `mempalace_diary_read` — Read recent diary entries. Optional `wing` filter scopes by project.
|
|
61
|
+
|
|
62
|
+
**Maintenance:**
|
|
63
|
+
|
|
64
|
+
- `mempalace_memories_filed_away` — Check whether a recent checkpoint was saved (message count, timestamp). Useful for confirming a stop-hook flush happened.
|
|
65
|
+
- `mempalace_reconnect` — Force reconnect to the palace database. Run after external CLI/scripts modify the palace directly (the in-memory HNSW index can otherwise go stale).
|
|
66
|
+
- `mempalace_hook_settings` — Toggle silent-save / desktop-toast for the auto-save hooks.
|
|
49
67
|
|
|
50
68
|
### Tips
|
|
51
69
|
|
|
52
70
|
- Search is **semantic** (meaning-based), not keyword. "What did we discuss about database performance?" works better than "database".
|
|
53
71
|
- The knowledge graph stores typed relationships with **time windows**. It knows WHEN things were true.
|
|
54
72
|
- Use `mempalace_check_duplicate` before storing new content to avoid clutter.
|
|
73
|
+
- **Tunnels auto-form** when drawers across different wings share topics (mempalace 3.3.4+). You don't have to wire connections by hand most of the time — but `mempalace_create_tunnel` is there when the auto-detector misses something obvious, and `mempalace_delete_tunnel` is there when it overreaches.
|
|
74
|
+
- After updating facts via the CLI / external scripts, call `mempalace_reconnect` so the live MCP server picks up the changes.
|
|
55
75
|
- Diary entries accumulate across sessions. Write them to build continuity of self.
|
|
56
76
|
- Entity detection runs per-language; results include `created_at` timestamps you can surface when the user asks "when did I last…".
|
|
57
77
|
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
DISALLOWED_TOOLS_BACKGROUND,
|
|
5
|
+
DISALLOWED_TOOLS_CORE,
|
|
6
|
+
} from "../core/constants.js";
|
|
7
|
+
import { DISALLOWED_TOOLS_CHAT } from "../backend/claude-sdk/constants.js";
|
|
8
|
+
|
|
9
|
+
describe("disallowed tool lists", () => {
|
|
10
|
+
describe("DISALLOWED_TOOLS_CORE", () => {
|
|
11
|
+
it("blocks interactive/planning tools that are nonsensical in headless contexts", () => {
|
|
12
|
+
const expected = [
|
|
13
|
+
"EnterPlanMode",
|
|
14
|
+
"ExitPlanMode",
|
|
15
|
+
"EnterWorktree",
|
|
16
|
+
"ExitWorktree",
|
|
17
|
+
"TodoWrite",
|
|
18
|
+
"TodoRead",
|
|
19
|
+
"TaskCreate",
|
|
20
|
+
"TaskUpdate",
|
|
21
|
+
"TaskGet",
|
|
22
|
+
"TaskList",
|
|
23
|
+
"TaskOutput",
|
|
24
|
+
"TaskStop",
|
|
25
|
+
"AskUserQuestion",
|
|
26
|
+
];
|
|
27
|
+
for (const tool of expected) {
|
|
28
|
+
expect(DISALLOWED_TOOLS_CORE).toContain(tool);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("blocks ScheduleWakeup — /loop-skill-only tool that wedges the dispatcher when called outside /loop mode", () => {
|
|
33
|
+
// Confirmed root cause of a 35-minute hang on 2026-04-27.
|
|
34
|
+
// ScheduleWakeup registers a wakeup the runtime never fires, so the
|
|
35
|
+
// chat lock is held indefinitely until manual restart.
|
|
36
|
+
expect(DISALLOWED_TOOLS_CORE).toContain("ScheduleWakeup");
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("DISALLOWED_TOOLS_BACKGROUND", () => {
|
|
41
|
+
it("inherits everything from CORE", () => {
|
|
42
|
+
for (const tool of DISALLOWED_TOOLS_CORE) {
|
|
43
|
+
expect(DISALLOWED_TOOLS_BACKGROUND).toContain(tool);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("additionally blocks Agent (no nested agents in dream/heartbeat)", () => {
|
|
48
|
+
expect(DISALLOWED_TOOLS_BACKGROUND).toContain("Agent");
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("DISALLOWED_TOOLS_CHAT", () => {
|
|
53
|
+
it("inherits everything from CORE", () => {
|
|
54
|
+
for (const tool of DISALLOWED_TOOLS_CORE) {
|
|
55
|
+
expect(DISALLOWED_TOOLS_CHAT).toContain(tool);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("additionally blocks Claude's built-in web tools (replaced by Brave MCP)", () => {
|
|
60
|
+
expect(DISALLOWED_TOOLS_CHAT).toContain("WebSearch");
|
|
61
|
+
expect(DISALLOWED_TOOLS_CHAT).toContain("WebFetch");
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -29,11 +29,6 @@ vi.mock("../core/errors.js", () => ({
|
|
|
29
29
|
TalonError: class TalonError extends Error {},
|
|
30
30
|
}));
|
|
31
31
|
|
|
32
|
-
vi.mock("../core/prompt-builder.js", () => ({
|
|
33
|
-
enrichDMPrompt: vi.fn((p: string) => p),
|
|
34
|
-
enrichGroupPrompt: vi.fn((p: string) => p),
|
|
35
|
-
}));
|
|
36
|
-
|
|
37
32
|
vi.mock("../storage/daily-log.js", () => ({
|
|
38
33
|
appendDailyLog: vi.fn(),
|
|
39
34
|
appendDailyLogResponse: vi.fn(),
|
|
@@ -5,10 +5,6 @@ const executeMock = vi.hoisted(() => vi.fn());
|
|
|
5
5
|
vi.mock("../core/dispatcher.js", () => ({
|
|
6
6
|
execute: executeMock,
|
|
7
7
|
}));
|
|
8
|
-
vi.mock("../core/prompt-builder.js", () => ({
|
|
9
|
-
enrichDMPrompt: vi.fn((p: string) => p),
|
|
10
|
-
enrichGroupPrompt: vi.fn((p: string) => p),
|
|
11
|
-
}));
|
|
12
8
|
vi.mock("../storage/daily-log.js", () => ({
|
|
13
9
|
appendDailyLog: vi.fn(),
|
|
14
10
|
appendDailyLogResponse: vi.fn(),
|
|
@@ -2641,11 +2637,8 @@ describe("handleStickerMessage — video sticker branch (L835 TRUE)", () => {
|
|
|
2641
2637
|
}, 3000);
|
|
2642
2638
|
});
|
|
2643
2639
|
|
|
2644
|
-
describe("processAndReply — group message without senderId
|
|
2645
|
-
it("
|
|
2646
|
-
const { enrichGroupPrompt } = await import("../core/prompt-builder.js");
|
|
2647
|
-
(enrichGroupPrompt as ReturnType<typeof vi.fn>).mockClear();
|
|
2648
|
-
|
|
2640
|
+
describe("processAndReply — group message without senderId", () => {
|
|
2641
|
+
it("processes anonymous group messages without mutating the prompt", async () => {
|
|
2649
2642
|
executeMock.mockResolvedValueOnce({
|
|
2650
2643
|
text: "",
|
|
2651
2644
|
durationMs: 10,
|
|
@@ -2672,13 +2665,14 @@ describe("processAndReply — group message without senderId (L552 FALSE branch)
|
|
|
2672
2665
|
await handleTextMessage(ctx, mockBot, mockConfig);
|
|
2673
2666
|
await new Promise((r) => setTimeout(r, 700));
|
|
2674
2667
|
|
|
2675
|
-
//
|
|
2676
|
-
expect(enrichGroupPrompt).not.toHaveBeenCalled();
|
|
2677
|
-
// But message was still processed
|
|
2668
|
+
// Message was still processed, and the prompt is passed through verbatim.
|
|
2678
2669
|
const calls = executeMock.mock.calls
|
|
2679
2670
|
.slice(before)
|
|
2680
2671
|
.filter((c) => (c[0] as { chatId: string }).chatId === String(chatId));
|
|
2681
2672
|
expect(calls.length).toBe(1);
|
|
2673
|
+
expect((calls[0][0] as { prompt: string }).prompt).toBe(
|
|
2674
|
+
"@testbot anonymous message",
|
|
2675
|
+
);
|
|
2682
2676
|
}, 3000);
|
|
2683
2677
|
});
|
|
2684
2678
|
|
|
@@ -71,10 +71,11 @@ describe("log", () => {
|
|
|
71
71
|
);
|
|
72
72
|
});
|
|
73
73
|
|
|
74
|
-
it("includes Error message in context", () => {
|
|
75
|
-
|
|
74
|
+
it("includes Error message and stack in context", () => {
|
|
75
|
+
const err = new Error("timeout");
|
|
76
|
+
logError("bridge", "request failed", err);
|
|
76
77
|
expect(mockError).toHaveBeenCalledWith(
|
|
77
|
-
{ component: "bridge", err: "timeout" },
|
|
78
|
+
{ component: "bridge", err: "timeout", stack: err.stack },
|
|
78
79
|
"request failed",
|
|
79
80
|
);
|
|
80
81
|
});
|