Class: KairosMcp::Daemon
- Inherits:
-
Object
- Object
- KairosMcp::Daemon
- Defined in:
- lib/kairos_mcp/daemon.rb,
lib/kairos_mcp/daemon/wal.rb,
lib/kairos_mcp/daemon/budget.rb,
lib/kairos_mcp/daemon/chronos.rb,
lib/kairos_mcp/daemon/planner.rb,
lib/kairos_mcp/daemon/pid_lock.rb,
lib/kairos_mcp/daemon/task_dag.rb,
lib/kairos_mcp/daemon/canonical.rb,
lib/kairos_mcp/daemon/heartbeat.rb,
lib/kairos_mcp/daemon/pdf_build.rb,
lib/kairos_mcp/daemon/credentials.rb,
lib/kairos_mcp/daemon/edit_kernel.rb,
lib/kairos_mcp/daemon/integration.rb,
lib/kairos_mcp/daemon/code_gen_act.rb,
lib/kairos_mcp/daemon/wal_recovery.rb,
lib/kairos_mcp/daemon/approval_gate.rb,
lib/kairos_mcp/daemon/attach_server.rb,
lib/kairos_mcp/daemon/daemon_policy.rb,
lib/kairos_mcp/daemon/active_observe.rb,
lib/kairos_mcp/daemon/signal_handler.rb,
lib/kairos_mcp/daemon/command_mailbox.rb,
lib/kairos_mcp/daemon/elevation_token.rb,
lib/kairos_mcp/daemon/mandate_factory.rb,
lib/kairos_mcp/daemon/proposal_routes.rb,
lib/kairos_mcp/daemon/policy_elevation.rb,
lib/kairos_mcp/daemon/restricted_shell.rb,
lib/kairos_mcp/daemon/scope_classifier.rb,
lib/kairos_mcp/daemon/daemon_llm_caller.rb,
lib/kairos_mcp/daemon/execution_context.rb,
lib/kairos_mcp/daemon/idempotency_check.rb,
lib/kairos_mcp/daemon/ooda_cycle_runner.rb,
lib/kairos_mcp/daemon/wal_phase_recorder.rb,
lib/kairos_mcp/daemon/llm_phase_functions.rb,
lib/kairos_mcp/daemon/code_gen_phase_handler.rb,
lib/kairos_mcp/daemon/restricted_shell/errors.rb,
lib/kairos_mcp/daemon/restricted_shell/runner.rb,
lib/kairos_mcp/daemon/idempotent_chain_recorder.rb,
lib/kairos_mcp/daemon/restricted_shell/argv_validators.rb,
lib/kairos_mcp/daemon/restricted_shell/binary_resolver.rb,
lib/kairos_mcp/daemon/restricted_shell/sandbox_context.rb,
lib/kairos_mcp/daemon/restricted_shell/sandbox_factory.rb
Overview
KairosChain long-running daemon (Phase 2 P2.1 skeleton).
Responsibilities (design v0.2 §3.1):
1. Acquire a single-instance PID lock (flock) at .kairos/run/daemon.pid.
2. Install signal handlers (TERM/INT/HUP/USR1).
3. Load config and initialize the logger.
4. Build Safety in :daemon mode (DaemonPolicy — see §3.1.x / CF-4).
5. Run a single-threaded event loop that:
drain_mailbox → chronos.tick → mandate queue → OODA cycle → heartbeat → sleep
6. Handle graceful shutdown within `graceful_timeout` seconds.
For P2.1, Chronos and OODA integration points are stubs — the loop just logs a ‘no_mandates` heartbeat. Real integration lands in later P2 tickets.
Defined Under Namespace
Modules: Canonical, DaemonPolicy, EditKernel, ExecutionContext, IdempotencyCheck, Integration, LlmPhaseFunctions, MandateFactory, PidLock, Planner, PolicyElevation, ProposalRoutes, ScopeClassifier, SignalHandler, WalRecovery Classes: ActiveObserve, ApprovalGate, AttachServer, Budget, Chronos, CodeGenAct, CodeGenPhaseHandler, CommandMailbox, Credentials, DaemonLlmCaller, ElevationToken, Heartbeat, IdempotentChainRecorder, OodaCycleRunner, PdfBuild, RestrictedShell, TaskDag, WAL, WalPhaseRecorder
Constant Summary collapse
- DEFAULT_CONFIG_PATH =
'.kairos/config/daemon.yml'- DEFAULT_PID_PATH =
'.kairos/run/daemon.pid'- DEFAULT_TICK_INTERVAL =
seconds (design §3.2)
10- DEFAULT_GRACEFUL_TIMEOUT =
seconds (design §3.4)
120- STATES =
Lifecycle states — mostly used by tests and status dumps.
%i[initialized starting running draining stopped error].freeze
Instance Attribute Summary collapse
-
#config ⇒ Object
readonly
Returns the value of attribute config.
-
#logger ⇒ Object
readonly
Returns the value of attribute logger.
-
#mailbox ⇒ Object
readonly
Returns the value of attribute mailbox.
-
#started_at ⇒ Object
readonly
Returns the value of attribute started_at.
-
#state ⇒ Object
readonly
Returns the value of attribute state.
-
#tick_count ⇒ Object
readonly
Returns the value of attribute tick_count.
Instance Method Summary collapse
-
#chronos_tick ⇒ Object
Chronos tick — real implementation lands in a later P2 ticket.
-
#dispatch_command(cmd) ⇒ Object
—————————————————————— commands.
-
#event_loop ⇒ Object
—————————————————————— event loop.
-
#handle_reload(_cmd) ⇒ Object
CF-7 fix: keep old config on reload failure instead of falling back to defaults.
- #handle_status_dump(_cmd) ⇒ Object
-
#initialize(config_path: nil, root: Dir.pwd, logger: nil, sleeper: nil, clock: nil) ⇒ Daemon
constructor
A new instance of Daemon.
- #request_reload! ⇒ Object
-
#request_shutdown!(signal = nil) ⇒ Object
Called from SignalHandler.handle — must be async-signal-safe.
- #request_status_dump! ⇒ Object
-
#run ⇒ Object
Start the daemon: acquire lock, install signals, enter event loop.
-
#run_one_ooda_cycle ⇒ Object
Mandate queue → OODA — stub for P2.1.
- #shutdown_requested? ⇒ Boolean
-
#start! ⇒ Object
Initialize but do NOT enter the event loop — useful for tests that want to drive ‘tick_once` manually.
-
#status_snapshot ⇒ Object
—————————————————————— helpers.
-
#stop! ⇒ Object
Release lock and uninstall signal handlers.
-
#tick_once ⇒ Object
One iteration of the event loop.
Constructor Details
#initialize(config_path: nil, root: Dir.pwd, logger: nil, sleeper: nil, clock: nil) ⇒ Daemon
Returns a new instance of Daemon.
44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
# File 'lib/kairos_mcp/daemon.rb', line 44 def initialize(config_path: nil, root: Dir.pwd, logger: nil, sleeper: nil, clock: nil) @root = root @config_path = config_path || File.join(@root, DEFAULT_CONFIG_PATH) @config = load_config(@config_path) @logger = logger || KairosMcp.logger @mailbox = CommandMailbox.new @sleeper = sleeper || ->(s) { sleep(s) } @clock = clock || -> { Time.now.utc } @pid_file = nil @pid_path = resolve_path(@config['pid_path'] || DEFAULT_PID_PATH) @tick_interval = Float(@config['tick_interval'] || DEFAULT_TICK_INTERVAL) @graceful_timeout = Float(@config['graceful_timeout'] || DEFAULT_GRACEFUL_TIMEOUT) @shutdown_requested = false @shutdown_signal = nil @reload_requested = false @status_dump_requested = false @state = :initialized @tick_count = 0 @started_at = nil @safety = nil end |
Instance Attribute Details
#config ⇒ Object (readonly)
Returns the value of attribute config.
36 37 38 |
# File 'lib/kairos_mcp/daemon.rb', line 36 def config @config end |
#logger ⇒ Object (readonly)
Returns the value of attribute logger.
36 37 38 |
# File 'lib/kairos_mcp/daemon.rb', line 36 def logger @logger end |
#mailbox ⇒ Object (readonly)
Returns the value of attribute mailbox.
36 37 38 |
# File 'lib/kairos_mcp/daemon.rb', line 36 def mailbox @mailbox end |
#started_at ⇒ Object (readonly)
Returns the value of attribute started_at.
36 37 38 |
# File 'lib/kairos_mcp/daemon.rb', line 36 def started_at @started_at end |
#state ⇒ Object (readonly)
Returns the value of attribute state.
36 37 38 |
# File 'lib/kairos_mcp/daemon.rb', line 36 def state @state end |
#tick_count ⇒ Object (readonly)
Returns the value of attribute tick_count.
36 37 38 |
# File 'lib/kairos_mcp/daemon.rb', line 36 def tick_count @tick_count end |
Instance Method Details
#chronos_tick ⇒ Object
Chronos tick — real implementation lands in a later P2 ticket. For P2.1 this is a no-op logged at debug level.
271 272 273 |
# File 'lib/kairos_mcp/daemon.rb', line 271 def chronos_tick @logger.debug('daemon_chronos_stub', source: 'daemon') end |
#dispatch_command(cmd) ⇒ Object
—————————————————————— commands
225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 |
# File 'lib/kairos_mcp/daemon.rb', line 225 def dispatch_command(cmd) case cmd[:type] when :shutdown request_shutdown!('mailbox') when :reload handle_reload(cmd) when :status_dump handle_status_dump(cmd) else @logger.debug('daemon_command_unknown', source: 'daemon', details: { type: cmd[:type].to_s, id: cmd[:id] }) end rescue StandardError => e @logger.error('daemon_command_failed', source: 'daemon', details: { type: cmd[:type].to_s, error: e. }) end |
#event_loop ⇒ Object
—————————————————————— event loop
150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 |
# File 'lib/kairos_mcp/daemon.rb', line 150 def event_loop shutdown_deadline = nil until @state == :stopped if @shutdown_requested && shutdown_deadline.nil? shutdown_deadline = @clock.call + @graceful_timeout @logger.info('daemon_shutdown_begin', source: 'daemon', details: { signal: @shutdown_signal, deadline: shutdown_deadline.iso8601 }) end if shutdown_deadline && @clock.call >= shutdown_deadline @logger.warn('daemon_graceful_timeout_exceeded', source: 'daemon', details: { timeout: @graceful_timeout }) break end tick_once # If shutdown was requested, there's nothing useful left to do once # the mailbox has been drained. break if @shutdown_requested && @mailbox.empty? @sleeper.call(@tick_interval) end end |
#handle_reload(_cmd) ⇒ Object
CF-7 fix: keep old config on reload failure instead of falling back to defaults.
245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 |
# File 'lib/kairos_mcp/daemon.rb', line 245 def handle_reload(_cmd) new_config = safe_load_config(@config_path) if new_config @config = new_config @tick_interval = Float(@config['tick_interval'] || DEFAULT_TICK_INTERVAL) @graceful_timeout = Float(@config['graceful_timeout'] || DEFAULT_GRACEFUL_TIMEOUT) @logger.info('daemon_config_reloaded', source: 'daemon', details: { tick_interval: @tick_interval }) else @logger.error('daemon_config_reload_failed', source: 'daemon', details: { note: 'keeping previous config' }) end end |
#handle_status_dump(_cmd) ⇒ Object
261 262 263 264 265 |
# File 'lib/kairos_mcp/daemon.rb', line 261 def handle_status_dump(_cmd) @logger.info('daemon_status', source: 'daemon', details: status_snapshot) end |
#request_reload! ⇒ Object
136 137 138 |
# File 'lib/kairos_mcp/daemon.rb', line 136 def request_reload! @reload_requested = true end |
#request_shutdown!(signal = nil) ⇒ Object
Called from SignalHandler.handle — must be async-signal-safe. CF-2 fix: only set simple flags, no allocations in signal context.
131 132 133 134 |
# File 'lib/kairos_mcp/daemon.rb', line 131 def request_shutdown!(signal = nil) @shutdown_requested = true @shutdown_signal = signal end |
#request_status_dump! ⇒ Object
140 141 142 |
# File 'lib/kairos_mcp/daemon.rb', line 140 def request_status_dump! @status_dump_requested = true end |
#run ⇒ Object
Start the daemon: acquire lock, install signals, enter event loop. This is the method ‘bin/kairos-daemon` invokes.
73 74 75 76 77 78 |
# File 'lib/kairos_mcp/daemon.rb', line 73 def run start! event_loop ensure stop! end |
#run_one_ooda_cycle ⇒ Object
Mandate queue → OODA — stub for P2.1.
276 277 278 279 280 |
# File 'lib/kairos_mcp/daemon.rb', line 276 def run_one_ooda_cycle @logger.debug('daemon_ooda_stub', source: 'daemon', details: { note: 'no mandates' }) end |
#shutdown_requested? ⇒ Boolean
144 145 146 |
# File 'lib/kairos_mcp/daemon.rb', line 144 def shutdown_requested? @shutdown_requested end |
#start! ⇒ Object
Initialize but do NOT enter the event loop — useful for tests that want to drive ‘tick_once` manually.
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
# File 'lib/kairos_mcp/daemon.rb', line 82 def start! return if @state == :running @state = :starting @pid_file = PidLock.acquire!(@pid_path) SignalHandler.install(self) @safety = build_safety @started_at = @clock.call @state = :running @logger.info('daemon_started', source: 'daemon', details: { pid: Process.pid, pid_path: @pid_path, tick_interval: @tick_interval, graceful_timeout: @graceful_timeout }) end |
#status_snapshot ⇒ Object
—————————————————————— helpers
284 285 286 287 288 289 290 291 292 293 294 295 |
# File 'lib/kairos_mcp/daemon.rb', line 284 def status_snapshot { state: @state.to_s, pid: Process.pid, tick_count: @tick_count, mailbox_size: @mailbox.size, started_at: @started_at&.iso8601, shutdown_requested: @shutdown_requested, tick_interval: @tick_interval, graceful_timeout: @graceful_timeout } end |
#stop! ⇒ Object
Release lock and uninstall signal handlers. Safe to call twice.
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 |
# File 'lib/kairos_mcp/daemon.rb', line 102 def stop! return if @state == :stopped previous_state = @state @state = :draining # Release PID lock FIRST so a restart isn't blocked by our cleanup. PidLock.release(@pid_file, @pid_path) @pid_file = nil SignalHandler.uninstall @state = :stopped # Avoid noise if we never successfully started. return if previous_state == :initialized @logger.info('daemon_stopped', source: 'daemon', details: { signal: @shutdown_signal, tick_count: @tick_count }) rescue StandardError => e @state = :error # Logging errors must not crash shutdown. warn "[kairos-daemon] stop! failed: #{e.class}: #{e.}" end |
#tick_once ⇒ Object
One iteration of the event loop. Exposed for tests.
180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 |
# File 'lib/kairos_mcp/daemon.rb', line 180 def tick_once @tick_count += 1 # CF-2 fix: translate signal flags into mailbox commands (no allocations in trap). if @reload_requested @reload_requested = false @mailbox.enqueue(:reload, signal: 'HUP') end if @status_dump_requested @status_dump_requested = false @mailbox.enqueue(:status_dump, signal: 'USR1') end # 1. Drain the command mailbox BEFORE doing work (design §CF-2). drained = @mailbox.drain drained.each { |cmd| dispatch_command(cmd) } # CF-5 fix: error boundary around each work phase. # Exceptions in one phase must not kill the daemon. begin # 2. Chronos tick (stub in P2.1). chronos_tick rescue StandardError => e @logger.error('daemon_chronos_tick_failed', source: 'daemon', details: { error: "#{e.class}: #{e.}" }) end begin # 3. Mandate queue → OODA cycle (stub in P2.1). run_one_ooda_cycle rescue StandardError => e @logger.error('daemon_ooda_cycle_failed', source: 'daemon', details: { error: "#{e.class}: #{e.}" }) end # 4. Heartbeat. @logger.debug('daemon_heartbeat', source: 'daemon', details: { tick: @tick_count, mailbox_size: @mailbox.size, state: @state.to_s }) end |