fix(routines): surface errors when sandbox unavailable for full_job routines (#769)

* feat(db): add list_dispatched_routine_runs to RoutineStore trait

Add method to query routine runs with status='running' AND job_id IS NOT NULL,
enabling the routine engine to sync completion status from background jobs.
Implements for both PostgreSQL and libSQL backends.

[skip-regression-check]

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix(routines): sync dispatched full-job runs with background job status (#697)

Full-job routines were immediately marked Ok on dispatch, so
failures/completions were never reflected in the routine run record.
Now dispatch returns Running status, and a periodic sync checks linked
jobs to update the run when the job completes, fails, or is cancelled.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix(routines): fail fast when sandbox unavailable at dispatch time (#697)

Thread sandbox_available bool from Docker detection through AgentDeps
to RoutineEngine. Full-job routines now fail immediately with a clear
error message when sandbox is enabled but Docker is not available,
instead of dispatching a job that silently fails.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat(startup): notify user when sandbox unavailable (#697)

When sandbox is enabled but Docker is not installed or not running,
send a user-visible warning through all channels at startup (with a
2s delay to let channels connect). Previously this was only logged
via tracing::warn, invisible to TUI/web users.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* style: fix formatting in routine_engine.rs

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix(tests): set sandbox_available=true in test rig for full_job traces

Test rig doesn't use real Docker — full_job routines execute via trace
replay. Setting sandbox_available=true allows the routine_news_digest
trace test to dispatch full_job routines as before.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix(routines): address review feedback on sync_dispatched_runs (#697)

- Sanitize last_reason from job transitions before using in
  notifications (truncate to 500 chars, strip control characters)
- Treat Submitted as in-progress (can still transition to Failed),
  only Completed and Accepted are terminal success states
- Add test for sanitize_summary

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix(tests): add missing sandbox_available field to test constructors

Staging added sandbox_available to AgentDeps and RoutineEngine::new.
Add the missing field/argument in test files to fix CI compilation.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: sanitize job reason in notifications, fix state handling for Submitted/Accepted

- Enhance sanitize_summary to strip HTML tags and collapse whitespace,
  preventing injection via untrusted container job reasons
- Use char-boundary-safe truncation to avoid panics on multi-byte strings
- Treat Submitted and Accepted as in-progress states (continue polling)
  rather than terminal success, since they can still transition to Failed
- Increase channel-connect delay from 2s to 5s and add debug log for
  sandbox-unavailable warning delivery

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* Replace sandbox_available bool with SandboxReadiness enum

Distinguishes DisabledByConfig from DockerUnavailable so full-job
routine errors give actionable guidance instead of a generic message.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* ci: re-trigger CI with latest changes

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: add missing owner_id arg to send_notification call

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: update e2e tests to use SandboxReadiness enum

Co-Authored-By: Claude Opus 4.6 <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
Co-authored-by: [email protected] <[email protected]>
This commit is contained in:
Zaki Manian
2026-03-19 21:20:41 -07:00
committed by GitHub
co-authored by Claude Opus 4.6 [email protected] <[email protected]>
parent 8526cde1be
commit 455f543ba5
11 changed files with 256 additions and 2 deletions
+10 -1
View File
@@ -20,7 +20,7 @@ mod tests {
RunStatus, Trigger,
};
use ironclaw::agent::routine_engine::RoutineEngine;
use ironclaw::agent::{HeartbeatConfig, HeartbeatRunner, Scheduler};
use ironclaw::agent::{HeartbeatConfig, HeartbeatRunner, SandboxReadiness, Scheduler};
use ironclaw::channels::IncomingMessage;
use ironclaw::config::{AgentConfig, RoutineConfig, SafetyConfig};
use ironclaw::context::{ContextManager, JobContext};
@@ -266,6 +266,7 @@ mod tests {
Some(scheduler),
registry,
safety,
SandboxReadiness::DisabledByConfig,
))
}
@@ -346,6 +347,7 @@ mod tests {
None,
tools,
safety,
SandboxReadiness::DisabledByConfig,
));
// Insert a cron routine with next_fire_at in the past.
@@ -423,6 +425,7 @@ mod tests {
None,
tools,
safety,
SandboxReadiness::DisabledByConfig,
));
// Insert an event routine matching "deploy.*production".
@@ -516,6 +519,7 @@ mod tests {
None,
tools,
safety,
SandboxReadiness::DisabledByConfig,
));
let routine = make_routine(
@@ -623,6 +627,7 @@ mod tests {
None,
tools,
safety,
SandboxReadiness::DisabledByConfig,
));
let mut filters = std::collections::HashMap::new();
@@ -764,6 +769,7 @@ mod tests {
None,
tools,
safety,
SandboxReadiness::DisabledByConfig,
));
// Insert an event routine with 1-hour cooldown.
@@ -949,6 +955,7 @@ mod tests {
None,
tools,
safety,
SandboxReadiness::DisabledByConfig,
));
(engine, db, dir)
@@ -1078,6 +1085,7 @@ mod tests {
None, // no scheduler — rejected before dispatch
tools,
safety,
SandboxReadiness::DisabledByConfig,
));
// Create a full_job routine with max_concurrent = 1
@@ -1186,6 +1194,7 @@ mod tests {
None,
tools,
safety,
SandboxReadiness::DisabledByConfig,
));
// Insert a due cron routine
+1
View File
@@ -198,6 +198,7 @@ mod tests {
http_interceptor: None,
transcription: None,
document_extraction: None,
sandbox_readiness: ironclaw::agent::SandboxReadiness::DisabledByConfig,
builder: None,
};
@@ -257,6 +257,7 @@ impl GatewayWorkflowHarness {
http_interceptor: None,
transcription: None,
document_extraction: None,
sandbox_readiness: ironclaw::agent::SandboxReadiness::DisabledByConfig,
builder: None,
},
channels,
+2
View File
@@ -578,6 +578,7 @@ impl TestRigBuilder {
None,
components.tools.clone(),
components.safety.clone(),
ironclaw::agent::SandboxReadiness::Available, // tests don't use real Docker
));
components
.tools
@@ -642,6 +643,7 @@ impl TestRigBuilder {
},
transcription: None,
document_extraction: None,
sandbox_readiness: ironclaw::agent::SandboxReadiness::Available, // tests don't use real Docker
builder: None,
};