Skip to content

Commit 54d537f

Browse files
committed
refactor: improve build ID generation with consistent timestamp format (#130)
refactor: improve build ID generation with consistent timestamp format fix: lazy-start native opencode and simplify binary resolution
1 parent 77f741f commit 54d537f

File tree

7 files changed

+257
-95
lines changed

7 files changed

+257
-95
lines changed

docs/cli.mdx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,15 +71,14 @@ sandbox-agent opencode [OPTIONS]
7171
| `-H, --host <HOST>` | `127.0.0.1` | Host to bind to |
7272
| `-p, --port <PORT>` | `2468` | Port to bind to |
7373
| `--session-title <TITLE>` | - | Title for the OpenCode session |
74-
| `--opencode-bin <PATH>` | - | Override `opencode` binary path |
7574

7675
```bash
7776
sandbox-agent opencode --token "$TOKEN"
7877
```
7978

8079
The daemon logs to a per-host log file under the sandbox-agent data directory (for example, `~/.local/share/sandbox-agent/daemon/daemon-127-0-0-1-2468.log`).
8180

82-
Requires the `opencode` binary to be installed (or set `OPENCODE_BIN` / `--opencode-bin`). If it is not found on `PATH`, sandbox-agent installs it automatically.
81+
Existing installs are reused and missing binaries are installed automatically.
8382

8483
---
8584

server/packages/agent-management/src/agents.rs

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -168,9 +168,7 @@ impl AgentManager {
168168
if agent == AgentId::Mock {
169169
return true;
170170
}
171-
self.binary_path(agent).exists()
172-
|| find_in_path(agent.binary_name()).is_some()
173-
|| default_install_dir().join(agent.binary_name()).exists()
171+
self.binary_path(agent).exists() || find_in_path(agent.binary_name()).is_some()
174172
}
175173

176174
pub fn binary_path(&self, agent: AgentId) -> PathBuf {
@@ -641,10 +639,6 @@ impl AgentManager {
641639
if let Some(path) = find_in_path(agent.binary_name()) {
642640
return Ok(path);
643641
}
644-
let fallback = default_install_dir().join(agent.binary_name());
645-
if fallback.exists() {
646-
return Ok(fallback);
647-
}
648642
Err(AgentError::BinaryNotFound { agent })
649643
}
650644
}
@@ -1193,12 +1187,6 @@ fn find_in_path(binary_name: &str) -> Option<PathBuf> {
11931187
None
11941188
}
11951189

1196-
fn default_install_dir() -> PathBuf {
1197-
dirs::data_dir()
1198-
.map(|dir| dir.join("sandbox-agent").join("bin"))
1199-
.unwrap_or_else(|| PathBuf::from(".").join(".sandbox-agent").join("bin"))
1200-
}
1201-
12021190
fn download_bytes(url: &Url) -> Result<Vec<u8>, AgentError> {
12031191
let client = Client::builder().build()?;
12041192
let mut response = client.get(url.clone()).send()?;

server/packages/sandbox-agent/build.rs

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -99,26 +99,23 @@ fn generate_version(out_dir: &Path) {
9999
fn generate_build_id(out_dir: &Path) {
100100
use std::process::Command;
101101

102-
let build_id = Command::new("git")
102+
let source_id = Command::new("git")
103103
.args(["rev-parse", "--short", "HEAD"])
104104
.output()
105105
.ok()
106106
.filter(|o| o.status.success())
107107
.and_then(|o| String::from_utf8(o.stdout).ok())
108108
.map(|s| s.trim().to_string())
109-
.unwrap_or_else(|| {
110-
// Fallback: use the package version + compile-time timestamp
111-
let version = env::var("CARGO_PKG_VERSION").unwrap_or_default();
112-
let timestamp = std::time::SystemTime::now()
113-
.duration_since(std::time::UNIX_EPOCH)
114-
.map(|d| d.as_secs().to_string())
115-
.unwrap_or_default();
116-
format!("{version}-{timestamp}")
117-
});
109+
.unwrap_or_else(|| env::var("CARGO_PKG_VERSION").unwrap_or_default());
110+
let timestamp = std::time::SystemTime::now()
111+
.duration_since(std::time::UNIX_EPOCH)
112+
.map(|d| d.as_nanos().to_string())
113+
.unwrap_or_else(|_| "0".to_string());
114+
let build_id = format!("{source_id}-{timestamp}");
118115

119116
let out_file = out_dir.join("build_id.rs");
120117
let contents = format!(
121-
"/// Unique identifier for this build (git short hash or version-timestamp fallback).\n\
118+
"/// Unique identifier for this build (source id + build timestamp).\n\
122119
pub const BUILD_ID: &str = \"{}\";\n",
123120
build_id
124121
);

server/packages/sandbox-agent/src/cli.rs

Lines changed: 11 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -126,9 +126,6 @@ pub struct OpencodeArgs {
126126

127127
#[arg(long)]
128128
session_title: Option<String>,
129-
130-
#[arg(long)]
131-
opencode_bin: Option<PathBuf>,
132129
}
133130

134131
impl Default for OpencodeArgs {
@@ -137,7 +134,6 @@ impl Default for OpencodeArgs {
137134
host: DEFAULT_HOST.to_string(),
138135
port: DEFAULT_PORT,
139136
session_title: None,
140-
opencode_bin: None,
141137
}
142138
}
143139
}
@@ -606,7 +602,7 @@ fn run_opencode(cli: &CliConfig, args: &OpencodeArgs) -> Result<(), CliError> {
606602
write_stdout_line(&format!("OpenCode session: {session_id}"))?;
607603

608604
let attach_url = format!("{base_url}/opencode");
609-
let opencode_bin = resolve_opencode_bin(args.opencode_bin.as_ref())?;
605+
let opencode_bin = resolve_opencode_bin()?;
610606
let mut opencode_cmd = ProcessCommand::new(opencode_bin);
611607
opencode_cmd
612608
.arg("attach")
@@ -844,50 +840,19 @@ fn create_opencode_session(
844840
Ok(session_id.to_string())
845841
}
846842

847-
fn resolve_opencode_bin(explicit: Option<&PathBuf>) -> Result<PathBuf, CliError> {
848-
if let Some(path) = explicit {
849-
return Ok(path.clone());
850-
}
851-
if let Ok(path) = std::env::var("OPENCODE_BIN") {
852-
return Ok(PathBuf::from(path));
853-
}
854-
if let Some(path) = find_in_path("opencode") {
855-
write_stderr_line(&format!(
856-
"using opencode binary from PATH: {}",
857-
path.display()
858-
))?;
859-
return Ok(path);
860-
}
861-
843+
fn resolve_opencode_bin() -> Result<PathBuf, CliError> {
862844
let manager = AgentManager::new(default_install_dir())
863845
.map_err(|err| CliError::Server(err.to_string()))?;
864-
match manager.resolve_binary(AgentId::Opencode) {
865-
Ok(path) => Ok(path),
866-
Err(_) => {
867-
write_stderr_line("opencode not found; installing...")?;
868-
let result = manager
869-
.install(
870-
AgentId::Opencode,
871-
InstallOptions {
872-
reinstall: false,
873-
version: None,
874-
},
875-
)
876-
.map_err(|err| CliError::Server(err.to_string()))?;
877-
Ok(result.path)
878-
}
879-
}
880-
}
881-
882-
fn find_in_path(binary_name: &str) -> Option<PathBuf> {
883-
let path_var = std::env::var_os("PATH")?;
884-
for path in std::env::split_paths(&path_var) {
885-
let candidate = path.join(binary_name);
886-
if candidate.exists() {
887-
return Some(candidate);
888-
}
846+
match manager.install(
847+
AgentId::Opencode,
848+
InstallOptions {
849+
reinstall: false,
850+
version: None,
851+
},
852+
) {
853+
Ok(result) => Ok(result.path),
854+
Err(err) => Err(CliError::Server(err.to_string())),
889855
}
890-
None
891856
}
892857

893858
fn run_credentials(command: &CredentialsCommand) -> Result<(), CliError> {

server/packages/sandbox-agent/src/daemon.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ use crate::cli::{CliConfig, CliError};
1010
mod build_id {
1111
include!(concat!(env!("OUT_DIR"), "/build_id.rs"));
1212
}
13-
1413
pub use build_id::BUILD_ID;
1514

1615
const DAEMON_HEALTH_TIMEOUT: Duration = Duration::from_secs(30);
@@ -446,7 +445,10 @@ pub fn ensure_running(
446445
// Check build version
447446
if !is_version_current(host, port) {
448447
let old = read_daemon_version(host, port).unwrap_or_else(|| "unknown".to_string());
449-
eprintln!("daemon outdated (build {old} -> {BUILD_ID}), restarting...");
448+
eprintln!(
449+
"daemon outdated (build {old} -> {}), restarting...",
450+
BUILD_ID
451+
);
450452
stop(host, port)?;
451453
return start(cli, host, port, token);
452454
}

server/packages/sandbox-agent/src/opencode_compat.rs

Lines changed: 80 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ use serde::{Deserialize, Serialize};
2323
use serde_json::{json, Value};
2424
use tokio::sync::{broadcast, Mutex};
2525
use tokio::time::interval;
26-
use tracing::warn;
26+
use tracing::{info, warn};
2727
use utoipa::{IntoParams, OpenApi, ToSchema};
2828

2929
use crate::router::{
@@ -656,21 +656,38 @@ fn default_agent_mode() -> &'static str {
656656
}
657657

658658
async fn opencode_model_cache(state: &OpenCodeAppState) -> OpenCodeModelCache {
659-
{
660-
let cache = state.opencode.model_cache.lock().await;
661-
if let Some(cache) = cache.as_ref() {
662-
return cache.clone();
663-
}
659+
// Keep this lock for the full build to enforce singleflight behavior.
660+
// Concurrent requests wait for the same in-flight build instead of
661+
// spawning duplicate provider/model fetches.
662+
let mut slot = state.opencode.model_cache.lock().await;
663+
if let Some(cache) = slot.as_ref() {
664+
info!(
665+
entries = cache.entries.len(),
666+
groups = cache.group_names.len(),
667+
connected = cache.connected.len(),
668+
"opencode model cache hit"
669+
);
670+
return cache.clone();
664671
}
665672

673+
let started = std::time::Instant::now();
674+
info!("opencode model cache miss; building cache");
666675
let cache = build_opencode_model_cache(state).await;
667-
let mut slot = state.opencode.model_cache.lock().await;
676+
info!(
677+
elapsed_ms = started.elapsed().as_millis() as u64,
678+
entries = cache.entries.len(),
679+
groups = cache.group_names.len(),
680+
connected = cache.connected.len(),
681+
"opencode model cache built"
682+
);
668683
*slot = Some(cache.clone());
669684
cache
670685
}
671686

672687
async fn build_opencode_model_cache(state: &OpenCodeAppState) -> OpenCodeModelCache {
688+
let started = std::time::Instant::now();
673689
// Check credentials upfront
690+
let creds_started = std::time::Instant::now();
674691
let credentials = match tokio::task::spawn_blocking(|| {
675692
extract_all_credentials(&CredentialExtractionOptions::new())
676693
})
@@ -684,6 +701,10 @@ async fn build_opencode_model_cache(state: &OpenCodeAppState) -> OpenCodeModelCa
684701
};
685702
let has_anthropic = credentials.anthropic.is_some();
686703
let has_openai = credentials.openai.is_some();
704+
info!(
705+
elapsed_ms = creds_started.elapsed().as_millis() as u64,
706+
has_anthropic, has_openai, "opencode model cache credential scan complete"
707+
);
687708

688709
let mut entries = Vec::new();
689710
let mut model_lookup = HashMap::new();
@@ -693,11 +714,38 @@ async fn build_opencode_model_cache(state: &OpenCodeAppState) -> OpenCodeModelCa
693714
let mut group_names: HashMap<String, String> = HashMap::new();
694715
let mut default_model: Option<String> = None;
695716

696-
for agent in available_agent_ids() {
697-
let response = match state.inner.session_manager().agent_models(agent).await {
717+
let agents = available_agent_ids();
718+
let manager = state.inner.session_manager();
719+
let fetches = agents.iter().copied().map(|agent| {
720+
let manager = manager.clone();
721+
async move {
722+
let agent_started = std::time::Instant::now();
723+
let response = manager.agent_models(agent).await;
724+
(agent, agent_started.elapsed(), response)
725+
}
726+
});
727+
let fetch_results = futures::future::join_all(fetches).await;
728+
729+
for (agent, elapsed, response) in fetch_results {
730+
let response = match response {
698731
Ok(response) => response,
699-
Err(_) => continue,
732+
Err(err) => {
733+
warn!(
734+
agent = agent.as_str(),
735+
elapsed_ms = elapsed.as_millis() as u64,
736+
?err,
737+
"opencode model cache failed fetching agent models"
738+
);
739+
continue;
740+
}
700741
};
742+
info!(
743+
agent = agent.as_str(),
744+
elapsed_ms = elapsed.as_millis() as u64,
745+
model_count = response.models.len(),
746+
has_default = response.default_model.is_some(),
747+
"opencode model cache fetched agent models"
748+
);
701749

702750
let first_model_id = response.models.first().map(|model| model.id.clone());
703751
for model in response.models {
@@ -805,7 +853,7 @@ async fn build_opencode_model_cache(state: &OpenCodeAppState) -> OpenCodeModelCa
805853
}
806854
}
807855

808-
OpenCodeModelCache {
856+
let cache = OpenCodeModelCache {
809857
entries,
810858
model_lookup,
811859
group_defaults,
@@ -814,7 +862,17 @@ async fn build_opencode_model_cache(state: &OpenCodeAppState) -> OpenCodeModelCa
814862
default_group,
815863
default_model,
816864
connected,
817-
}
865+
};
866+
info!(
867+
elapsed_ms = started.elapsed().as_millis() as u64,
868+
entries = cache.entries.len(),
869+
groups = cache.group_names.len(),
870+
connected = cache.connected.len(),
871+
default_group = cache.default_group.as_str(),
872+
default_model = cache.default_model.as_str(),
873+
"opencode model cache build complete"
874+
);
875+
cache
818876
}
819877

820878
fn resolve_agent_from_model(
@@ -1123,8 +1181,16 @@ async fn proxy_native_opencode(
11231181
headers: &HeaderMap,
11241182
body: Option<Value>,
11251183
) -> Option<Response> {
1126-
let Some(base_url) = state.opencode.proxy_base_url() else {
1127-
return None;
1184+
let base_url = if let Some(base_url) = state.opencode.proxy_base_url() {
1185+
base_url.to_string()
1186+
} else {
1187+
match state.inner.ensure_opencode_server().await {
1188+
Ok(base_url) => base_url,
1189+
Err(err) => {
1190+
warn!(path, ?err, "failed to lazily start native opencode server");
1191+
return None;
1192+
}
1193+
}
11281194
};
11291195

11301196
let mut request = state

0 commit comments

Comments
 (0)