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
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,26 @@
# Changelog

## 1.0.10-beta.0 — 2026-09-30

### Features

- Uploads, `fp` calls and evaluator calls carry a request id; the daemon also sends batch and machine ids, and `fp` errors show a `ref` (#872)

### Fixes

- The daemon logs each failed upload attempt at warning level with its request id and batch id, so every attempt of a batch can be found; before, only the last one was visible (#872)
- Parking a failed batch works when the spool and state directories are on different filesystems (e.g. separate Docker volumes); before, the batch stayed in the spool and was re-sent on every sweep. The move is crash-safe: the original is deleted only after the copy's directory entry is on disk (#872)

### Docs

- Troubleshooting, HTTP API and Cloud CLI pages explain the `ref` / `request_id` to quote to support, and where the daemon logs them (#872)

### Dependencies

- Python SDK dev lockfile: oauthlib 3.3.1 → 4.0.0 and pyjwt 2.13.0 → 2.15.1, clearing three OSV advisories (#872)
- Pin brace-expansion 5.0.9 → 5.0.12 (package.json override), clearing three OSV advisories (#872)
- Python SDK and `fp` CLI lockfiles: urllib3 2.7.0 → 2.8.0, clearing three OSV advisories (#872)

## 1.0.9 — 2026-09-29

Action needed if you use Jev: its log-only mode is now `observe`, a `jev.json` still set to `shadow` is refused (Jev stays off until `failproofai jev setup` is run again), and Jev's checks now come only from `failproofai policies add FailproofAI/jev-policies`. For Hermes, `failproofai update` moves every profile from the old shell hooks (never run for cron jobs) to the native plugin, and every agent config failproofai edits is written crash-safely with a `.failproofai-backup`. Collects 1.0.9-beta.0 to beta.2 below.
Expand Down
2 changes: 2 additions & 0 deletions Cargo.lock

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

4 changes: 2 additions & 2 deletions bun.lock

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

6 changes: 4 additions & 2 deletions crates/failproofaid/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -790,8 +790,10 @@ fn collector_tasks() -> Vec<fpai_collect::TaskSpec> {
ingest.key.clone(),
cfg.failed_dir.clone(),
)
.map(|u| u.with_redact(cfg.settings.redact))
{
.map(|u| {
u.with_redact(cfg.settings.redact)
.with_machine_id(cfg.settings.machine_id.as_deref())
}) {
Ok(u) => std::sync::Arc::new(u),
Err(err) => {
eprintln!("[failproofaid] collector disabled: {err}");
Expand Down
5 changes: 5 additions & 0 deletions crates/fpai-collect/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ rusqlite = { version = "0.40", features = ["bundled"] }
# reason rustls is chosen above: no C toolchain on the four cross-compiled legs.
ruzstd = "0.9"
time = { version = "0.3", default-features = false, features = ["std", "formatting", "macros"] }
# Request ids and batch ids on each upload (see uploader.rs). Both were already
# in the lockfile — sha2 via failproofaid, getrandom via the TLS stack — so
# neither adds a crate to the four cross-compiled legs; `uuid` would have.
getrandom = "0.3"
sha2 = "0.11"

[dev-dependencies]
tokio = { version = "1", features = ["rt-multi-thread", "macros", "time", "sync", "test-util", "net"] }
Expand Down
27 changes: 24 additions & 3 deletions crates/fpai-collect/src/delivery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ use tokio::sync::Semaphore;

use crate::spool::is_batch_file;
use crate::supervisor::{Shutdown, TaskError};
use crate::uploader::{ParkedName, Uploader};
use crate::uploader::{ParkedName, Uploader, batch_id};

/// Concurrent uploads across the watcher and sweeper combined.
///
Expand Down Expand Up @@ -141,7 +141,14 @@ impl Delivery {

match self.uploader.upload_file(&path).await {
Ok(()) => tracing::debug!(file = %path.display(), "uploaded"),
Err(err) => tracing::warn!(file = %path.display(), %err, "upload failed"),
// The batch id ties this to the attempt and park lines, which
// carry the request ids.
Err(err) => tracing::warn!(
file = %path.display(),
batch_id = %batch_id(&path),
%err,
"upload failed"
),
}
}
}
Expand Down Expand Up @@ -324,6 +331,12 @@ async fn stale_batches(dir: &Path, min_age: Duration, max: usize) -> Vec<PathBuf
/// Skips anything poison or carrying a definitive client status — those fail
/// identically until a human fixes the key or the URL, and retrying them burns
/// permits that batches which could succeed are waiting for.
/// Whether the parked-batch sweep may retry the file with this name.
fn sweepable(name: &str) -> bool {
// A dotfile is a park still being copied across filesystems.
!name.starts_with('.') && ParkedName::parse(name).is_auto_retryable()
}

async fn retry_parked(delivery: &Delivery, failed_dir: &Path, sd: &Shutdown) {
let Ok(mut rd) = tokio::fs::read_dir(failed_dir).await else {
return;
Expand All @@ -336,7 +349,7 @@ async fn retry_parked(delivery: &Delivery, failed_dir: &Path, sd: &Shutdown) {
let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
continue;
};
if !ParkedName::parse(name).is_auto_retryable() {
if !sweepable(name) {
continue;
}
let Ok(meta) = entry.metadata().await else {
Expand Down Expand Up @@ -414,6 +427,14 @@ mod tests {
std::fs::remove_dir_all(&dir).ok();
}

#[test]
fn the_parked_sweep_skips_rejected_batches_and_half_copied_parks() {
assert!(sweepable("hooks-a-1-0.a1.jsonl"));
assert!(!sweepable("hooks-a-1-0.a1.c401.jsonl"));
assert!(!sweepable("hooks-a-1-0.a3.jsonl.poison"));
assert!(!sweepable(".hooks-a-1-0.a1.jsonl.partial"));
}

#[tokio::test]
async fn the_sweeper_only_claims_batch_files() {
let dir = tmpdir("filter");
Expand Down
Loading
Loading