Skip to content

Commit a81e9b8

Browse files
committed
Fix: prevent storing heartbeat status logs in memory
1 parent ac48085 commit a81e9b8

4 files changed

Lines changed: 68 additions & 2 deletions

File tree

internal/agent/tools/memory.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,10 @@ func (t *EditMemoryTool) Execute(ctx context.Context, args map[string]interface{
155155
return "", fmt.Errorf("edit_memory: 'old_text' argument required")
156156
}
157157
newText, _ := args["new_text"].(string) // defaults to "" (deletion) if absent
158+
if isHeartbeatContent(newText) {
159+
// return "", fmt.Errorf("edit_memory: heartbeat status logs must not be stored in memory — skip this edit")
160+
return "", nil // skip sliently
161+
}
158162

159163
name, err := resolveMemoryTarget(target)
160164
if err != nil {

internal/agent/tools/write_memory.go

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,35 @@ package tools
33
import (
44
"context"
55
"fmt"
6+
"strings"
67

78
"github.com/local/picobot/internal/agent/memory"
89
)
910

11+
// heartbeatPhrases contains lowercase substrings that identify heartbeat-status
12+
// log entries. Any write_memory or edit_memory call whose content matches one
13+
// of these is rejected — the heartbeat loop must never pollute memory files.
14+
var heartbeatPhrases = []string{
15+
"heartbeat check",
16+
"heartbeat.md",
17+
"heartbeat log",
18+
"system status: healthy",
19+
"no pending tasks",
20+
"no actions required",
21+
"periodic tasks",
22+
}
23+
24+
// isHeartbeatContent returns true if content looks like a heartbeat status log.
25+
func isHeartbeatContent(content string) bool {
26+
l := strings.ToLower(content)
27+
for _, phrase := range heartbeatPhrases {
28+
if strings.Contains(l, phrase) {
29+
return true
30+
}
31+
}
32+
return false
33+
}
34+
1035
// WriteMemoryTool writes to the agent's memory (today's note or long-term MEMORY.md)
1136
type WriteMemoryTool struct {
1237
mem *memory.MemoryStore
@@ -18,7 +43,7 @@ func NewWriteMemoryTool(mem *memory.MemoryStore) *WriteMemoryTool {
1843

1944
func (w *WriteMemoryTool) Name() string { return "write_memory" }
2045
func (w *WriteMemoryTool) Description() string {
21-
return "Write or append to memory (today's note or long-term MEMORY.md)"
46+
return "Write or append to memory (today's note or long-term MEMORY.md). NEVER store heartbeat status, health checks, or 'no pending tasks' results."
2247
}
2348

2449
func (w *WriteMemoryTool) Parameters() map[string]interface{} {
@@ -63,6 +88,9 @@ func (w *WriteMemoryTool) Execute(ctx context.Context, args map[string]interface
6388
if !ok {
6489
return "", fmt.Errorf("write_memory: 'content' must be a string")
6590
}
91+
if isHeartbeatContent(content) {
92+
return "", fmt.Errorf("write_memory: heartbeat status logs must not be stored in memory — skip this write")
93+
}
6694
appendFlag := true
6795
if a, ok := args["append"]; ok {
6896
if b, ok := a.(bool); ok {
@@ -96,3 +124,4 @@ func (w *WriteMemoryTool) Execute(ctx context.Context, args map[string]interface
96124
return "", fmt.Errorf("write_memory: unknown target '%s'", target)
97125
}
98126
}
127+

internal/agent/tools/write_memory_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,28 @@ func TestWriteMemoryTool_TodayAndLong(t *testing.T) {
4545
t.Fatalf("expected LT1 to be gone after overwrite, got %q", lt2)
4646
}
4747
}
48+
49+
func TestWriteMemoryTool_RejectsHeartbeatContent(t *testing.T) {
50+
tmp := t.TempDir()
51+
mem := memory.NewMemoryStoreWithWorkspace(tmp, 10)
52+
w := NewWriteMemoryTool(mem)
53+
54+
cases := []string{
55+
"HEARTBEAT CHECK: system status: HEALTHY",
56+
"Reviewed HEARTBEAT.md. No pending tasks. System status: HEALTHY. No actions required.",
57+
"heartbeat check complete",
58+
"[2026-03-07T00:12:36Z] HEARTBEAT CHECK at 2026-03-06: no pending tasks",
59+
}
60+
for _, c := range cases {
61+
_, err := w.Execute(context.Background(), map[string]interface{}{"target": "today", "content": c})
62+
if err == nil {
63+
t.Fatalf("expected heartbeat content to be rejected, but it was accepted: %q", c)
64+
}
65+
}
66+
67+
// Confirm that legitimate content is still accepted.
68+
_, err := w.Execute(context.Background(), map[string]interface{}{"target": "today", "content": "user prefers dark mode"})
69+
if err != nil {
70+
t.Fatalf("expected legitimate content to be accepted, got: %v", err)
71+
}
72+
}

internal/config/onboard.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,8 @@ Never create files directly in the workspace root. Always use a project folder.
118118
- Use list_memory to see all available memory files
119119
- Use delete_memory to clean up outdated daily notes
120120
- Do NOT just say you'll remember something — actually call write_memory
121-
- Do NOT use write_memory tool for redundant information like heartbeat logs
121+
- NEVER write heartbeat results, health checks, or periodic status logs to memory — these are ephemeral and must be discarded after each run
122+
- Memory is for durable user knowledge only: facts, preferences, project notes, decisions
122123
123124
## Skills
124125
@@ -270,6 +271,13 @@ Schedule or manage cron jobs.
270271
271272
This file is checked periodically (every 60 seconds). Add tasks here that should run on a schedule.
272273
274+
## IMPORTANT RULES FOR HEARTBEAT PROCESSING
275+
276+
- After reviewing this file, take actions ONLY if there are explicit tasks listed below
277+
- If there are no tasks (or all tasks are complete), do NOTHING — do not send any message, do not call write_memory or any memory tool
278+
- NEVER log "heartbeat check complete", "system status: healthy", or any status message to memory files — these clutter memory with useless noise
279+
- Heartbeat results are ephemeral: process, act if needed, then stop silently
280+
273281
## Periodic Tasks
274282
275283
<!-- Add tasks below. The agent will process them on each heartbeat check. -->

0 commit comments

Comments
 (0)