Skip to content

feat(headless): persist evaluated commands to history database#134

Merged
eitsupi merged 10 commits intomainfrom
feat/headless-history
Mar 29, 2026
Merged

feat(headless): persist evaluated commands to history database#134
eitsupi merged 10 commits intomainfrom
feat/headless-history

Conversation

@eitsupi
Copy link
Copy Markdown
Owner

@eitsupi eitsupi commented Mar 29, 2026

Summary

  • Initialize SqliteBackedHistory in headless mode so that IPC evaluate and user_input commands are recorded in the same SQLite database used by the REPL
  • Generate session ID and pass history_session_id to the IPC server, matching REPL behavior
  • History saving is best-effort (failures logged, never block IPC responses); history.disabled config is respected
  • Add --history-dir and --no-history CLI flags to the headless subcommand, matching the REPL
  • Populate metadata fields (timestamp, hostname, cwd) on history items
  • IPC reply is sent before history persistence to avoid blocking clients on SQLite I/O
  • history_session_id is only advertised when the database was actually opened

Known limitations

  • start_timestamp is post-execution: The timestamp is set after evaluate_with_capture returns, not before. This matches REPL behavior (reedline sets the timestamp at save time), so the two modes are consistent.
  • hostname is 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_HISTORY is OnceLock: 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 clippy pass (including --tests)
  • All existing tests pass (unit + pty_ipc_tests + headless_tests)
  • New integration test: test_headless_history_persistence — verifies command_line, exit_status, hostname, cwd are saved correctly for evaluate (success/error) and user_input
  • New integration test: test_headless_no_history_flag — verifies --no-history prevents database creation
  • Shell completion snapshots updated

🤖 Generated with Claude Code

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]>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 SqliteBackedHistory in headless mode and store it for IPC request handling.
  • Persist headless evaluate and user_input commands to the history DB (best-effort, log on failure).
  • Generate and pass history_session_id into 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]>
@eitsupi eitsupi marked this pull request as draft March 29, 2026 07:35
eitsupi and others added 2 commits March 29, 2026 07:36
- 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]>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

eitsupi and others added 4 commits March 29, 2026 07:43
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]>
@eitsupi eitsupi requested a review from Copilot March 29, 2026 07:48
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]>
@eitsupi eitsupi marked this pull request as ready for review March 29, 2026 07:58
@eitsupi eitsupi merged commit 4e74999 into main Mar 29, 2026
10 checks passed
@eitsupi eitsupi deleted the feat/headless-history branch March 29, 2026 07:59
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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));
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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"
);
}

Copilot uses AI. Check for mistakes.
Comment on lines +875 to +877
if !code.is_empty() {
let exit_status = if has_error { 1 } else { 0 };
save_to_headless_history(&code, Some(exit_status));
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +899 to +901
if !code.is_empty() {
save_to_headless_history(&code, Some(exit_status));
}
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +1004 to +1006
// Small delay to let SQLite flush
std::thread::sleep(Duration::from_millis(200));

Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
///
/// Verifies that:
/// - IPC evaluate commands are saved with correct command_line and exit_status
/// - Metadata fields (hostname, cwd, start_timestamp) are populated
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
/// - Metadata fields (hostname, cwd, start_timestamp) are populated
/// - Metadata fields (hostname, cwd) are populated

Copilot uses AI. Check for mistakes.
eitsupi added a commit that referenced this pull request Mar 29, 2026
## 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]>
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.

2 participants