Problem
The heartbeat system treats every tick the same regardless of how long the user has been away. After a 3-day absence, the first tick runs the normal pipeline: confluence scoring, cooldown checks, fingerprint dedup. This means the user might get a single low-priority nudge instead of a comprehensive "here's what happened while you were gone" briefing.
The system already tracks session_messages (table in sqlite_migrate.go) with timestamps, so detecting absence is straightforward.
Proposal
Detection
Before the normal signal evaluation in evaluateShouldAct(), check for absence:
func (k *Keyoku) detectAbsence(ctx context.Context, entityID string) (bool, time.Duration) {
// Query session_messages for most recent user message
// SELECT MAX(created_at) FROM session_messages
// WHERE entity_id = ? AND role = 'user'
lastActivity := ...
gap := time.Since(lastActivity)
return gap > 24*time.Hour, gap
}
Re-entry mode
When absence > 24h, bypass the normal decision pipeline and enter re-entry mode:
- Skip confluence threshold, cooldown, fingerprint dedup, and fatigue checks
- Collect ALL active signals (run all 14
HeartbeatCheckType checks)
- Sort by tier priority (
tierPriority map: Immediate=0, Elevated=1, Normal=2, Low=3)
- Include positive deltas (
CheckPositiveDeltas) to balance bad news
- Set
DecisionReason = "reentry" on the HeartbeatResult
- Set
ShouldAct = true with EscalationLevel = 0 (fresh start, not nagging)
- If LLM analysis is enabled (
cfg.llmProvider != nil), call LLM once with all signals to generate a prioritized briefing summary
- Include absence duration in result: "You've been away for 3 days"
Format hint
Add a field to HeartbeatResult:
AbsenceDuration *time.Duration // nil if not a re-entry tick
The plugin's formatHeartbeatContext() in context.ts can use this to switch to a briefing format.
Files to modify
heartbeat_decide.go — add detectAbsence(), add re-entry branch at top of evaluateShouldAct() (before first-contact check)
heartbeat.go — add AbsenceDuration field to HeartbeatResult
storage/sqlite_heartbeat.go — add GetLastUserActivity() query against session_messages
Plugin side (separate PR in keyoku repo)
packages/openclaw/src/context.ts — add re-entry formatting path in formatHeartbeatContext()
packages/types/src/memory.ts — add absence_duration_ms?: number to HeartbeatContextResult
Constraints
- Only triggers once per absence (after re-entry tick, normal pipeline resumes)
- Absence threshold: 24h (configurable via
HeartbeatParams)
- Re-entry tick should NOT reset fatigue counters (user hasn't acted yet)
- If no signals at all after absence, send a simple "Welcome back, nothing happened" instead of silence
Problem
The heartbeat system treats every tick the same regardless of how long the user has been away. After a 3-day absence, the first tick runs the normal pipeline: confluence scoring, cooldown checks, fingerprint dedup. This means the user might get a single low-priority nudge instead of a comprehensive "here's what happened while you were gone" briefing.
The system already tracks
session_messages(table insqlite_migrate.go) with timestamps, so detecting absence is straightforward.Proposal
Detection
Before the normal signal evaluation in
evaluateShouldAct(), check for absence:Re-entry mode
When absence > 24h, bypass the normal decision pipeline and enter re-entry mode:
HeartbeatCheckTypechecks)tierPrioritymap: Immediate=0, Elevated=1, Normal=2, Low=3)CheckPositiveDeltas) to balance bad newsDecisionReason = "reentry"on theHeartbeatResultShouldAct = truewithEscalationLevel = 0(fresh start, not nagging)cfg.llmProvider != nil), call LLM once with all signals to generate a prioritized briefing summaryFormat hint
Add a field to
HeartbeatResult:The plugin's
formatHeartbeatContext()incontext.tscan use this to switch to a briefing format.Files to modify
heartbeat_decide.go— adddetectAbsence(), add re-entry branch at top ofevaluateShouldAct()(before first-contact check)heartbeat.go— addAbsenceDurationfield toHeartbeatResultstorage/sqlite_heartbeat.go— addGetLastUserActivity()query againstsession_messagesPlugin side (separate PR in keyoku repo)
packages/openclaw/src/context.ts— add re-entry formatting path informatHeartbeatContext()packages/types/src/memory.ts— addabsence_duration_ms?: numbertoHeartbeatContextResultConstraints
HeartbeatParams)