Repository navigation
vcli-daemon: long-running process that wires IPC + store + runtime (plan 4) - #10
Merged
Merged
Conversation
Wall-clock ms convenience for daemon callers that do not hold a Clock handle. Equivalent to SystemClock::new().unix_ms(). Re-exported from the crate root. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ordered by submitted_at ASC then rowid for stable output. Unit tests cover both the no-filter and filtered paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
i64::try_from(i).unwrap() for the usize→i64 cast. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds crates/vcli-daemon as a workspace member, a lib + placeholder bin, and the workspace dependencies (tracing-subscriber, tracing-appender, fs2, dirs, signal-hook-tokio, futures-util) the later tasks consume. Also extends tokio feature set with `signal`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
thiserror-based enum covering startup, pidfile, paths, logging, store, ipc, io, and invalid-program branches. `code()` maps each to a stable vcli_core::ErrorCode (InvalidProgram → InvalidProgram, rest → Internal). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
from_platform_defaults() resolves data_root / log_dir per platform (Library on macOS, XDG on Linux) and socket via vcli_ipc. with_roots() gives tests and alt installs explicit overrides. ensure_dirs() idempotently mkdir -p's data_root and log_dir. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PidFile::acquire opens <path>, takes fs2 exclusive lock, writes current PID. Collision returns DaemonError::AlreadyRunning with the owning PID read from the file. release() consumes self, unlocks + unlinks; Drop covers the panic / fast-exit path the same way. Stale PIDs after SIGKILL are handled automatically: the kernel releases the flock on fd close, so the next daemon acquires cleanly without needing a kill-check. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
logging::init(log_dir) installs a JSON tracing subscriber whose file writer is tracing-appender's daily roller (`daemon.log.YYYY-MM-DD`). Respects RUST_LOG via EnvFilter; defaults to info. Returns a LogGuard the caller must keep alive to avoid losing buffered records. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
new_channels() returns a bundle of endpoints: a bounded crossbeam cmd_tx for tokio→scheduler commands, an unbounded event_rx/sched_event_tx pair for scheduler→tokio events, and a tokio broadcast::Sender (capacity 1024) for fanning persisted events to IPC subscribers. Also adds build.rs with the same macOS libswift_Concurrency rpath vcli-runtime uses — required because vcli-daemon pulls in vcli-capture transitively. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
spawn_event_pump drains the scheduler's crossbeam Receiver<Event> on a tokio spawn_blocking task. For program-scoped events it calls Store::append_event before broadcasting; daemon-scoped events (DaemonStarted/Stopped/Pressure/StreamDropped/TickFrameSkipped/ CapturePermissionMissing) bypass the DB and broadcast only (Decision 1.7). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DaemonHandler carries Arc<Mutex<Store>>, CommandChannel, started_at Instant, and an oneshot shutdown_tx. handle() dispatches to handle_health / handle_shutdown; every other op returns Internal "not yet wired" until the follow-up 8b–8g subtasks land each op. Tests live on a test_support::fresh_handler() fixture that keeps the tempdir alive and exposes the bridge's cmd_rx for later subtasks. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
handle_submit runs vcli_dsl::validate_value on the incoming JSON. On success it canonicalizes and writes a Pending row via spawn_blocking, then pushes SchedulerCommand::SubmitValidated into cmd_tx. DslError already exposes to_payload(), so error paths map straight to the wire. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
handle_list parses the optional state filter, queries
Store::list_programs off the async executor via spawn_blocking, and
emits {items: [...]}. handle_status returns the single-row view or
maps StoreError::UnknownProgram to ErrorCode::UnknownProgram.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both ops push the matching SchedulerCommand onto cmd_tx and surface a DaemonBusy error if the bounded queue is full. Scheduler owns the state transition; the handler is a pure forwarder. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
handle_resume runs Store::resume_program (DB state transition) then re-reads and re-validates the source_json via vcli_dsl so we hand the scheduler a fresh vcli_core::Program. StoreError::NotResumable and UnknownProgram are mapped to matching ErrorCode variants so clients can distinguish "wrong state" from "missing id" cleanly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
stream_events handles both firehose and per-program filtering plus follow-mode broadcast receives with lag-recovery via a stream.dropped frame. Non-follow mode pages the stored history out of SQLite and returns. Trace is a stubbed no-op pending runtime trace support. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
handle_gc computes a retention cutoff (now - RETENTION_DAYS) on the async executor, calls Store::gc_all via spawn_blocking, and surfaces the GcReport fields (programs/assets/blobs/orphan_blobs deleted). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
emit_recovery_events writes (program.state_changed, program.failed) pairs for every RecoveredProgram returned by Store::open — so IPC subscribers see a clean transition from running into failed(daemon_restart). emit_daemon_started posts the version banner. reload_waiting_programs walks rows in Waiting state and pushes each through SchedulerCommand::SubmitValidated so triggers are re-armed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
install_signal_handlers spawns a signal-hook-tokio task that watches SIGTERM + SIGINT and fires the shared shutdown oneshot. shutdown_tx is an Arc<Mutex<Option<…>>> shared with DaemonHandler so either the OS or an IPC Shutdown request wins — whichever races first. emit_daemon_stopped posts the terminal event on the scheduler→pump channel so the DB and every subscriber see the shutdown. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Orchestrates the full startup sequence in the order spec §Restart semantics Phase A requires: ensure_dirs → logging → pidfile → open store (restart recovery) → channels → event pump → recovery events → daemon.started → reload waiting programs → spawn scheduler thread → build handler + shutdown oneshot → signal handlers → bind socket LAST (Decision 1.2, observable readiness). Shutdown path drains the scheduler, emits daemon.stopped, closes the pump, and releases the pidfile. The RuntimeFactory closure seam lets tests inject MockCapture and MockInputSink; the binary wires the real macOS backends in Task 12. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the placeholder main with --version / --help plus the real run_foreground entrypoint. Builds a multi-thread tokio runtime, resolves platform defaults into a Config, and hands run_foreground the default_runtime_factory — which, for v0, returns MockCapture + MockInputSink + default Perception + SystemRuntimeClock. Real macOS backends land in a future lane. Exit code policy: 0 on clean shutdown, 1 on any DaemonError. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plants a Running row, starts the daemon, waits for the socket to appear, then sends Shutdown over IPC. After the daemon drains cleanly, reopens the store and asserts both the row state transitioned to Failed and the event log contains program.state_changed and program.failed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
End-to-end: starts the daemon with mock backends, connects via IpcClient, sends Submit with a minimal valid program, and asserts the response carries a program_id. Then issues Shutdown so the daemon drains cleanly inside the test timeout. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Starts the daemon, connects, issues Shutdown, and after the daemon drains asserts both the socket file and the pidfile are unlinked — observable readiness in reverse (Decision 1.2). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mechanical rustfmt reflow of config.rs, handler.rs, and graceful_shutdown.rs (no behavior changes). Also commits the Cargo.lock update from adding libc to vcli-daemon's deps in the shutdown lane commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 of 5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Ships the
vcli-daemonbinary — a long-running process that wiresvcli-ipc(transport),vcli-store(SQLite persistence), andvcli-runtime(scheduler) into a single coordinating daemon that clients talk to over a Unix-domain socket.Architecture
std::thread::spawn;crossbeam_channelbridges tokio↔scheduler;tokio::sync::broadcastfans events out to IPC subscribers.Store::append_eventbefore broadcast to subscribers.DaemonError::code()→vcli_core::ErrorCode(InvalidProgram / Internal).stream.droppedframes onRecvError::Lagged.What's in this PR
17 plan tasks across 25 commits, per
docs/superpowers/plans/2026-04-17-vcli-daemon.md. Highlights:DaemonError+ platform path resolutionPidFilewith fs2 advisory lockCommandChanneltokio↔scheduler bridgeEventSinkpersist-then-broadcastDaemonHandler(Health, Submit, List, Status, Cancel, Start, Resume, Gc, Shutdown, streaming Logs/Events/Trace)mainwith runtime factoryTest Plan
cargo test --workspace --lockedgreen on macOScargo clippy --workspace --all-targets -- -D warningscleancargo fmt --all -- --checkclean#![forbid(unsafe_code)]at crate root🤖 Generated with Claude Code