Class: KairosMcp::Daemon::Heartbeat
- Inherits:
-
Object
- Object
- KairosMcp::Daemon::Heartbeat
- Defined in:
- lib/kairos_mcp/daemon/heartbeat.rb
Overview
Heartbeat — P2.8 liveness beacon.
Design (v0.2 P2.8):
* At most once per `interval` seconds, the daemon writes a small JSON
file at .kairos/run/heartbeat.json with current liveness fields.
* External monitors (health checks, attach clients, ops scripts)
stat+read this file to answer "is the daemon alive, and when did
it last complete a cycle?"
Atomicity:
Writes go tmp → rename. A torn write leaves either the previous
heartbeat or the new one intact — never a half-written JSON.
Decoupling:
`emit(daemon)` duck-types the daemon. Required: `#status_snapshot`
(for pid + tick_count). Optional: `#active_mandate_id`,
`#queue_depth`, `#last_cycle_at`. Missing optionals default to
nil / 0 — this keeps Heartbeat testable without the full daemon.
Constant Summary collapse
- DEFAULT_PATH =
'.kairos/run/heartbeat.json'- DEFAULT_INTERVAL =
seconds
10
Instance Attribute Summary collapse
-
#path ⇒ Object
readonly
Returns the value of attribute path.
Instance Method Summary collapse
-
#emit(daemon) ⇒ Object
Emit a heartbeat right now.
-
#emit_if_due(daemon, last_emit_at, interval: DEFAULT_INTERVAL) ⇒ Object
Rate-limited variant.
-
#initialize(path: DEFAULT_PATH, clock: nil) ⇒ Heartbeat
constructor
A new instance of Heartbeat.
-
#read ⇒ Object
Parse and return the current heartbeat.json, or nil if absent / unparseable.
Constructor Details
#initialize(path: DEFAULT_PATH, clock: nil) ⇒ Heartbeat
Returns a new instance of Heartbeat.
35 36 37 38 |
# File 'lib/kairos_mcp/daemon/heartbeat.rb', line 35 def initialize(path: DEFAULT_PATH, clock: nil) @path = path @clock = clock || -> { Time.now.utc } end |
Instance Attribute Details
#path ⇒ Object (readonly)
Returns the value of attribute path.
31 32 33 |
# File 'lib/kairos_mcp/daemon/heartbeat.rb', line 31 def path @path end |
Instance Method Details
#emit(daemon) ⇒ Object
Emit a heartbeat right now. Returns the Time it was emitted at.
41 42 43 44 45 46 |
# File 'lib/kairos_mcp/daemon/heartbeat.rb', line 41 def emit(daemon) now = @clock.call payload = build_payload(daemon, now) write_atomic(payload) now end |
#emit_if_due(daemon, last_emit_at, interval: DEFAULT_INTERVAL) ⇒ Object
Rate-limited variant. If ‘interval` seconds have not yet elapsed since `last_emit_at`, do nothing and return `last_emit_at`. Otherwise emit and return the new emit time.
‘last_emit_at` may be nil (first call) — in that case we emit.
53 54 55 56 57 58 59 60 |
# File 'lib/kairos_mcp/daemon/heartbeat.rb', line 53 def emit_if_due(daemon, last_emit_at, interval: DEFAULT_INTERVAL) now = @clock.call return last_emit_at if last_emit_at && (now - last_emit_at) < interval payload = build_payload(daemon, now) write_atomic(payload) now end |
#read ⇒ Object
Parse and return the current heartbeat.json, or nil if absent / unparseable. Never raises.
64 65 66 67 68 69 70 |
# File 'lib/kairos_mcp/daemon/heartbeat.rb', line 64 def read return nil unless File.exist?(@path) JSON.parse(File.read(@path)) rescue StandardError nil end |