Orion’s background work lives in the cron service. It dispatches LLM jobs through one of two engines based on payload.model, applies AI-WITH-YOU gates for autopilot-typed jobs (but NOT plain cron), and routes external work to other CLIs (Gemini, Codex, Claude CLI, Qwen, opencode) through delegate_to_cli.

Cron job lifecycle — dual-engine

Routing logic at cron/executor.mjs:260, 273, 283-290. Three-tier model resolution: payload.model > user_preferences[stage_key] > BACKGROUND_MODEL_DEFAULTS. ✓ VERIFIED

Cancellation contract

Engine-branched cancellation lives in cron-service.mjs:1825-1862:
const childRef = _runningChildren.get(jobId);
// shape: { process, abortController, aborted }
childRef.aborted = true;  // synchronous flag BEFORE async cancellation
if (childRef.process)             gracefulKill(childRef.process);  // Claude SDK
else if (childRef.abortController) childRef.abortController.abort(); // Pi
else                              orphan-DB-cleanup-only;            // pathological
The _runningChildren map shape is mutually exclusive — exactly one of process or abortController is set per entry. The synchronous aborted=true flag prevents races with in-flight emit calls. ✓ VERIFIED

Per-task model preferences

Five user_preferences keys, read once at sidecar startup (restart-to-take-effect):
KeyDefault (BACKGROUND_MODEL_DEFAULTS)FallbackConsumed by
heartbeat_modelgoogle/gemini-2.5-flashclaude-haiku-4-5-20251001cron/heartbeat.mjs
enrichment_modelgoogle/gemini-2.5-flashclaude-haiku-4-5-20251001cron/enrichment-pipeline.mjs
consolidation_modelgoogle/gemini-2.5-flashclaude-haiku-4-5-20251001cron/consolidation-engine.mjs
dream_modelgoogle/gemini-2.5-flashclaude-haiku-4-5-20251001future BRS-memory-evolution Phase 4
cron_default_modelclaude-haiku-4-5-20251001claude-sonnet-4-6cron/executor.mjs (when payload.model omitted)
Constants live in config/constants.mjs:80-97. Resolution via lib/preferences.mjs:69. ✓ VERIFIED

Heartbeat fallback chain

runHeartbeatWithFallback in heartbeat.mjs:881-891 triggers automatic Claude Haiku retry on two failure modes:
  1. Pi auth / safety errors — matched by predicate isHeartbeatFallbackError at L797-806: /no model found|API key|unauthorized|no auth/i
  2. Empty responseEMPTY_QUERY_RESULT sentinel from pi-query-lifecycle.mjs:124 (typically Gemini safety filter)
The fallback strips effort from payload (Pi-specific field) at L906 because Claude doesn’t understand it. ✓ VERIFIED

Recipe-mode guard

Recipe-mode jobs (payload.executionMode === 'recipe') are always routed to Claude SDK regardless of payload.model. Setting model: 'google/gemini-2.5-flash' on a recipe job is silently overridden at executor.mjs:273. Recipes require SDK behavior. ✓ VERIFIED

Autopilot — three-table model & 3-layer gate

Autopilot is the AI-WITH-YOU gate enforcement engine for background work. It splits “what” / “when” / “proposal” across three tables (migration 0046 C2):

The three enforcement layers

LayerWhat it enforcesWhere
L1 — DDL CHECKacceptance_mode IN ('suggest','auto') + DEFAULT ‘suggest’Migration 0045
L2 — Runtime JSBranch on acceptance_mode; suggest → proposal onlyautopilot/runner.mjs:386-403, 412-514
L3 — Rust command guardassert_auto_opt_in rejects auto without explicit_user_opt_in=trueautopilots.rs:172-185
✓ VERIFIED at all three layers. Lock-in tests at autopilots.rs L749-761.
GAP-D2: Plain cron jobs bypass the AI-WITH-YOU gate.The 3-layer gate only fires for payload.type === 'autopilot' cron jobs — those get short-circuited into autopilot/runner.mjs at executor.mjs:260. Plain cron jobs (payload.message directly, no autopilot wrapper) run end-to-end on cadence with no proposal/acceptance phase.A user creating a cron job with payload.message: "Send my weekly digest to Slack" and schedule 0 9 * * MON gets full auto-execution: LLM fires, Slack MCP tool sends digest, user has no acceptance step. Tools still go through canUseTool (Claude) or the Pi middleware — but the run itself is uninterruptible from the user’s perspective.This matches the documented model in cron-engine-routing.md (cron is “fire-and-forget by design”) but the handoff specifically demanded this be visible. See Trust → GAP-D2.

CCW multi-CLI bus

Orion delegates external work to other CLIs via two MCP tools in mcp__orion-tools:
ToolModeWhat it does
delegate_to_cliFire-and-forgetReturns immediately with delegation ID; background work continues
execute_cli_inprocessAwaitingBlocks until CLI exits; returns the output
Both call ccw/cli/executor.mjs::execute(), which spawns the chosen CLI subprocess.
GAP-D1: Multi-CLI dispatch has no OS sandbox.Spawned CLIs receive:
  • Sanitized env via buildSpawnEnv() (strips 28 Orion-internal vars — executor.mjs:128-175)
  • Caller-supplied cwd (defaults to process.cwd())
  • 10-minute timeout
  • No sandbox-exec, no bwrap, no @carderne/sandbox-runtime
  • No canUseTool integration — auto-allow at tool dispatch (internalServers auto-trust)
  • No path policy / no .env/*.pem hard-blocks
Explicit asymmetry:
  • Pi’s own Bash → wrapped by @carderne/sandbox-runtime + denyWrite hard-blocks
  • Claude SDK’s own Bash → sandbox-exec profile with additionalDirectories allowlist
  • CCW external CLI spawn → neither
A mode: 'write' Codex or Gemini execution can write to any path the sidecar can access — including outside the vault. The 28-key env strip protects Orion’s credentials, but filesystem write scope = sidecar’s permissions. See Trust → GAP-D1.
GAP-D4: Claude CLI deliberately inherits user’s ~/.claude/.executor.mjs:645-649 explicitly strips CLAUDE_CONFIG_DIR from the spawned Claude CLI’s env. The comment documents this is intentional: Claude CLI falls back to ~/.claude/ (user’s own Claude Code config), not Orion’s runtime config (prompts/).This means:
  • User’s personal skills, hooks, settings.json, MCP servers — all load into the spawned Claude CLI.
  • Spawned CLI uses user’s OAuth tokens (not Orion’s Cloudflare-Worker-proxied tokens).
  • Billing flows through user’s account, not Orion’s.
This is opposite to the rest of Orion’s isolation architecture (CLAUDE_CONFIG_DIR=prompts/ keeps user config out of Orion runtime). When Orion delegates to Claude CLI, the CLI operates with the user’s full personal context. Users should know that “delegate to Claude CLI” is fundamentally different from “Orion’s Claude SDK” in trust posture.

Cron model selection guide

  • Routine summary, digest, scan, monitor
  • Cost-sensitive jobs running many times/day
  • Local-only file ops in vault
  • Ambient: heartbeat, enrichment, consolidation
  • ~36× cheaper than Sonnet per run

Key files

executeIsolated dispatcher, three-tier model resolution at L283-290, recipe-mode guard at L273, autopilot short-circuit at L260, 10s watchdog, VAL-007 post-settlement guard.
_runningChildren map at L243, cancelJob engine-branched cancellation at L1825-1862.
isHeartbeatFallbackError predicate L797-806, runHeartbeatWithFallback L881-891.
_fireSuggestPath L386-403 (proposed-only), _fireAutoPath L412-514 (atomic txn).
assert_auto_opt_in Rust guard L172-185. Callers at L453 (create), L508 (update). Lock-in tests at L749-761.
delegate_to_cli L725-750. execute_cli_inprocess L773-823.
buildSpawnEnv 28-key strip L128-175. spawn() with no sandbox wrapper L505-700. Claude CLI deliberate non-override L645-649.
BACKGROUND_MODEL_DEFAULTS/FALLBACKS L80-97. BACKGROUND_MODEL_STAGE_KEYS L204-235.

Next

Trust → Known Gaps

The consolidated safety appendix. All gaps from every page with severity, evidence, fix paths, and links to filed issues.