fix(release): sync release tags during preflight (#780)

This commit is contained in:
Wes
2026-05-28 16:29:25 -07:00
committed by GitHub
parent 3f3ec64791
commit c761a76ff2
26 changed files with 139 additions and 114 deletions
+15 -13
View File
@@ -30,20 +30,22 @@ fn main() {
let p = |name: &str| dir.join(name).to_string_lossy().into_owned();
let t0 = Instant::now();
let mut cfg = OfflineTtsConfig::default();
cfg.model = OfflineTtsModelConfig {
pocket: OfflineTtsPocketModelConfig {
lm_main: Some(p("lm_main.int8.onnx")),
lm_flow: Some(p("lm_flow.int8.onnx")),
encoder: Some(p("encoder.onnx")),
decoder: Some(p("decoder.int8.onnx")),
text_conditioner: Some(p("text_conditioner.onnx")),
vocab_json: Some(p("vocab.json")),
token_scores_json: Some(p("token_scores.json")),
voice_embedding_cache_capacity: 16,
let cfg = OfflineTtsConfig {
model: OfflineTtsModelConfig {
pocket: OfflineTtsPocketModelConfig {
lm_main: Some(p("lm_main.int8.onnx")),
lm_flow: Some(p("lm_flow.int8.onnx")),
encoder: Some(p("encoder.onnx")),
decoder: Some(p("decoder.int8.onnx")),
text_conditioner: Some(p("text_conditioner.onnx")),
vocab_json: Some(p("vocab.json")),
token_scores_json: Some(p("token_scores.json")),
voice_embedding_cache_capacity: 16,
},
num_threads: 1,
debug: false,
..Default::default()
},
num_threads: 1,
debug: false,
..Default::default()
};
let engine = OfflineTts::create(&cfg).expect("engine create");
@@ -42,20 +42,22 @@ fn main() {
let dir = PathBuf::from(&model_dir);
let p = |name: &str| dir.join(name).to_string_lossy().into_owned();
let mut cfg = OfflineTtsConfig::default();
cfg.model = OfflineTtsModelConfig {
pocket: OfflineTtsPocketModelConfig {
lm_main: Some(p("lm_main.int8.onnx")),
lm_flow: Some(p("lm_flow.int8.onnx")),
encoder: Some(p("encoder.onnx")),
decoder: Some(p("decoder.int8.onnx")),
text_conditioner: Some(p("text_conditioner.onnx")),
vocab_json: Some(p("vocab.json")),
token_scores_json: Some(p("token_scores.json")),
voice_embedding_cache_capacity: 16,
let cfg = OfflineTtsConfig {
model: OfflineTtsModelConfig {
pocket: OfflineTtsPocketModelConfig {
lm_main: Some(p("lm_main.int8.onnx")),
lm_flow: Some(p("lm_flow.int8.onnx")),
encoder: Some(p("encoder.onnx")),
decoder: Some(p("decoder.int8.onnx")),
text_conditioner: Some(p("text_conditioner.onnx")),
vocab_json: Some(p("vocab.json")),
token_scores_json: Some(p("token_scores.json")),
voice_embedding_cache_capacity: 16,
},
num_threads: 1,
debug: false,
..Default::default()
},
num_threads: 1,
debug: false,
..Default::default()
};
let engine = OfflineTts::create(&cfg).expect("engine create");
+17 -15
View File
@@ -98,20 +98,22 @@ fn main() {
let dir = PathBuf::from(&model_dir);
let p = |name: &str| dir.join(name).to_string_lossy().into_owned();
let mut cfg = OfflineTtsConfig::default();
cfg.model = OfflineTtsModelConfig {
pocket: OfflineTtsPocketModelConfig {
lm_main: Some(p("lm_main.int8.onnx")),
lm_flow: Some(p("lm_flow.int8.onnx")),
encoder: Some(p("encoder.onnx")),
decoder: Some(p("decoder.int8.onnx")),
text_conditioner: Some(p("text_conditioner.onnx")),
vocab_json: Some(p("vocab.json")),
token_scores_json: Some(p("token_scores.json")),
voice_embedding_cache_capacity: 16,
let cfg = OfflineTtsConfig {
model: OfflineTtsModelConfig {
pocket: OfflineTtsPocketModelConfig {
lm_main: Some(p("lm_main.int8.onnx")),
lm_flow: Some(p("lm_flow.int8.onnx")),
encoder: Some(p("encoder.onnx")),
decoder: Some(p("decoder.int8.onnx")),
text_conditioner: Some(p("text_conditioner.onnx")),
vocab_json: Some(p("vocab.json")),
token_scores_json: Some(p("token_scores.json")),
voice_embedding_cache_capacity: 16,
},
num_threads: 1,
debug: false,
..Default::default()
},
num_threads: 1,
debug: false,
..Default::default()
};
let engine = OfflineTts::create(&cfg).expect("engine create");
@@ -199,8 +201,8 @@ fn find_gap(samples: &[f32], sr: u32, thresh: f32, min_ms: u32) -> String {
let scan_start = (sr as usize * 30) / 1000;
let min_samples = (sr as usize * min_ms as usize) / 1000;
let mut silence_from: Option<usize> = None;
for i in scan_start..samples.len() {
if samples[i].abs() < thresh {
for (i, sample) in samples.iter().enumerate().skip(scan_start) {
if sample.abs() < thresh {
silence_from.get_or_insert(i);
} else if let Some(start) = silence_from {
if i - start >= min_samples {
@@ -277,7 +277,7 @@ mod tests {
let agent_hex = agent.public_key().to_hex();
let agent_compat = nostr::PublicKey::from_hex(&agent_hex).unwrap();
let owner_compat_secret =
nostr::SecretKey::from_slice(&owner.secret_key().as_secret_bytes()[..]).unwrap();
nostr::SecretKey::from_slice(owner.secret_key().as_secret_bytes()).unwrap();
let owner_compat_keys = nostr::Keys::new(owner_compat_secret);
let tag_json = sprout_sdk::nip_oa::compute_auth_tag(&owner_compat_keys, &agent_compat, "")
.expect("compute_auth_tag");
+2 -2
View File
@@ -133,7 +133,7 @@ fn sign_blossom_upload_auth(
Tag::parse(vec!["expiration", &(now + expiry_secs).to_string()])
.map_err(|e| e.to_string())?,
];
if let Some(domain) = extract_server_authority(&base_url) {
if let Some(domain) = extract_server_authority(base_url) {
tags.push(Tag::parse(vec!["server".to_string(), domain]).map_err(|e| e.to_string())?);
}
EventBuilder::new(Kind::from(24242), "Upload sprout-media")
@@ -265,7 +265,7 @@ fn find_ffmpeg() -> Result<std::path::PathBuf, String> {
/// Detect if a file is a video based on magic bytes.
fn is_video_file(buf: &[u8]) -> bool {
infer::get(buf).map_or(false, |t| t.mime_type().starts_with("video/"))
infer::get(buf).is_some_and(|t| t.mime_type().starts_with("video/"))
}
/// Maximum wall-clock time for an ffmpeg transcode before we kill it.
@@ -65,7 +65,7 @@ pub async fn download_image(
.ok()
.and_then(|u| {
u.path_segments()?
.last()
.next_back()
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
})
+5 -8
View File
@@ -295,14 +295,11 @@ async fn pairing_ws_task_inner(
let mut guard = session.lock().await;
let Some(s) = guard.as_mut() else { break };
match s.handle_abort(&event) {
Ok(reason) => {
let _ = app.emit("pairing-aborted", PairingAbortedPayload {
reason: format!("{reason:?}"),
});
break;
}
Err(_) => {}
if let Ok(reason) = s.handle_abort(&event) {
let _ = app.emit("pairing-aborted", PairingAbortedPayload {
reason: format!("{reason:?}"),
});
break;
}
if let Ok(sas) = s.handle_offer(&event) {
@@ -67,7 +67,7 @@ pub async fn add_relay_member(
) -> Result<serde_json::Value, String> {
let builder = events::build_relay_admin_add(&target_pubkey, &role)?;
let result = submit_event(builder, &state).await?;
Ok(serde_json::to_value(result).map_err(|e| e.to_string())?)
serde_json::to_value(result).map_err(|e| e.to_string())
}
#[tauri::command]
@@ -77,7 +77,7 @@ pub async fn remove_relay_member(
) -> Result<serde_json::Value, String> {
let builder = events::build_relay_admin_remove(&target_pubkey)?;
let result = submit_event(builder, &state).await?;
Ok(serde_json::to_value(result).map_err(|e| e.to_string())?)
serde_json::to_value(result).map_err(|e| e.to_string())
}
#[tauri::command]
@@ -88,5 +88,5 @@ pub async fn change_relay_member_role(
) -> Result<serde_json::Value, String> {
let builder = events::build_relay_admin_change_role(&target_pubkey, &new_role)?;
let result = submit_event(builder, &state).await?;
Ok(serde_json::to_value(result).map_err(|e| e.to_string())?)
serde_json::to_value(result).map_err(|e| e.to_string())
}
+2 -2
View File
@@ -279,7 +279,7 @@ pub async fn get_liked_notes(
})],
)
.await?;
reactions.sort_by(|left, right| right.created_at.cmp(&left.created_at));
reactions.sort_by_key(|reaction| std::cmp::Reverse(reaction.created_at));
let reaction_ids: Vec<String> = reactions.iter().map(|event| event.id.to_hex()).collect();
let deletions = if reaction_ids.is_empty() {
@@ -395,7 +395,7 @@ pub async fn get_notes_timeline(
.collect();
// Sort newest-first.
notes.sort_by(|a, b| b.created_at.cmp(&a.created_at));
notes.sort_by_key(|note| std::cmp::Reverse(note.created_at));
notes.truncate(200);
Ok(UserNotesResponse {
+12 -13
View File
@@ -664,13 +664,12 @@ impl ModelManager {
MAX_STT_DOWNLOAD_BYTES,
"stt archive",
|downloaded, content_length| {
if let Some(total) = content_length {
if total > 0 {
let pct = ((downloaded * 89) / total).min(89) as u8;
slot.set_status(ModelStatus::Downloading {
progress_percent: pct,
});
}
if let Some(pct) =
content_length.and_then(|total| (downloaded * 89).checked_div(total))
{
slot.set_status(ModelStatus::Downloading {
progress_percent: pct.min(89) as u8,
});
}
},
)
@@ -779,10 +778,11 @@ impl ModelManager {
for (i, (url, filename)) in downloads.iter().enumerate() {
eprintln!("sprout-desktop: downloading Pocket TTS {filename} from {url}");
let response = fetch_url(&http_client, url, filename).await.map_err(|e| {
let _ = std::fs::remove_dir_all(&temp_dir);
e
})?;
let response = fetch_url(&http_client, url, filename)
.await
.inspect_err(|_| {
let _ = std::fs::remove_dir_all(&temp_dir);
})?;
let dest = temp_dir.join(filename);
let slot = self.tts.clone();
@@ -807,9 +807,8 @@ impl ModelManager {
},
)
.await
.map_err(|e| {
.inspect_err(|_| {
let _ = std::fs::remove_dir_all(&temp_dir);
e
})?;
eprintln!("sprout-desktop: downloaded {bytes} bytes ({filename}), wrote to disk");
+1 -1
View File
@@ -84,7 +84,7 @@ pub(crate) async fn maybe_start_stt_pipeline(
if !models::is_stt_ready() {
return Ok(false); // Models not downloaded yet — voice-only mode.
}
let model_dir = models::stt_model_dir().ok_or_else(|| "STT model directory not found")?;
let model_dir = models::stt_model_dir().ok_or("STT model directory not found")?;
let channel_uuid = parse_channel_uuid(ephemeral_channel_id)?;
+13 -7
View File
@@ -38,7 +38,7 @@
//! - `load_text_to_speech(model_dir)` → `Result<Engine, String>`
//! - `load_voice_style(path)` → `Result<VoiceStyle, String>`
//! - `Engine::synth_chunk(&self, text, lang, &VoiceStyle, steps, speed)`
//! → `Result<Vec<f32>, String>`
//! → `Result<Vec<f32>, String>`
//!
//! `lang` and `steps` are accepted for API compatibility with the previous
//! Kokoro engine but are unused — Pocket TTS does its own language ID from
@@ -589,10 +589,12 @@ mod tests {
assert_eq!(out.text, format!("{}Yep.", short_prefix()));
assert!(out.is_short, "1-word input is short");
assert_eq!(out.max_frames, Some(SHORT_PROMPT_MAX_FRAMES));
assert!(
SHORT_PROMPT_MAX_FRAMES < SHERPA_ONNX_MAX_FRAMES_DEFAULT,
"short cap must be tighter than the upstream default"
);
const {
assert!(
SHORT_PROMPT_MAX_FRAMES < SHERPA_ONNX_MAX_FRAMES_DEFAULT,
"short cap must be tighter than the upstream default"
);
}
}
#[test]
@@ -758,10 +760,14 @@ mod tests {
#[test]
fn short_prompt_max_frames_is_below_upstream_default() {
// Sanity: the override only ever *lowers* the cap, never raises it.
assert!(SHORT_PROMPT_MAX_FRAMES < SHERPA_ONNX_MAX_FRAMES_DEFAULT);
const {
assert!(SHORT_PROMPT_MAX_FRAMES < SHERPA_ONNX_MAX_FRAMES_DEFAULT);
}
// …and is still large enough for a one-to-four-word reply. At Mimi's
// 12.5 Hz frame rate, 100 frames = 8 s, which is roomy.
assert!(SHORT_PROMPT_MAX_FRAMES >= 50, "would risk truncation");
const {
assert!(SHORT_PROMPT_MAX_FRAMES >= 50, "would risk truncation");
}
}
// ── trim_leading_cold_start ──────────────────────────────────────────────
@@ -40,7 +40,7 @@ const ABBREVIATIONS: &[&str] = &[
/// Returns non-empty, trimmed strings.
pub fn split_sentences(text: &str) -> Vec<String> {
// First, split on newlines and em-dashes to get coarse segments.
let coarse: Vec<&str> = text.split(|c: char| c == '\n' || c == '—').collect();
let coarse: Vec<&str> = text.split(['\n', '—']).collect();
let mut sentences = Vec::new();
@@ -240,7 +240,7 @@ fn strip_urls(text: &str) -> String {
// belongs to the surrounding sentence rather than the URL itself.
// A trailing `.`, `!`, or `?` is preserved when it is at end-of-string
// or followed by whitespace (i.e. it is a sentence boundary).
let trailing_punct = if url_token.ends_with(|c: char| matches!(c, '.' | '!' | '?')) {
let trailing_punct = if url_token.ends_with(['.', '!', '?']) {
let after = rest; // rest is already past url_end
if after.is_empty() || after.starts_with(|c: char| c.is_whitespace()) {
// Preserve the trailing punctuation.
+22 -8
View File
@@ -170,17 +170,17 @@ pub(crate) async fn connect_audio_relay(
.clone();
tokio::spawn(async move {
if let Err(e) = audio_relay_pipeline(
if let Err(e) = audio_relay_pipeline(AudioRelayPipelineArgs {
ws_tx,
ws_rx,
pcm_rx,
cancel_clone.clone(),
app_handle.clone(),
cancel: cancel_clone.clone(),
app_handle: app_handle.clone(),
initial_peers,
tts_cancel,
tts_active,
output_device_name,
)
})
.await
{
eprintln!("sprout-desktop: audio relay pipeline exited: {e}");
@@ -204,17 +204,31 @@ pub(crate) async fn connect_audio_relay(
pub(crate) type WsStream =
tokio_tungstenite::WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>>;
async fn audio_relay_pipeline(
struct AudioRelayPipelineArgs {
ws_tx: futures_util::stream::SplitSink<WsStream, WsMsg>,
ws_rx: futures_util::stream::SplitStream<WsStream>,
mut pcm_rx: tokio::sync::mpsc::Receiver<Vec<u8>>,
pcm_rx: tokio::sync::mpsc::Receiver<Vec<u8>>,
cancel: CancellationToken,
app_handle: Option<tauri::AppHandle>,
initial_peers: Vec<(u8, String)>,
tts_cancel: Arc<AtomicBool>,
tts_active: Arc<AtomicBool>,
output_device_name: Option<String>,
) -> Result<(), String> {
}
async fn audio_relay_pipeline(args: AudioRelayPipelineArgs) -> Result<(), String> {
let AudioRelayPipelineArgs {
ws_tx,
ws_rx,
mut pcm_rx,
cancel,
app_handle,
initial_peers,
tts_cancel,
tts_active,
output_device_name,
} = args;
let mut encoder = opus::Encoder::new(48000, opus::Channels::Mono, opus::Application::Voip)
.map_err(|e| format!("opus encoder: {e}"))?;
encoder
@@ -383,7 +397,7 @@ pub(crate) async fn fetch_channel_members(
let all = fetch_channel_members_with_roles(channel_id, state).await?;
Ok(all
.into_iter()
.filter(|(_, role)| role_filter.map_or(true, |r| role.as_deref() == Some(r)))
.filter(|(_, role)| role_filter.is_none_or(|r| role.as_deref() == Some(r)))
.map(|(pubkey, _)| pubkey)
.collect())
}
+3 -3
View File
@@ -127,7 +127,7 @@ impl SttPipeline {
/// Returns `true` if the worker thread has exited (init failure, crash, or normal exit).
/// Used by hot-start to detect dead pipelines and clear them for retry.
pub fn is_finished(&self) -> bool {
self.thread.as_ref().map_or(true, |h| h.is_finished())
self.thread.as_ref().is_none_or(|h| h.is_finished())
}
/// Feed raw PCM bytes into the pipeline.
@@ -136,7 +136,7 @@ impl SttPipeline {
/// better to lose frames than to stall the UI thread.
pub fn push_audio(&self, pcm_bytes: Vec<u8>) -> Result<(), String> {
// Reject non-4-byte-aligned input — would silently truncate in bytes_to_f32.
if pcm_bytes.len() % 4 != 0 {
if !pcm_bytes.len().is_multiple_of(4) {
return Err(format!(
"audio input not 4-byte aligned ({} bytes) — expected f32 LE samples",
pcm_bytes.len()
@@ -283,7 +283,7 @@ fn stt_worker(
let mut tts_was_active = false;
let mut ptt_was_active = ptt_active
.as_ref()
.map_or(false, |p| p.load(Ordering::Acquire));
.is_some_and(|p| p.load(Ordering::Acquire));
loop {
// Check shutdown flag before blocking.
if shutdown.load(Ordering::Acquire) {
+3 -3
View File
@@ -221,7 +221,7 @@ impl TtsPipeline {
/// Returns `true` if the worker thread has exited (init failure, crash, or normal exit).
/// Used by hot-start to detect dead pipelines and clear them for retry.
pub fn is_finished(&self) -> bool {
self.thread.as_ref().map_or(true, |h| h.is_finished())
self.thread.as_ref().is_none_or(|h| h.is_finished())
}
}
@@ -317,7 +317,7 @@ fn tts_worker(
let channels = NonZero::new(1u16).unwrap();
let rate = NonZero::new(SAMPLE_RATE).unwrap();
let silence = vec![0.0f32; SAMPLE_RATE as usize / 10]; // 100ms of silence
let player = Player::connect_new(&sink_handle.mixer());
let player = Player::connect_new(sink_handle.mixer());
player.append(SamplesBuffer::new(channels, rate, silence));
// Wait for the silent buffer to drain — this ensures the output stream
// is fully initialized before the main loop creates its first Player.
@@ -387,7 +387,7 @@ fn tts_worker(
// Single persistent Player — all sentences append here, rodio plays
// them gaplessly without per-sentence device setup overhead.
let player = Player::connect_new(&sink_handle.mixer());
let player = Player::connect_new(sink_handle.mixer());
// NOTE: tts_active is set AFTER the first player.append(), not before.
// Setting it before synthesis would cause STT to discard user speech
// during the synthesis window as "echo" even though no audio is
@@ -461,7 +461,7 @@ pub fn discover_acp_providers() -> Vec<AcpProviderCatalogEntry> {
let underlying_cli_path = provider
.underlying_cli
.and_then(|cli| find_command(cli))
.and_then(find_command)
.map(|p| p.display().to_string());
let default_args = command
+2 -2
View File
@@ -759,8 +759,8 @@ mod tests {
let external = tmp.path().join("external");
fs::create_dir(&external).unwrap();
fs::set_permissions(&external, fs::Permissions::from_mode(0o755)).unwrap();
fs::remove_dir(&root.join("REPOS")).unwrap();
std::os::unix::fs::symlink(&external, &root.join("REPOS")).unwrap();
fs::remove_dir(root.join("REPOS")).unwrap();
std::os::unix::fs::symlink(&external, root.join("REPOS")).unwrap();
// Second call should succeed — it skips chmod on the symlinked child.
ensure_nest_at(&root).unwrap();
@@ -7,6 +7,9 @@ use crate::util;
use std::sync::atomic::{AtomicBool, Ordering};
use tauri::Manager;
type SpawnResult = Result<(std::process::Child, std::path::PathBuf), String>;
type AgentSpawnResult = (String, SpawnResult);
/// Restore managed agents that were running before the app was closed.
///
/// Split into three phases to minimise lock contention with the frontend:
@@ -94,10 +97,7 @@ pub fn restore_managed_agents_on_launch(
.map(|k| k.public_key().to_hex());
// ── Phase B (no locks): resolve commands and spawn processes in parallel ──
let spawn_results: Vec<(
String,
Result<(std::process::Child, std::path::PathBuf), String>,
)> = std::thread::scope(|scope| {
let spawn_results: Vec<AgentSpawnResult> = std::thread::scope(|scope| {
let owner_hex_ref = owner_hex.as_deref();
let handles: Vec<_> = agents_to_start
.iter()
@@ -11,6 +11,8 @@ use crate::{
util::now_iso,
};
type RespondToEnv = (Vec<(&'static str, String)>, Vec<&'static str>);
/// Binary name fragments for all known agent/harness processes that Sprout
/// may spawn. Used by `process_belongs_to_us()` and the orphan sweep to
/// identify processes we should clean up. Both hyphenated and underscored
@@ -459,7 +461,7 @@ pub fn find_managed_agent_mut<'a>(
pub(crate) fn build_respond_to_env(
record: &ManagedAgentRecord,
owner_hex: Option<&str>,
) -> Result<(Vec<(&'static str, String)>, Vec<&'static str>), String> {
) -> Result<RespondToEnv, String> {
// Defensive re-validation: an on-disk record could have been hand-edited.
let normalized = super::types::validate_respond_to_allowlist(&record.respond_to_allowlist)?;
if record.respond_to == super::types::RespondTo::Allowlist && normalized.is_empty() {
@@ -188,7 +188,7 @@ pub fn read_log_tail(path: &Path, max_lines: usize) -> Result<String, String> {
// Strip ANSI escapes here (not in the harness) so the desktop log view
// renders cleanly while terminals and other tools still get the colors
// sprout-acp emits.
let cleaned = strip_ansi_escapes::strip_str(&String::from_utf8_lossy(&buf));
let cleaned = strip_ansi_escapes::strip_str(String::from_utf8_lossy(&buf));
let lines: Vec<&str> = cleaned.lines().collect();
let start = lines.len().saturating_sub(max_lines);
Ok(lines[start..].join("\n"))
@@ -696,7 +696,7 @@ mod tests {
fn validate_respond_to_allowlist_accepts_valid_hex_and_lowercases() {
let upper = "A".repeat(64);
let lower = "a".repeat(64);
let result = validate_respond_to_allowlist(&[upper.clone()]).unwrap();
let result = validate_respond_to_allowlist(std::slice::from_ref(&upper)).unwrap();
assert_eq!(result, vec![lower.clone()]);
}
+1 -3
View File
@@ -111,9 +111,7 @@ async fn proxy_handler(AxumState(state): AxumState<ProxyState>, req: Request) ->
}
// Stream the body — no buffering.
let stream = resp
.bytes_stream()
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e));
let stream = resp.bytes_stream().map_err(std::io::Error::other);
let body = Body::from_stream(stream);
(status, headers, body).into_response()
+2 -2
View File
@@ -1046,7 +1046,7 @@ mod tests {
#[test]
fn agents_overwrites_pubkey_from_event_author() {
let e = ev(10100, r#"{"pubkey":"forged","name":"agent-1"}"#, vec![]);
let v = agents_from_events(&[e.clone()]);
let v = agents_from_events(std::slice::from_ref(&e));
let arr = v.get("agents").and_then(Value::as_array).unwrap();
assert_eq!(arr.len(), 1);
assert_eq!(
@@ -1059,7 +1059,7 @@ mod tests {
#[test]
fn agents_handles_invalid_content() {
let e = ev(10100, "not-json", vec![]);
let v = agents_from_events(&[e.clone()]);
let v = agents_from_events(std::slice::from_ref(&e));
let arr = v.get("agents").and_then(Value::as_array).unwrap();
assert_eq!(
arr[0].get("pubkey").and_then(Value::as_str).unwrap(),
+2 -2
View File
@@ -61,8 +61,8 @@ pub fn acquire(
#[cfg(target_os = "macos")]
{
let assertion_type = b"PreventUserIdleSystemSleep\0".as_ptr() as *const std::ffi::c_char;
let reason = b"Sprout \xe2\x80\x94 agents are active\0".as_ptr() as *const std::ffi::c_char;
let assertion_type = c"PreventUserIdleSystemSleep".as_ptr();
let reason = c"Sprout \u{2014} agents are active".as_ptr();
unsafe {
let cf_type = macos::CFStringCreateWithCString(
+5 -2
View File
@@ -406,8 +406,11 @@ release *ARGS:
echo "Error: must be on main branch (currently on '$CURRENT_BRANCH')"
exit 1
fi
# Ensure local main is up-to-date
git fetch origin main --tags --quiet
# Ensure local main and release tags are up-to-date.
git fetch origin refs/heads/main:refs/remotes/origin/main --no-tags
# Release tags are remote-owned state; sync only v* tags so stale local
# tags from older histories do not make release preflight fail.
git fetch origin '+refs/tags/v*:refs/tags/v*'
if [[ "$(git rev-parse HEAD)" != "$(git rev-parse origin/main)" ]]; then
echo "Error: local main is not up-to-date with origin/main. Run 'git pull' first."
exit 1