Repository navigation
Add heartbeat system for periodic background maintenance - #9
Conversation
Runs a background agent at configurable intervals (default 60min) to perform workspace cleanup, memory consolidation, VM updates, and health checks. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds an optional “heartbeat” background maintenance loop that periodically runs a Claude agent using a prompt template and user-provided instructions, with state tracking and dedicated logging, and wires it into startup/shutdown.
Changes:
- Introduces
src/core/heartbeat.tsto schedule and execute periodic background maintenance runs (with logging + state file). - Adds new config flags for enabling heartbeat, setting its interval (minutes), and optionally overriding the model.
- Wires heartbeat start/stop into application bootstrap and graceful shutdown; adds path + log component support and a new prompt template.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/core/heartbeat.ts | New heartbeat scheduler/runner, state persistence, and streaming log capture for agent runs |
| prompts/heartbeat.md | Prompt template for the heartbeat agent with variable interpolation and default task guidance |
| src/util/config.ts | Adds heartbeat, heartbeatInterval, and heartbeatModel to config schema |
| src/util/paths.ts | Adds files.heartbeatState path for persisted heartbeat state |
| src/util/log.ts | Extends LogComponent union to include "heartbeat" |
| src/bootstrap.ts | Initializes heartbeat config alongside other background subsystems |
| src/index.ts | Starts heartbeat timer on boot (if enabled) and stops it during graceful shutdown |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
… tests - Guard startHeartbeatTimer against double-start during startup delay - Track last_started separately; only update last_run/run_count on success - Add awaitCurrentRun() for graceful shutdown of in-flight heartbeats - Add comprehensive test suite (16 tests covering all review concerns) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Validate all HeartbeatState fields (run_count, status, last_started)
via normalizeHeartbeatState() — corrupt state now returns null
- Throw on uninitialized configRef instead of silently succeeding
- Add sequence number to log filenames for uniqueness
- Remove unused dreamModel from heartbeat config shape
- Clarify prompt template for first-ever run ("never" case)
- Add 3 new validation tests (19 total)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 6 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Validate intervalMinutes (reject NaN, 0, negative values) - Derive instructions file path from workspace config, not dirs constant - Document timeout limitation (SDK lacks abort mechanism) - Omit last_run_at when last_run is 0 to avoid misleading epoch timestamps - Rename heartbeatInterval → heartbeatIntervalMinutes for clarity - Keep logWarn import (now used for interval validation) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- clearTimeout on successful agent completion to prevent dangling timers - On timeout, await agentPromise before releasing running lock to prevent overlapping heartbeat runs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- awaitCurrentRun() now accepts a timeout (default 10s) to avoid blocking shutdown indefinitely when a heartbeat agent is mid-run - Reset readFileSyncMock in test beforeEach with path-sensitive impl to prevent order-dependent test failures Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Cast path-sensitive readFileSyncMock to any to satisfy vi.fn() type constraint while accepting a filePath parameter. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Load heartbeat prompt from dirs.prompts instead of relative import.meta.url resolution, fixing breakage when running from non-root working directories - Remove unused dirname/fileURLToPath imports - Replace no-op timer guard assertions with setTimeout/setInterval spy counts - Add prompts dir to test path mock Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Replace appendFileSync/mkdirSync with async appendFile/mkdir from node:fs/promises to avoid blocking the event loop during heartbeat agent streaming. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
JSON.stringify(NaN) serializes to null, so the previous test was hitting the wrong validation path. Use raw JSON with 1e309 (Infinity) to actually exercise the Number.isFinite() guard. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Validate mcpServer.args elements are strings and reject empty command (#1) - Shell-quote interpolated paths in dream bash commands (#2, #11) - Replace CLI-based diary write with mempalace_diary_write MCP tool (#3) - Make validation error message platform-agnostic (#4) - Update mempalacePython comment for platform-dependent default (#5) - Wrap mp.init() in Promise.race with 30s timeout (#6) - Make init conditional on successful validation, pass actual config (#7) - Move import mempalace check into validateConfig (#8) - Replace execFileSync with async execFile in init() (#9) - Document that registerPlugin does NOT call init (#10) - Update dream prompt header from "4-stage" to "5-stage" (#12) - Update getPluginMcpServers JSDoc to document mcpServer path (#13, #17) - Add .min(1) to palacePath/pythonPath zod schemas (#14) - Distinguish ENOENT/EACCES/EPERM from import failures in validation (#15) - Fix test name from "logs warning" to match actual behavior (#16) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: integrate mempalace as built-in plugin for long-term memory Adds mempalace (Python MCP server) as a first-class memory system. When enabled, the agent gets semantic search, knowledge graph, and verbatim memory storage via ChromaDB — all local, zero API calls. Key changes: - Extend plugin system with `mcpServer` field for non-Node MCP servers - Add `registerPlugin()` for built-in plugin registration - Create mempalace plugin (factory pattern, validates python venv) - Wire mempalace into dream mode (Stage 5: mine logs into palace) - Add `mempalace` config schema (enabled, palacePath, pythonPath) - Add default paths for palace dir and python venv binary Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor(dream): mine daily notes instead of raw logs, add diary writing Dream Stage 5 now mines memory/daily/ (curated observations) instead of raw logs/ directory, eliminating junk chunks (tool JSON, df output, etc). Added personal diary writing instruction — agent reflects on feelings, state of mind, learnings, and loose threads after each dream run. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address PR #27 review comments - plugin.ts: validate mcpServer.args entries are strings and reject empty command - dream.ts: quote interpolated paths in shell commands, replace mcp_server CLI diary with direct file write - mempalace/index.ts: platform-agnostic error message for missing python binary - paths.ts: update comment to reflect platform-dependent venv path - bootstrap.ts: wrap mempalace init in 30s timeout to match loadSinglePlugin behavior Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: restore mempalace CLI diary writer, keep path quoting Copilot suggested removing the mcp_server CLI invocation for diary writing but that's the intended mempalace interface. Restored it with quoted paths. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: add gating/validation tests for mempalace integration - plugin.ts: test rejection of empty mcpServer.command and non-string args elements - dream.ts: test mempalace section gating — verify mining/diary instructions only appear when mempalace is configured, skip message when not - 1306 tests passing (4 new) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: upgrade mempalace system prompt with comprehensive tool docs Adapted from mempalace SKILL.md (v3.1.0). Key improvements: - Session protocol (verify before responding, invalidate stale facts) - Full tool documentation including kg_timeline, traverse, find_tunnels, diary_read/write, delete_drawer, graph_stats, check_duplicate - Semantic search tips (meaning-based, not keyword) - Knowledge graph temporal validity guidance - Tests updated to verify all tool names appear in prompt Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: extract mempalace prompt to prompts/mempalace.md Move system prompt instructions out of TypeScript into a .md file, matching the pattern used by dream.md and other prompts. Plugin loads and interpolates {{palacePath}} at runtime with graceful fallback. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: replace pixi.intel.com registry URLs in lockfile with npmjs.org Lockfile had resolved URLs pointing to pixi.intel.com (private/corporate registry) for @Anthropic-AI packages, causing CI to fail with ENOTFOUND. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: resolve all lint warnings and formatting issues Remove unused imports, variables, and catch bindings across 14 files. Add yield statements to generator function mocks. Fix prettier formatting. 0 lint warnings, 0 format issues, 1307 tests passing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address all 17 Copilot review comments on PR #27 - Validate mcpServer.args elements are strings and reject empty command (#1) - Shell-quote interpolated paths in dream bash commands (#2, #11) - Replace CLI-based diary write with mempalace_diary_write MCP tool (#3) - Make validation error message platform-agnostic (#4) - Update mempalacePython comment for platform-dependent default (#5) - Wrap mp.init() in Promise.race with 30s timeout (#6) - Make init conditional on successful validation, pass actual config (#7) - Move import mempalace check into validateConfig (#8) - Replace execFileSync with async execFile in init() (#9) - Document that registerPlugin does NOT call init (#10) - Update dream prompt header from "4-stage" to "5-stage" (#12) - Update getPluginMcpServers JSDoc to document mcpServer path (#13, #17) - Add .min(1) to palacePath/pythonPath zod schemas (#14) - Distinguish ENOENT/EACCES/EPERM from import failures in validation (#15) - Fix test name from "logs warning" to match actual behavior (#16) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: add 'mempalace' to LogComponent type TypeScript type check was failing because 'mempalace' wasn't in the LogComponent union type used by log/logError/logWarn functions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: allow MCP tools in dream prompt when required by Stage 5 Update tool access statement to permit MCP tools for mempalace mining stage instead of blanket-blocking all MCP tools. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: duplicate guard in registerPlugin, pass MCP servers to dream - registerPlugin now checks for duplicates before setting env vars or logging success, preventing misleading logs and env clobbering - Dream agent now receives mempalace MCP servers when configured, so Stage 5 diary/mining tools actually work Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add selective MCP server loading via 'only' filter getPluginMcpServers now accepts an optional plugin name filter: - omitted = all plugins (backwards compatible for chat sessions) - [] = none - ["mempalace"] = only mempalace Dream mode uses ["mempalace"] to load only what it needs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: load mempalace prompt from dirs.prompts, fix dream systemPrompt - Mempalace prompt now loads from ~/.talon/prompts/mempalace.md (user-customisable, seeded on first run) instead of relative to source file. Consistent with heartbeat/dream prompt loading. - Dream systemPrompt now permits MemPalace MCP tools when configured, preventing conflict with the markdown prompt's Stage 5 instructions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: move duplicate check before validation, gate dream on plugin registration - registerPlugin checks for duplicates before running validateConfig, avoiding expensive re-validation on accidental double registration - Dream mempalace integration now gated on getPlugin("mempalace") instead of just config.mempalace.enabled, so failed validation or registration doesn't cause dream-time MCP tool failures Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(mempalace): correct CLI arg order for status check The --palace flag is a global option that must come before the subcommand. Wrong order caused the init health check to always fail with exit 2, logging a misleading "not yet initialized" warning. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address Copilot review round 8 — validation, error handling, unused param - Validate `import mempalace.mcp_server` (actual spawned module) instead of just `import mempalace` in validateConfig - Add timeout/killed error branching in validateConfig catch block (ETIMEDOUT, signal, killed) with specific messages instead of generic "not installed" - Include stderr details in import failure messages for debugging - Remove unused `config` from ProcessAndReplyParams and all processAndReply call sites (flushQueue, retry, callback handler) - Replace `mempalace status` CLI smoke test in init() with a simple import check — fixes false "Palace not yet initialized" warning when palace IS initialized but CLI subcommand doesn't exist Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: remove config from message queue chain, fix pythonPath comment - Remove config from queue entry type, enqueueMessage signature, and all 3 call sites — completes the cleanup started in round 8 - Eliminates unnecessary TalonConfig reference (including botToken) from queue state - Update mempalace plugin header comment to document platform-dependent pythonPath default (bin/python on Unix, Scripts/python.exe on Windows) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Summary
heartbeat-instructions.mdand executes them with filesystem-only tools (no Telegram/MCP access)logs/heartbeats/, state tracking inheartbeat_state.json, 10-minute timeout with proper cleanup, and graceful shutdown supportheartbeat(bool),heartbeatIntervalMinutes(min 5, default 60),heartbeatModel(optional model override)Key implementation details
last_run/run_countonly updated on successful completion;last_startedtracked separately for failure visibilitynormalizeHeartbeatState()validates all fields (type, finiteness, enum values) — corrupt state returns nulltimerandstartupTimer;runninglock held through timeout to prevent overlapping runsawaitCurrentRun()with configurable timeout budget (default 10s) so heartbeat doesn't block shutdown indefinitelyreadFileSyncMockprevents order-dependent test failuresFiles
src/core/heartbeat.ts— core implementation (~480 lines)src/__tests__/heartbeat.test.ts— 19 tests covering guards, state persistence, failure paths, shutdownprompts/heartbeat.md— agent prompt template with variable interpolationsrc/util/config.ts— new config schema fieldssrc/util/paths.ts— heartbeat state file pathsrc/util/log.ts— heartbeat log componentsrc/bootstrap.ts/src/index.ts— init and lifecycle wiringTest plan
/restartcommand🤖 Generated with Claude Code