Skip to content

feat: opt-in smart heartbeat that only fires when session state changes#592

Open
c2keesey wants to merge 17 commits intoasheshgoplani:mainfrom
c2keesey:feat/smart-heartbeat-v2
Open

feat: opt-in smart heartbeat that only fires when session state changes#592
c2keesey wants to merge 17 commits intoasheshgoplani:mainfrom
c2keesey:feat/smart-heartbeat-v2

Conversation

@c2keesey
Copy link
Copy Markdown

Summary

Adds a heartbeat_smart flag under [conductor] settings. When enabled, the per-conductor heartbeat.sh installed by agent-deck conductor setup snapshots session state on each tick and only sends a check-in to the conductor when the snapshot differs from the previous run.

The default is false, so behavior is unchanged unless the user opts in.

[conductor]
heartbeat_smart = true   # default: false

Closes #545 (this PR replaces that earlier, rougher attempt).

Motivation

On profiles where the conductor is mostly idle, the timer-driven heartbeat wakes the conductor every N minutes just to have it report "all clear". For users running the conductor against expensive models (or who just want quieter tmux output), that's wasted work. With heartbeat_smart = true, the heartbeat tick is a cheap shell snapshot when nothing has moved, and the conductor is only woken when there's actually something to look at.

How it works

The smart variant of heartbeat.sh:

  1. Same conductor status enabled-guard as the standard script.
  2. Snapshots the current set of title:status pairs from agent-deck list --json, sorted, with the conductor's own session filtered out. Parsing uses portable awk (works on GNU and BSD/macOS), matching the parser style used by the existing heartbeat script.
  3. Compares against the previous snapshot in heartbeat.state (an absolute path interpolated next to heartbeat.sh at install time, so the script doesn't depend on $0).
  4. If they match, exits 0.
  5. If they differ, writes the new snapshot atomically (tmp + mv) before sending, so a crash mid-send can't cause a re-fire loop.
  6. Same idle/waiting status gate and same group-scoped check-in message as the standard heartbeat.

Migration

MigrateConductorHeartbeatScripts honours the same setting, so flipping heartbeat_smart and re-running agent-deck conductor setup (or letting the migration sweep run on next start) refreshes existing managed scripts in place. The managed-script detector recognises both the standard # Heartbeat for conductor: header and the new # Smart heartbeat for conductor: header, so neither variant is mistaken for a user-authored custom script.

Setup output

  [ok] Heartbeat timer installed (every 15 min, smart)

When heartbeat_smart = false (the default), the existing (every 15 min) line is unchanged.

Tests

8 new tests in internal/session/conductor_test.go:

  • TestConductorSmartHeartbeatScript_TemplateStructure — smart template contains the diffing comparison, atomic state write, status guard, and group-scoped message.
  • TestInstallHeartbeatScript_StandardWritesScriptWithoutSmartMarkersInstallHeartbeatScript(name, "default", false) writes the standard script via t.Setenv("HOME", tmpHome), strips -p for the default profile, and contains no smart markers.
  • TestInstallHeartbeatScript_SmartInterpolatesStateFileAndPreservesProfile — smart install interpolates the absolute heartbeat.state path, leaves no {STATE_FILE} placeholder, keeps -p for non-default profiles, and produces an executable file.
  • TestInstallHeartbeatScript_SmartDefaultProfileStripsFlag — smart install with the default profile still strips -p.
  • TestInstallHeartbeatScript_SmartScriptIsExecutableShell — runs bash -n against the rendered smart script so awk/quoting regressions surface at go test time.
  • TestConductorSettings_HeartbeatSmartDefaultHeartbeatSmart defaults to false.
  • TestMigrateConductorHeartbeatScripts_DetectsSmartHeader — both header variants are recognised by the migration sweep's managed-script check.

All existing conductor tests still pass.

Test plan

  • go test ./internal/session/ -run "Heartbeat|Smart|Conductor" -count=1 — passes
  • go build ./... — passes
  • gofmt clean
  • bash -n syntax check on rendered smart script (covered by test above)
  • Manual: install with heartbeat_smart = true, observe heartbeat.state populating and conductor only being poked on real state changes

asheshgoplani and others added 17 commits March 18, 2026 18:28
…hgoplani#366)

Check existing terminal-features before appending to avoid duplicates
that balloon the list to 260+ entries over multiple session starts.

Committed by Ashesh Goplani
When the name field is left empty, a random adjective-noun name (e.g.,
"golden-eagle") is shown as a dimmed placeholder and used on submit.
The worktree branch placeholder also reflects the generated name.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Branch shows as dimmed placeholder (not filled input) when using
  generated name; only fills when user types a custom name
- Align Validate() and GetValuesWithWorktree() branch derivation logic
- Add tests for generated name fallback, branch placeholder behavior,
  and worktree branch derivation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…nal-features-spam

Fix asheshgoplani#366: prevent terminal-features spam by checking before appending
Feat: auto-generate session names as placeholders when name field is empty
…-happy

Feat: add happy wrapper support for Claude and Codex (opt-in via use_happy config)
…ranch

Feat: worktree branch reuse (local+remote) and fzf branch picker via Ctrl+F
…key input

The extended-keys option was set server-wide (`set -sq`), which caused
tmux to activate xterm modifyOtherKeys mode on the outer terminal
(iTerm2, etc.). This persisted even after the tmux option was turned off,
causing Ctrl+R and other modified keys to be sent as escape sequences
that Bubble Tea cannot parse — breaking the recent sessions picker and
other Ctrl-key shortcuts in the dashboard.

Two fixes:
- tmux.go: changed `set -sq extended-keys on` to per-session
  `set-option -t <session> -q extended-keys on` at both call sites
- keyboard_compat.go: also disable xterm modifyOtherKeys (ESC[>4;0m)
  on TUI startup alongside the existing Kitty protocol disable, as a
  defense-in-depth measure

Fixes regression introduced in b427418 (asheshgoplani#342).
# Conflicts:
#	internal/git/git.go
#	internal/git/git_test.go
#	internal/session/instance.go
#	internal/tmux/tmux.go
#	internal/ui/branch_picker.go
#	internal/ui/claudeoptions.go
#	internal/ui/forkdialog.go
#	internal/ui/forkdialog_test.go
#	internal/ui/home.go
#	internal/ui/keyboard_compat.go
#	internal/ui/keyboard_compat_test.go
#	internal/ui/newdialog.go
#	internal/ui/settings_panel.go
#	skills/agent-deck/references/config-reference.md
Adds a `heartbeat_smart` flag under `[conductor]` settings. When enabled,
the per-conductor heartbeat.sh installed by `agent-deck conductor setup`
snapshots session state on each tick and only forwards a check-in to the
conductor session when the snapshot differs from the previous run. The
default remains unchanged (every tick fires a heartbeat) so behavior is
backward compatible.

The state snapshot is the sorted set of "title:status" pairs from
`agent-deck list --json`, excluding the conductor session itself, parsed
with portable awk that works on both GNU and BSD. Snapshots are written
atomically to `heartbeat.state` next to `heartbeat.sh`.

`MigrateConductorHeartbeatScripts` now respects the same setting, so
toggling `heartbeat_smart` and rerunning conductor setup (or letting the
migration sweep run) refreshes existing managed scripts in place. The
managed-script detector recognizes both header variants.

Tests cover:

- Smart template structure (state diffing, atomic write, status guard,
  group-scoped message)
- `InstallHeartbeatScript(..., false)` writes the standard script and
  strips `-p` for the default profile
- `InstallHeartbeatScript(..., true)` interpolates an absolute state
  file path, preserves `-p` for non-default profiles, and emits an
  executable file
- `bash -n` syntax check on the rendered smart script so awk/quoting
  regressions surface at `go test` time
- `HeartbeatSmart` defaults to false
- Migration sweep recognizes the smart header
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants