Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,36 @@ Current operational considerations:
- Unix endpoints use `/tmp` to stay below macOS Unix-socket path limits. A future hardening step should move them into a user-private runtime directory where available, enforce restrictive socket permissions, and validate peer credentials.
- A 96-bit endpoint digest makes accidental cross-project collisions extraordinarily unlikely, but the repository-local lock remains the final ownership check.

### Reclaiming unused database space

Run explicit maintenance when the repository is quiet:

```bash
atomic compact
atomic compact --repository /path/to/repo --json
```

The command compacts the existing `.atomic/atomic.redb` and reports its
before/after sizes and reclaimed bytes. It preserves history, views,
provenance, change files and unrecorded working files. It does not delete
live data or deduplicate change objects; zero bytes reclaimed is a valid result.

Compaction runs through libatomic, in-process by default. With
`ATOMIC_SERVICE=reactor`, it uses the same handler through the configured
Reactor service; that service must advertise `CompactDatabase`. An older or
unavailable service produces an error, with no local fallback.

Other database readers and writers must release their handles. Lock waiting
uses `ATOMIC_DB_LOCK_WAIT_MS` (30 seconds by default); a busy database produces
an error that can be retried. Once blocking maintenance starts, it finishes
independently of the caller, including the bounded lock wait. Use a quiet period:
other commands may time out while the database is being compacted. Persistent
savepoints block compaction and are never deleted automatically.

This is manual maintenance of an existing combined database, with no implicit
initialization or migration. Hosted Storage's open-handle cache requires separate
maintenance coordination before online compaction can be supported.

### Provenance Graphs

Every agent session builds a causal decision DAG. Not just *what* changed, but *why*:
Expand Down
1 change: 1 addition & 0 deletions atomic-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -95,5 +95,6 @@ git2 = { workspace = true }
windows-sys = { version = "0.61", features = ["Win32_Foundation"] }

[dev-dependencies]
tokio-stream = { workspace = true, features = ["net"] }
tempfile = { workspace = true }
serial_test = { workspace = true }
52 changes: 52 additions & 0 deletions atomic-cli/src/commands/compact.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//! Explicit database maintenance through the shared service layer.

use crate::commands::Command;
use crate::error::{CliError, CliResult};
use crate::service::Service;
use atomic_client::proto::{CompactDatabaseRequest, RequestMeta};
use clap::Args;
use std::path::PathBuf;

/// Reclaim unused database space without deleting repository data.
#[derive(Debug, Args)]
pub struct Compact {
/// Path inside the repository or one of its sandboxes.
#[arg(long, default_value = ".")]
repository: PathBuf,
/// Emit the database path and before/after byte counts as JSON.
#[arg(long)]
json: bool,
}

impl Command for Compact {
fn run(&self) -> CliResult<()> {
let service =
Service::open_root(&self.repository)?.ok_or_else(|| CliError::RepositoryNotFound {
searched_path: self.repository.clone(),
})?;
let report = service.compact_database(CompactDatabaseRequest {
repository: Some(service.reference.clone()),
meta: Some(RequestMeta {
request_id: uuid::Uuid::new_v4().to_string(),
observed_at: None,
}),
})?;
if self.json {
println!(
"{}",
serde_json::json!({
"database": report.database,
"before_bytes": report.before_bytes,
"after_bytes": report.after_bytes,
"reclaimed_bytes": report.reclaimed_bytes,
})
);
} else {
println!(
"Compacted {}: {} -> {} bytes ({} bytes reclaimed)",
report.database, report.before_bytes, report.after_bytes, report.reclaimed_bytes
);
}
Ok(())
}
}
2 changes: 2 additions & 0 deletions atomic-cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ use crate::error::{CliError, CliResult};
// Phase 2: Core Local Commands
pub mod add;
pub mod change;
pub mod compact;
pub mod complete;
pub mod completions;
pub mod conflicts;
Expand Down Expand Up @@ -137,6 +138,7 @@ pub use add::Add;
pub use agent::Agent;
pub use change::ChangeCmd;
pub use clone::Clone;
pub use compact::Compact;
pub use completions::Completions;
pub use conflicts::Conflicts;
pub use diff::Diff;
Expand Down
5 changes: 5 additions & 0 deletions atomic-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ use commands::{
ChangeCmd,
Clone,
Command,
Compact,
Completions,
Conflicts,
Diff,
Expand Down Expand Up @@ -447,6 +448,9 @@ enum Commands {
/// such as backfilling the dependency index for legacy repositories.
Doctor(Doctor),

/// Reclaim unused space in the repository database.
Compact(Compact),

/// Git interoperability commands.
///
/// Import Git repositories into Atomic, preserving history, authorship,
Expand Down Expand Up @@ -996,6 +1000,7 @@ fn main() {
Commands::Diff(diff) => diff.run(),

Commands::Doctor(doctor) => doctor.run(),
Commands::Compact(compact) => compact.run(),

Commands::Git(git) => git.run(),

Expand Down
49 changes: 49 additions & 0 deletions atomic-cli/src/service/compact.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
use super::{pb, services_maintenance, Backend, Service};
use crate::error::CliResult;
use libatomic::atomic::maintenance_service_server::MaintenanceService as _;
use tonic::{Request, Status};

impl Service {
pub fn compact_database(
&self,
request: pb::CompactDatabaseRequest,
) -> CliResult<pb::CompactDatabaseResponse> {
self.call(move |backend| async move {
let mut request = Request::new(request);
request
.metadata_mut()
.insert("x-atomic-contract-version", "2".parse().unwrap());
match backend {
Backend::Local(state) => services_maintenance::MaintenanceImpl { state }
.compact_database(request)
.await
.map(|response| response.into_inner()),
Backend::Reactor(channel) => {
let mut daemon =
pb::daemon_service_client::DaemonServiceClient::new(channel.clone());
let capabilities = daemon
.get_capabilities(pb::GetCapabilitiesRequest {})
.await?
.into_inner();
if capabilities.protocol_version != 2 {
return Err(Status::failed_precondition(
"the running Reactor uses an incompatible service contract; update/restart it and retry",
));
}
let supported = capabilities.methods.iter().any(|method| {
method.service == "MaintenanceService" && method.method == "CompactDatabase"
});
if !supported {
return Err(Status::failed_precondition(
"the running Reactor does not support database compaction; update/restart it and retry",
));
}
pb::maintenance_service_client::MaintenanceServiceClient::new(channel)
.compact_database(request)
.await
.map(|response| response.into_inner())
}
}
})
}
}
2 changes: 2 additions & 0 deletions atomic-cli/src/service/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ use tonic::{Request, Status};

use crate::error::{CliError, CliResult};

mod compact;

/// The service-area selector. `ATOMIC_SERVICE` wins; `ATOMIC_RPC` is the
/// legacy alias (1 → reactor, anything else → local); unset routes local.
pub const ENV_SERVICE: &str = "ATOMIC_SERVICE";
Expand Down
Loading