Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
a3a6d56
feat: integrate mempalace as built-in plugin for long-term memory
dylanneve1 Apr 9, 2026
de9662e
refactor(dream): mine daily notes instead of raw logs, add diary writing
dylanneve1 Apr 9, 2026
38c9eda
fix: address PR #27 review comments
dylanneve1 Apr 10, 2026
7245f8f
fix: restore mempalace CLI diary writer, keep path quoting
dylanneve1 Apr 10, 2026
1144a74
test: add gating/validation tests for mempalace integration
dylanneve1 Apr 10, 2026
e4c4423
feat: upgrade mempalace system prompt with comprehensive tool docs
dylanneve1 Apr 10, 2026
e8b528b
refactor: extract mempalace prompt to prompts/mempalace.md
dylanneve1 Apr 10, 2026
edbaf63
fix: replace pixi.intel.com registry URLs in lockfile with npmjs.org
dylanneve1 Apr 10, 2026
dff4620
fix: resolve all lint warnings and formatting issues
dylanneve1 Apr 10, 2026
bc015c4
fix: address all 17 Copilot review comments on PR #27
dylanneve1 Apr 10, 2026
20f428a
fix: add 'mempalace' to LogComponent type
dylanneve1 Apr 10, 2026
4d020d0
fix: allow MCP tools in dream prompt when required by Stage 5
dylanneve1 Apr 10, 2026
0923934
fix: duplicate guard in registerPlugin, pass MCP servers to dream
dylanneve1 Apr 10, 2026
4af64f0
feat: add selective MCP server loading via 'only' filter
dylanneve1 Apr 10, 2026
210f2f0
fix: load mempalace prompt from dirs.prompts, fix dream systemPrompt
dylanneve1 Apr 10, 2026
877dd4a
fix: move duplicate check before validation, gate dream on plugin reg…
dylanneve1 Apr 10, 2026
717477e
fix(mempalace): correct CLI arg order for status check
dylanneve1 Apr 10, 2026
c7e292d
fix: address Copilot review round 8 — validation, error handling, unu…
dylanneve1 Apr 10, 2026
be69bc4
fix: remove config from message queue chain, fix pythonPath comment
dylanneve1 Apr 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 6 additions & 2 deletions prompts/dream.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
You are Talon's background memory consolidation agent. Your job is to update the persistent memory file with new information learned from recent interaction logs.

You have access ONLY to filesystem tools (Read, Write, Edit, Bash, Glob, Grep). Do NOT attempt to use any Telegram, MCP, or messaging tools.
You primarily use filesystem tools (Read, Write, Edit, Bash, Glob, Grep). Do NOT attempt to use any Telegram or other messaging tools. MCP tools may be used if required by Stage 5.

## Your 4-stage task
## Your 5-stage task

Comment thread
dylanneve1 marked this conversation as resolved.
Resolved
### Stage 1 — Orient

Expand Down Expand Up @@ -42,4 +42,8 @@ You have access ONLY to filesystem tools (Read, Write, Edit, Bash, Glob, Grep).
- Do NOT remove entries just because they're old — only remove if wrong or superseded
- Write the updated memory.md back to `{{memoryFile}}`

### Stage 5 — Mine to MemPalace & Write Diary (optional)

{{mempalaceSection}}

When done with memory consolidation, stop. The system handles all dream_state.json updates.
Comment thread
dylanneve1 marked this conversation as resolved.
Resolved
57 changes: 57 additions & 0 deletions prompts/mempalace.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
## MemPalace — Long-term Memory

You have access to a local memory palace via MCP tools. The palace stores verbatim conversation history and a temporal knowledge graph — all local, zero cloud, zero API calls.

### Architecture

- **Wings** = top-level categories (people, projects, topics)
- **Rooms** = specific subjects within a wing
- **Drawers** = individual memory chunks (verbatim text)
- **Knowledge Graph** = entity-relationship facts with temporal validity

### Protocol — FOLLOW EVERY SESSION

1. **BEFORE RESPONDING** about any person, project, or past event: call `mempalace_search` or `mempalace_kg_query` FIRST. Never guess — verify from the palace.
2. **IF UNSURE** about a fact (name, age, relationship, preference): query the palace. Wrong is worse than slow.
3. **WHEN FACTS CHANGE**: Call `mempalace_kg_invalidate` on the old fact, then `mempalace_kg_add` for the new one.
4. **AFTER LEARNING** something important: store it. Use `mempalace_add_drawer` for rich context, `mempalace_kg_add` for structured facts.

### Tools

**Search & Browse:**

- `mempalace_search` — Semantic search. Use short keywords/questions, not full sentences. Filter by wing/room.
- `mempalace_check_duplicate` — Check before filing new content (threshold default 0.9, lower to 0.85 to catch near-dupes).
- `mempalace_status` — Palace overview: total drawers, wings, rooms.
- `mempalace_list_wings` / `mempalace_list_rooms` — Browse structure.
- `mempalace_get_taxonomy` — Full wing/room/count tree.

**Knowledge Graph (Temporal Facts):**

- `mempalace_kg_query` — Query entity relationships. Supports `as_of` date filtering.
- `mempalace_kg_add` — Add fact: subject -> predicate -> object. Optional `valid_from`.
- `mempalace_kg_invalidate` — Mark a fact as no longer true.
- `mempalace_kg_timeline` — Chronological story of an entity.
- `mempalace_kg_stats` — Graph overview: entities, triples, relationship types.

**Palace Graph (Cross-Domain Connections):**

- `mempalace_traverse` — Walk from a room, find connected ideas across wings.
- `mempalace_find_tunnels` — Find rooms that bridge two wings.
- `mempalace_graph_stats` — Graph connectivity overview.

**Write:**

- `mempalace_add_drawer` — Store verbatim content into a wing/room. Auto-checks duplicates.
- `mempalace_delete_drawer` — Remove a drawer by ID.
- `mempalace_diary_write` — Write a session diary entry (agent_name, entry, topic).
- `mempalace_diary_read` — Read recent diary entries.

### Tips

- Search is **semantic** (meaning-based), not keyword. "What did we discuss about database performance?" works better than "database".
- The knowledge graph stores typed relationships with **time windows**. It knows WHEN things were true.
- Use `mempalace_check_duplicate` before storing new content to avoid clutter.
- Diary entries accumulate across sessions. Write them to build continuity of self.

### Palace location: `{{palacePath}}`
2 changes: 1 addition & 1 deletion src/__tests__/cron-store-extended.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -596,7 +596,7 @@ describe("loadCronJobs — invalid timezone stripping", () => {
beforeEach(() => existsSyncMock.mockReset().mockReturnValue(false));

it("strips invalid timezone from loaded job", async () => {
const { isValidTimezone } = await import("../storage/cron-store.js");
await import("../storage/cron-store.js");
const jobWithBadTz: Record<string, unknown> = {
"tz-bad-id": {
id: "tz-bad-id",
Expand Down
119 changes: 118 additions & 1 deletion src/__tests__/dream.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -747,6 +747,7 @@ describe("dream error paths", () => {
vi.doMock("write-file-atomic", () => ({ default: { sync: vi.fn() } }));
vi.doMock("@anthropic-ai/claude-agent-sdk", () => ({
query: vi.fn(async function* () {
yield; // satisfy require-yield
throw new Error("agent crashed unexpectedly");
}),
}));
Expand Down Expand Up @@ -811,6 +812,7 @@ describe("dream error paths", () => {
}));
vi.doMock("@anthropic-ai/claude-agent-sdk", () => ({
query: vi.fn(async function* () {
yield; // satisfy require-yield
await queryPromise; // suspend until we release
}),
}));
Expand Down Expand Up @@ -957,7 +959,8 @@ describe("runDreamAgent — timeout arrow fn fires after DREAM_TIMEOUT_MS", () =
// query never resolves — so the 10-minute timeout wins the race
vi.doMock("@anthropic-ai/claude-agent-sdk", () => ({
query: vi.fn(async function* () {
await new Promise(() => {}); // never yields
yield; // satisfy require-yield
await new Promise(() => {}); // never resolves
}),
}));

Expand All @@ -973,6 +976,120 @@ describe("runDreamAgent — timeout arrow fn fires after DREAM_TIMEOUT_MS", () =
});
});

describe("mempalace section gating in dream prompt", () => {
beforeEach(() => {
vi.resetModules();
});

it("includes mempalace mining command and diary instructions when mempalace is configured", async () => {
vi.doMock("node:fs", () => ({
existsSync: vi.fn(() => false),
readFileSync: vi.fn(() => "PROMPT START {{mempalaceSection}} PROMPT END"),
mkdirSync: vi.fn(),
appendFileSync: vi.fn(),
}));
vi.doMock("write-file-atomic", () => ({ default: { sync: vi.fn() } }));
vi.doMock("../util/log.js", () => ({
log: vi.fn(),
logError: vi.fn(),
logWarn: vi.fn(),
}));
vi.doMock("../util/paths.js", () => ({
files: {
dreamState: "/fake/.talon/data/dream_state.json",
memory: "/fake/.talon/workspace/memory/memory.md",
log: "/fake/.talon/talon.log",
},
dirs: {
root: "/fake/.talon",
logs: "/fake/.talon/workspace/logs",
workspace: "/fake/.talon/workspace",
data: "/fake/.talon/data",
memory: "/fake/.talon/workspace/memory",
dailyMemory: "/fake/.talon/workspace/memory/daily",
},
}));
const queryMock = vi.fn(async function* () {});
vi.doMock("@anthropic-ai/claude-agent-sdk", () => ({ query: queryMock }));

const mod = await import("../core/dream.js");
mod.initDream({
model: "claude-sonnet-4-6",
workspace: "/fake/ws",
mempalace: {
pythonPath: "/usr/bin/python3",
palacePath: "/fake/palace",
},
});
await mod.forceDream();

expect(queryMock).toHaveBeenCalled();
const callArgs = (queryMock.mock.calls[0] as unknown[])[0] as {
prompt: string;
};
// Mempalace mining command should be in the prompt
expect(callArgs.prompt).toContain("-m mempalace mine");
// Diary instructions should be in the prompt
expect(callArgs.prompt).toContain("mempalace_diary_write");
// Should NOT contain the skip message
expect(callArgs.prompt).not.toContain(
"MemPalace is not configured. Skip this stage.",
);
});

it("includes skip message when mempalace is not configured", async () => {
vi.doMock("node:fs", () => ({
existsSync: vi.fn(() => false),
readFileSync: vi.fn(() => "PROMPT START {{mempalaceSection}} PROMPT END"),
mkdirSync: vi.fn(),
appendFileSync: vi.fn(),
}));
vi.doMock("write-file-atomic", () => ({ default: { sync: vi.fn() } }));
vi.doMock("../util/log.js", () => ({
log: vi.fn(),
logError: vi.fn(),
logWarn: vi.fn(),
}));
vi.doMock("../util/paths.js", () => ({
files: {
dreamState: "/fake/.talon/data/dream_state.json",
memory: "/fake/.talon/workspace/memory/memory.md",
log: "/fake/.talon/talon.log",
},
dirs: {
root: "/fake/.talon",
logs: "/fake/.talon/workspace/logs",
workspace: "/fake/.talon/workspace",
data: "/fake/.talon/data",
memory: "/fake/.talon/workspace/memory",
dailyMemory: "/fake/.talon/workspace/memory/daily",
},
}));
const queryMock = vi.fn(async function* () {});
vi.doMock("@anthropic-ai/claude-agent-sdk", () => ({ query: queryMock }));

const mod = await import("../core/dream.js");
// No mempalace config
mod.initDream({
model: "claude-sonnet-4-6",
workspace: "/fake/ws",
});
await mod.forceDream();

expect(queryMock).toHaveBeenCalled();
const callArgs = (queryMock.mock.calls[0] as unknown[])[0] as {
prompt: string;
};
// Should contain the skip message
expect(callArgs.prompt).toContain(
"MemPalace is not configured. Skip this stage.",
);
// Should NOT contain mempalace mining commands
expect(callArgs.prompt).not.toContain("-m mempalace mine");
expect(callArgs.prompt).not.toContain("mempalace_diary_write");
});
});

describe("maybeStartDream — () => {} catch callback on executeDream rejection", () => {
it("fires the catch callback silently when executeDream('auto') rejects", async () => {
vi.resetModules();
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/fuzz.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ vi.mock("../storage/cron-store.js", () => ({
// ── Imports (after mocks) ────────────────────────────────────────────────────

const { classify, TalonError } = await import("../core/errors.js");
const { validateCronExpression } = await import("../storage/cron-store.js");
await import("../storage/cron-store.js");
const { handleSharedAction } = await import("../core/gateway-actions.js");
const { resolveModelName } = await import("../storage/chat-settings.js");
const { Cron } = await import("croner");
Expand Down
4 changes: 0 additions & 4 deletions src/__tests__/gateway-retry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,10 +244,8 @@ describe("withRetry", () => {

describe("exhausting all retries", () => {
it("throws the last error after 3 total attempts for retryable errors", async () => {
let calls = 0;
const networkErr = talonErr("network");
const fn = vi.fn(async () => {
calls++;
throw networkErr;
});

Expand All @@ -257,9 +255,7 @@ describe("withRetry", () => {
});

it("throws the last error after 3 total attempts for overloaded errors", async () => {
let calls = 0;
const fn = vi.fn(async () => {
calls++;
throw talonErr("overloaded");
});

Expand Down
4 changes: 0 additions & 4 deletions src/__tests__/handlers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -533,8 +533,6 @@ describe("handleTextMessage — integration via mock Context", () => {
});

describe("handlePhotoMessage — downloads and enqueues photo", () => {
let restoreFetch: () => void;

beforeEach(() => {
// Mock bot.api.getFile for file download
mockBot.api.getFile = vi.fn(async () => ({ file_path: "photos/test.jpg" }));
Expand All @@ -547,7 +545,6 @@ describe("handlePhotoMessage — downloads and enqueues photo", () => {
headers: { get: (_name: string) => null },
arrayBuffer: async () => jpegBuf.buffer,
}));
restoreFetch = () => {};
vi.stubGlobal("fetch", mockFetch);

executeMock.mockResolvedValue({
Expand Down Expand Up @@ -795,7 +792,6 @@ describe("rate limiting — isUserRateLimited via handleTextMessage", () => {
}

// 16th message should be rate limited (return early without enqueuing)
const before = executeMock.mock.calls.length;
await handleTextMessage(makeCtx(15), mockBot, mockConfig);

// Wait to confirm no debounce fires for the 16th chat
Expand Down
3 changes: 3 additions & 0 deletions src/__tests__/heartbeat.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ describe("forceHeartbeat", () => {

// Make agent throw
queryMock.mockImplementationOnce(async function* () {
yield; // satisfy require-yield
throw new Error("Agent exploded");
});

Expand All @@ -217,6 +218,7 @@ describe("forceHeartbeat", () => {
existsSyncMock.mockReturnValue(false);

queryMock.mockImplementationOnce(async function* () {
yield; // satisfy require-yield
throw new Error("Boom");
});

Expand Down Expand Up @@ -340,6 +342,7 @@ describe("awaitCurrentRun", () => {
});

queryMock.mockImplementationOnce(async function* () {
yield; // satisfy require-yield
await agentPromise;
});

Expand Down
Loading
Loading