* fix(cycle): budget the patterns subagent from remaining job time, not a fixed constant (#2781)
The autopilot-cycle job gets an interval-derived timeout stamped at submit,
but the patterns phase submits its subagent with a fixed 30-min job timeout
and waits up to 35 min — one phase's worst case exceeds ANY interval-derived
budget <= 35 min, so the parent job dead-letters mid-patterns and the tail
phases (consolidate -> schema-suggest) starve for days (#2781, the deeper
half left open by the #2852 dispatch-floor fix).
- MinionJobContext.deadlineAtMs: absolute deadline from the claim-time
timeout_at stamp (the DB ground truth handleTimeouts() sweeps against;
re-stamped on every claim so retries get a fresh budget). Null when the
job has no per-job timeout.
- worker: the per-job abort timer now derives its delay from timeout_at
when present, so the in-process timer, the DB sweeper, and the
handler-visible deadline agree on ONE absolute instant.
- autopilot-cycle + autopilot-global-maintenance handlers thread
deadlineAtMs into runCycle; CycleOpts carries it to the patterns phase.
- patterns: clampSubagentBudgets() derives BOTH the child job timeout and
the wait timeout from the same child deadline (parent deadline minus a
60s stop-margin reserve — enough for the wait poll + force-evict grace
+ cleanup, deliberately NOT a promise that tail phases complete). Under
a 2-min minimum the phase skips honestly (insufficient_cycle_budget)
instead of submitting a guaranteed-kill LLM call; the next cycle
retries with a fresh budget.
- Direct callers (gbrain dream) pass no deadline and keep the configured
timeouts unchanged.
Follow-up (separate PR): synthesize has the same shape plus sequential
per-child waits that accumulate N x subagent_wait_timeout_ms past any
parent budget; it needs per-wait remaining-time recomputation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RaNDjecPPvhC7LRqkxkhGF
* fix(cycle): address review — cancel timed-out patterns child; thread deadline through phase-wrapper handlers
- P1: the child's timeout_ms clock starts at ITS claim, so a queued child
could outlive the parent deadline the wait was clamped to. On wait
timeout, cancelJob strips it (waiting -> cancelled; active -> lock
stripped, worker abort fires next renew tick).
- P2: makePhaseHandler (standalone patterns/synthesize/... minion jobs)
now threads job.deadlineAtMs into runCycle like the autopilot handlers.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RaNDjecPPvhC7LRqkxkhGF
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>