* feat: structured fallback deliverables for failed/stuck jobs (#221)
When a job fails or gets stuck, build a FallbackDeliverable that captures
partial results, action statistics, cost, timing, and repair attempts.
This replaces opaque error strings with structured data users can act on.
- Add FallbackDeliverable, LastAction, ActionStats types in context/fallback.rs
- Store fallback in JobContext.metadata["fallback_deliverable"] on failure
- Surface fallback in job_status tool output and SSE job_result events
- Update mark_failed() and mark_stuck() in worker to build fallback
- 8 unit tests covering zero/mixed actions, truncation, timing, serialization
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address review comments on fallback deliverables
- Fix doc comment: "200 chars" -> "200 bytes (UTF-8 safe)" since
truncate_str operates on byte length, not character count.
- Add code comment documenting that SSE fallback_deliverable is
currently always None (forward-compatible infrastructure).
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* refactor: take Option<&FallbackDeliverable> instead of &Option<…>
Addresses Gemini review feedback: idiomatic Rust prefers
Option<&T> over &Option<T> for borrowed optional values.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: guard against non-object metadata and add fallback test
- store_fallback_in_metadata now resets metadata to {} when it's any
non-object type (string, array, number), not just null. Prevents
panic on index assignment.
- Add test_job_status_includes_fallback_deliverable to verify the
fallback field is surfaced in job_status tool output.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: use sanitized output in fallback preview + add integration tests
Security fix: FallbackDeliverable::build() now uses output_sanitized
instead of output_raw, preventing secrets/PII from leaking through
the job_status tool and SSE job_result events.
Also adds:
- test_fallback_uses_sanitized_output: proves raw secrets don't leak
- test_store_fallback_in_metadata_roundtrip: full serialize/deserialize
- test_store_fallback_handles_non_object_metadata: edge case coverage
- test_store_fallback_none_is_noop: None input is safe
Addresses serrrfirat review feedback on PR #236.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: harden fallback deliverables against review findings
- Truncate failure_reason to 1000 bytes to prevent metadata bloat
- Add tracing::warn on fallback serialization failure (was silently discarded)
- Fix module/struct docs to cover stuck jobs, remove stale SSE claim
- Fix job.rs test to use real FallbackDeliverable field names
- Add tests for failure_reason truncation and completed_at=None elapsed time
- Fix pre-existing clippy warning in settings.rs (field_reassign_with_default)
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address Copilot review findings on fallback deliverables
- Fix output_raw/output_sanitized field swap in ActionRecord::succeed()
so sanitized data actually goes into the sanitized field (security)
- Return None instead of empty Memory when get_memory fails in
build_fallback, with tracing::warn for observability
- Replace manual elapsed calculation with ctx.elapsed() which already
clamps negative durations
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: resolve rebase conflicts and update tests for parameter swap
- Add fallback field to SseEvent::JobResult in job_monitor
- Fix type annotation in fallback deliverable test
- Update test_action_record_succeed_sets_fields for new parameter order
- Use create_job_for_user in test (API changed on main)
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* chore: trigger CI re-check after rebase
* fix: fall back to error message for failed action output_preview
When the last action is a failed tool call, output_sanitized is None,
leaving output_preview empty. Now falls back to the action's error
message so users see what went wrong.
[skip-regression-check]
* ci: add safety comments to test code for no-panics check
The CI no-panics grep check cannot distinguish test code inside
src/ files from production code. Add // safety: test annotations
to .unwrap(), .expect(), and assert!() calls in #[cfg(test)] modules.
* fix: clarify succeed() doc and avoid clone in output_preview
- Fix doc comment: output_raw is stored as pretty-printed JSON string,
not a raw JSON value
- Borrow string slice directly in fallback preview to avoid cloning
potentially large sanitized outputs before truncation
* refactor: reuse floor_char_boundary in truncate_str
Replace hand-rolled UTF-8 boundary logic with existing
crate::util::floor_char_boundary to reduce duplication.
* fix: rename SSE fallback field to fallback_deliverable for consistency
The SSE JobResult field was named `fallback` while everywhere else
(metadata key, job_status tool) uses `fallback_deliverable`. Align
the SSE wire format to avoid forcing clients to handle two names.
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
Address three deferred implementation items flagged during code review:
1. SIGHUP lock held across .await (#883): Split restart_with_addr into
merged_router_clone() + install_listener() so the async TcpListener
bind happens outside the mutex, eliminating lock contention risk.
2. Recursion depth limit for check_strings (#848): Cap JSON traversal
at 32 levels to prevent stack overflow on pathological tool params.
3. Named error type for add_tokens (#788): Replace Result<(), String>
with TokenBudgetExceeded { used, limit } for type-safe budget errors.
Co-authored-by: Claude Opus 4.6 <[email protected]>