feat(headless): persist evaluated commands to history database#134
feat(headless): persist evaluated commands to history database#134
Conversation
Initialize SqliteBackedHistory in headless mode so that IPC evaluate and user_input commands are recorded in the same SQLite database used by the REPL. The session ID and history_session_id are now generated and passed to the IPC server, matching the REPL behavior. History saving is best-effort: failures are logged but never block IPC response delivery. The history.disabled config setting is respected. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
There was a problem hiding this comment.
Pull request overview
Enables command history persistence for headless mode so IPC-driven evaluate / user_input calls are recorded in the same SQLite history database used by the interactive REPL, and exposes a headless history_session_id via IPC session metadata for history isolation.
Changes:
- Initialize a
SqliteBackedHistoryin headless mode and store it for IPC request handling. - Persist headless
evaluateanduser_inputcommands to the history DB (best-effort, log on failure). - Generate and pass
history_session_idinto IPC server startup in headless mode.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| crates/arf-console/src/main.rs | Adds headless history initialization and passes history_session_id to IPC server startup. |
| crates/arf-console/src/ipc/mod.rs | Adds a global headless history backend and saves IPC-evaluated commands into SQLite history. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
HistoryItem::from_command_line() leaves all metadata fields as None. Set start_timestamp, hostname, and cwd so headless history entries match the REPL behavior and display correctly in the history browser. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
- Send IPC reply before saving to history so SQLite I/O does not delay the client response - Only advertise history_session_id when the SQLite backend was actually opened successfully, avoiding a misleading session JSON Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
into_string().ok() silently drops the hostname on systems with non-UTF-8 hostnames, while the REPL uses to_string_lossy() which always succeeds. Align the headless path to match. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Expose the same history configuration options available in the REPL (--history-dir, --no-history) for the headless subcommand. These override the config file settings, making it easy to control history from the command line and simplifying testing. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Verify that headless mode correctly saves evaluated commands to the SQLite history database with proper metadata (exit_status, hostname, cwd). Also verify that --no-history prevents database creation. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…ry_dir Idiomatic Rust: &PathBuf is unnecessary double indirection since it derefs to &Path. Aligns with the other path parameters in the same function signature. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
The readiness probe only runs in polling mode (--quiet/--json/--log-file). This test uses stderr-based readiness, so no probe row is recorded. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Remove the claim that history_session_id is always None in headless mode, since headless now supports history persistence. The field is None only when history is disabled or the database failed to open. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Use Vec<_> with type inference instead of spelling out the full tuple type, which clippy flags as too complex. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| /// Once set, `headless_handle_request` will persist evaluated commands | ||
| /// (both `evaluate` and `user_input`) to the SQLite history database. | ||
| pub fn set_headless_history(history: reedline::SqliteBackedHistory) { | ||
| let _ = HEADLESS_HISTORY.set(Mutex::new(history)); |
There was a problem hiding this comment.
set_headless_history ignores the result of OnceLock::set. If this function is accidentally called twice (e.g. in future refactors or tests that run headless initialization more than once), the second call will silently do nothing and keep using the old backend. Consider returning a Result or at least logging a warning when the history backend was already initialized.
| let _ = HEADLESS_HISTORY.set(Mutex::new(history)); | |
| if HEADLESS_HISTORY | |
| .set(Mutex::new(history)) | |
| .is_err() | |
| { | |
| log::warn!( | |
| "Headless history backend already initialized; ignoring duplicate set_headless_history call" | |
| ); | |
| } |
| if !code.is_empty() { | ||
| let exit_status = if has_error { 1 } else { 0 }; | ||
| save_to_headless_history(&code, Some(exit_status)); |
There was a problem hiding this comment.
Headless history saving currently skips only strictly empty strings. In the REPL loop, whitespace-only input is ignored via let trimmed = line.trim(); if trimmed.is_empty() { continue; } (see crates/arf-console/src/repl/mod.rs:638-641). To keep behavior consistent and avoid saving blank/whitespace commands from IPC clients, use a trimmed emptiness check here as well.
| if !code.is_empty() { | ||
| save_to_headless_history(&code, Some(exit_status)); | ||
| } |
There was a problem hiding this comment.
Same as the Evaluate path: this saves whitespace-only user_input to history because it checks !code.is_empty() rather than trimming. The REPL ignores whitespace-only input before evaluation/history (crates/arf-console/src/repl/mod.rs:638-641), so consider matching that behavior here too.
| // Small delay to let SQLite flush | ||
| std::thread::sleep(Duration::from_millis(200)); | ||
|
|
There was a problem hiding this comment.
The test uses a fixed sleep(Duration::from_millis(200)) to wait for history persistence. This is timing-dependent and can be flaky on slow/loaded CI machines. Prefer polling for the DB file/expected rows with a bounded timeout (similar to other loops in this test file) so the test is deterministic.
| /// | ||
| /// Verifies that: | ||
| /// - IPC evaluate commands are saved with correct command_line and exit_status | ||
| /// - Metadata fields (hostname, cwd, start_timestamp) are populated |
There was a problem hiding this comment.
This doc comment says the test verifies start_timestamp is populated, but the query/assertions only check command_line, exit_status, hostname, and cwd. Either include start_timestamp in the SELECT + assert it is non-null, or update the comment so it matches what the test actually validates.
| /// - Metadata fields (hostname, cwd, start_timestamp) are populated | |
| /// - Metadata fields (hostname, cwd) are populated |
## Summary - Warn when `set_headless_history` is called more than once instead of silently ignoring the duplicate - Skip whitespace-only commands in headless history (Evaluate and UserInput paths) to match REPL behavior - Fix doc comment in test that incorrectly mentioned `start_timestamp` verification Addresses review feedback from Copilot on PR #134. Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
Summary
SqliteBackedHistoryin headless mode so that IPCevaluateanduser_inputcommands are recorded in the same SQLite database used by the REPLhistory_session_idto the IPC server, matching REPL behaviorhistory.disabledconfig is respected--history-dirand--no-historyCLI flags to theheadlesssubcommand, matching the REPLhistory_session_idis only advertised when the database was actually openedKnown limitations
start_timestampis post-execution: The timestamp is set afterevaluate_with_capturereturns, not before. This matches REPL behavior (reedline sets the timestamp at save time), so the two modes are consistent.hostnameis fetched per-command: Could be cached since it doesn't change during a session. The syscall cost is negligible, so this is left as a future optimization.HEADLESS_HISTORYisOnceLock: Cannot be re-initialized within the same process. Fine for production (one headless session per process), but limits in-process unit testing.Test plan
cargo check/cargo clippypass (including--tests)test_headless_history_persistence— verifies command_line, exit_status, hostname, cwd are saved correctly for evaluate (success/error) and user_inputtest_headless_no_history_flag— verifies--no-historyprevents database creation🤖 Generated with Claude Code