| name | memory-heartbeat |
| description | use when: scheduled/cron-driven open-brain maintenance — detects time window (WORK-HOURS / END-OF-DAY / WEEKLY / QUIET) and runs the matching silent action (lifecycle pipeline, daily digest, weekly summary, provenance check). NOT for: manual memory review with approvals (use ob-triage); writing new memories (use ob-migrate or ingest-content). boundary: memory-heartbeat is autonomous and silent in QUIET mode; ob-triage is interactive with AskUserQuestion per action. |
| version | 0.2.0 |
| requires_standards | ["open-brain/cli-routing","open-brain/memory-status-conventions"] |
| requires | ["standard:english-only","mcp:open-brain"] |
Memory Heartbeat
Run the appropriate open-brain maintenance action for the current time window.
Determine time context, select the correct mode, execute MCP tools, and report results.
Self-Test Scenarios
These scenarios document the expected behavior for each time window.
Use these as a verification reference when testing or reviewing the skill.
| Scenario | Input (DOW, HOUR) | Expected Mode | Expected Action |
|---|
| Monday morning | Mon, 09:00 | WORK-HOURS | run_lifecycle_pipeline(scope="recent") |
| Wednesday afternoon | Wed, 14:30 | WORK-HOURS | run_lifecycle_pipeline(scope="recent") |
| WORK-HOURS + extraction due | Mon, 09:00, no last_learnings_run | WORK-HOURS | lifecycle pipeline + spawn learning-extractor(scope=all-projects) |
| WORK-HOURS + extraction recent | Mon, 10:00, last_learnings_run=1h ago | WORK-HOURS | lifecycle pipeline only, skip learnings extraction |
| Thursday end-of-day | Thu, 17:30 | END-OF-DAY | search today + stats → daily digest |
| Friday end-of-day | Fri, 17:30 | WEEKLY | run_lifecycle_pipeline(scope=None) + weekly summary |
| Friday morning | Fri, 09:00 | WORK-HOURS | run_lifecycle_pipeline(scope="recent") |
| Saturday midday | Sat, 12:00 | QUIET | silent exit, no MCP calls |
| Sunday evening | Sun, 20:00 | QUIET | silent exit, no MCP calls |
| Monday evening | Mon, 20:00 | QUIET | silent exit, no MCP calls (after 7PM, no mode matches → quiet) |
| Monday late night | Mon, 23:00 | QUIET | silent exit, no MCP calls (10PM-6AM) |
| Tuesday early morning | Tue, 05:00 | QUIET | silent exit, no MCP calls (10PM-6AM) |
| Tuesday 5PM-6PM | Tue, 17:30 | END-OF-DAY | daily digest (5PM-7PM overlap: EOD beats work-hours) |
| Friday 5PM-6PM | Fri, 17:30 | WEEKLY | weekly supersedes EOD in overlap |
| Second run same window | any active window | IDEMPOTENT | "No pending work in this window." if 0 actions |
| MCP unreachable | any active window | ERROR | warning message + reconnect hint |
Step 1: Detect Time Context
Run: uv run python hooks/scripts/check_time_window.py
Parse the JSON output: window, hour, dow.
Classify the window using this priority order (highest first):
- QUIET — if
HOUR < 6 OR HOUR >= 22 → applies any day
- QUIET — if
DOW >= 6 (Saturday=6, Sunday=7) → weekend, always quiet
- WEEKLY — if
DOW == 5 AND HOUR >= 17 AND HOUR < 19 (Friday 5PM-7PM)
- END-OF-DAY — if
DOW <= 4 AND HOUR >= 17 AND HOUR < 19 (Mon-Thu 5PM-7PM; Friday is handled by WEEKLY above)
- WORK-HOURS — if
DOW <= 5 AND HOUR >= 6 AND HOUR < 17 (Mon-Fri 6AM-5PM)
Note: Step 3 checks Friday explicitly before step 4 so Friday 5PM-7PM always routes to WEEKLY.
Any hour not matched by rules 1-5 (e.g. weekday 7PM-10PM) falls through to QUIET (no active mode).
Step 2: Execute Mode
QUIET mode
Output nothing. Do not call any MCP tools. Exit silently.
WORK-HOURS mode
Call mcp__open-brain__run_lifecycle_pipeline with scope="recent" and
dry_run=false.
If the tool call fails (MCP unreachable), emit the standard error message from
the open-brain/memory-status-conventions standard with verb "heartbeat
skipped".
If the result shows 0 newly staged actions:
No pending work in this window.
Otherwise summarize:
Memory Heartbeat — Work Hours
Lifecycle pipeline (scope=recent):
- Analyzed: <N> memories
- Newly staged: <N> proposals
- <triage summary from result>
Then proceed to Step 3: Provenance Check (see below).
Learnings Extraction (every 4h)
After running the lifecycle pipeline, check if periodic learnings extraction is due:
-
Check the rate-limit in processing-state.json:
Run: uv run python hooks/scripts/check_learnings_due.py
Parse data.result from the execution-result envelope: due or skip.
-
If output is due:
-
If output is skip:
- Include in summary: "Learnings extraction: skipped (last run < 4h ago)"
END-OF-DAY mode
Goal: Produce a daily digest of what was stored in open-brain today.
- Get today's date:
TODAY=$(date +%Y-%m-%d)
- Call
mcp__open-brain__search with:
query = "session summary observations decisions"
date_start = TODAY's date
order_by = "created_at"
limit = 50
- Call
mcp__open-brain__stats (no arguments)
If either tool call fails, emit the standard MCP error message (see
open-brain/memory-status-conventions).
If no memories found today and nothing notable in stats:
No pending work in this window.
Otherwise produce a digest:
Memory Heartbeat — End of Day (<date>)
Today's memories: <count from search>
Total memories: <from stats>
Key themes today:
- <summarize top 3-5 themes from search results>
Sessions today: <count if available>
WEEKLY mode
Goal: Full triage + weekly summary.
- Call
mcp__open-brain__run_lifecycle_pipeline without a scope argument (omit the parameter entirely) and with dry_run=false.
- Call
mcp__open-brain__stats (no arguments).
- Call
mcp__open-brain__search with:
query = "session summary week"
date_start = date 7 days ago (date -v-7d +%Y-%m-%d on macOS; Linux: date -d '7 days ago' +%Y-%m-%d)
order_by = "created_at"
limit = 100
If any tool call fails, emit the standard MCP error message (see
open-brain/memory-status-conventions).
If lifecycle returned 0 newly staged actions and no memories in the past week:
No pending work in this window.
Otherwise produce a weekly summary:
Memory Heartbeat — Weekly Summary (<date>)
Full lifecycle triage:
- Processed: <N> memories
- <key actions from pipeline result>
This week's memory stats:
- Total memories: <from stats>
- Memories this week: <count from search>
- DB size: <from stats if available>
Weekly themes:
- <summarize top 5-7 themes from search results>
Notable patterns:
- <any recurring topics, decisions, or projects from the week>
Step 3: Provenance Check (WORK-HOURS only)
After the lifecycle pipeline, run a provenance check on code-referencing memories.
Goal: Find top-10 memories most likely to reference code artifacts, verify they are still
valid, and update their confidence_score + last_verified metadata.
The implementation module lives at:
malte/skills/memory_heartbeat/provenance.py (pure functions, no MCP calls).
3.1 Search for code-referencing memories
Call mcp__open-brain__search with:
query = "file path function code artifact"
limit = 10
3.2 For each memory, run the staleness check
Use hooks/scripts/provenance_check.py to compute the metadata patch.
Assign the JSON-serialised dict for each memory to a shell variable, then pipe it:
MEMORY_JSON=$(...)
printf '%s' "$MEMORY_JSON" | uv run python hooks/scripts/provenance_check.py
Parse stdout as JSON; null means no code refs found.
Important: always assign the JSON to a variable first and pipe via printf '%s' "$MEMORY_JSON".
Do NOT use echo '<literal_json>' — single quotes cannot be nested and newlines break the shell
syntax if the JSON contains either.
Set the PROVENANCE_REPO_ROOT env var to the absolute path of the claude-config repo root
(e.g. /Users/malte/code/claude) so relative code refs are resolved correctly.
3.3 Apply updates — stale memories (AK4)
For each memory where update["metadata_patch"]["confidence_score"] == "low":
- Call
mcp__open-brain__update_memory(id=X, metadata=update["metadata_patch"])
— stores confidence_score="low", last_verified=<now>, stale_refs=[...]
- Call
mcp__open-brain__update_memory(id=X, type="archived")
— auto-archives the stale memory (AK4)
3.4 Apply updates — valid memories
For each memory where update is not None and confidence_score is "high" or "medium":
Call mcp__open-brain__update_memory(id=X, metadata=update["metadata_patch"])
Skip memories where build_provenance_update returned null (no code refs found).
3.5 Report summary
Provenance check: N memories scanned, M stale → archived, K verified
Step 4: Idempotency
The skill is idempotent within a time window by design:
- WORK-HOURS:
run_lifecycle_pipeline(scope="recent") stores proposals behind a (memory_id, policy_version) uniqueness constraint. If there is nothing new to stage it returns newly_staged_count=0.
- END-OF-DAY: Search returns the same memories on repeated calls → re-running produces the same read-only digest without side effects, which is acceptable idempotency for a stateless skill (no actions are duplicated).
- WEEKLY:
run_lifecycle_pipeline with no scope uses the same ledger guard, so memories already classified by the active policy are skipped.
Error Handling Reference
| Situation | Response |
|---|
| MCP tool call throws / unreachable | Standard MCP error message (see open-brain/memory-status-conventions) with verb "heartbeat skipped" |
| Lifecycle returns 0 newly staged actions | No pending work in this window. |
| Search returns 0 results | Include "No memories found today/this week" in digest |
| date command fails | Fall back to QUIET mode (safe default) |