Repository navigation
feat: routines — scheduled and API-triggered agent runs with a zero-cost check gate and real budgets - #382
Merged
Conversation
…t gate and real budgets A routine is a saved agent job: a prompt, a workdir, a backend, and the policy that makes running it unattended safe. It fires on a schedule (cron in an IANA timezone, an interval, or once), on its own API endpoint, or on demand. Each run is bounded four ways and recorded: - a check command runs first; no output means nothing to do, so the run ends `silent` without starting an agent and costs nothing — minute-level monitors are practical; - autonomous within a tool budget, or guarded so every non-read tool waits for the owner; a run that parks ends `needs_attention` and stays parked for them; - a wall-clock limit with a hard interrupt; - a per-run spend ceiling (the Claude SDK's native maxBudgetUsd on fresh run sessions, and the daemon's own check between turns everywhere), and a per-routine daily cap. Runs record outcome, cost, tokens and the agent's summary; [SILENT] means nothing to report. The routine pauses itself after repeated failures, notifies per its policy (content-blind push, and a new `routine_end` hook that can post to Slack), keeps the last N run sessions, and feeds the last summary to the next run. Fresh sessions per run (optionally in their own git worktree) or one persistent session; overlap skip/queue; catch-up after downtime; stable jitter. A fire is claimed by compare-and-set on the next fire time, so it can never run twice. Authority: routine:manage is a new owner-tier scope; changing a routine also needs session:create + session:send, and pre-approving work (autonomous, or a check command) needs session:approve. Runs act as the owner, delegated and named routine:<name>; audit rows name the routine. Surfaces: routine.* protocol messages, `codeoid routine add|edit|ls|show| runs|run|pause|resume|rm|cancel|token|preview`, POST /routines/<id>/fire with a per-routine token, and a `routines` config block with a kill switch. Refs #54 Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
… tokens count cache reads The live run showed three gaps: routine_end only fired when a push would, a needs_attention run stayed at $0 after the owner approved and the agent finished (so the daily cap undercounted), and input tokens excluded cache reads. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
…h tests Both guards survived mutation: the claim test raced two ticks that ran in sequence, and the timeout test passed whether or not the shell's children died. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
…TION, COMPARISON, hooks and CHANGELOG Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
…it cut more Dogfooding found the 16 KB cap too small for a real check (open issues plus history is 34 KB), and the truncation note understated what was dropped. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
…ole turn Dogfooding: a docs-audit run's summary opened with "Checking the config…" narration from between its tool calls. Like claude -p, the result is now the last text block of the turn. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
…safe session reuse and retention Security: a routine is its owner's alone (names unique per owner, others get not-found); every routine read needs routine:manage; delegated tokens can't change routines; the check runs with a built environment, never the daemon's secrets; edits are audited with what changed; run start and cancel are audited. Correctness: a persistent session that is parked or busy is not prompted (the fire is skipped); retention spares sessions that are attached, working or parked; CODEOID_ROUTINE_SINCE (was LAST_RUN_AT) is the start of the last run that handled its input, so a since-check never loses items; a turn with no text never reuses an older reply; hooks fire for skips, queued cancels and boot interruptions; a daemon stop ends runs interrupted and keeps queued runs queued across restart; cancel and stop kill a running check; pause/resume don't revalidate the spec; only resuming resets the failure streak; a paused routine's queued scheduled run is skipped; one-off times must be future ISO 8601 with a zone; a cron without --tz takes the daemon's zone (or keeps its own on edit); run history is capped at 500; one broken schedule can't stall the tick; --wait follows its run by id; an overlap-queue fire right behind a starting run is no longer skipped. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
…fe replies, trimming keeps what runs read Security: a run's session is set back to guarded when the run ends (and when a parked run settles), so nobody prompting it later gets pre-approved tool calls; firing or re-tokening a routine that pre-approves work needs session:approve; check commands are audited by digest, never text; qwen routines can't take an API trigger. Docs state the boundary plainly: the routine is owner-only, its run sessions are ordinary tenant sessions, and ownership follows the credential's identity. Correctness: a run recognises its reply by turn id, so context rotation shrinking history mid-run no longer drops it; history trimming never removes today's runs (the daily cap's sum), the newest summary, or the newest handled run; an abort after a check settled no longer signals a reused pid; prune re-examines every kept session and spares a former persistent one; a busy skip doesn't point a push at the session; a schedule that can't be evaluated pauses the routine instead of erroring every tick; the accumulator forgets its last text block when history is replaced. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
…n't need Live runs on Claude ended every summary with a paragraph about unrelated claude.ai connectors needing authorization. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
…nd qwen refused as the default too A run's session steps down from autonomous the moment the run ends or parks (and only from autonomous, so a mode the owner chose is kept), before any store write that could fail. An API trigger is refused for a routine that would run on qwen as the daemon's default backend, and an API run fails if the default has since become qwen. Only a schedule that can't be evaluated pauses a routine (other tick errors are logged), and resuming one gives a clear error. Prune scans every kept session. Pausing no longer needs session:approve. Docs name what send-only tokens can do in an autonomous run and warn against persistent + autonomous where they can reach it. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
Collaborator
Author
|
pr-shepherd: CI is green on 98aad5f (iteration 1). All 4 checks passed: daemon-check, web-check, Socket Security: Project Report, Socket Security: Pull Request Alerts. Nothing needed fixing and no commits were pushed. The branch is up to date with main. |
Comments: #routineHandle's scope rules, routine_end firing for every run, needs_attention meaning a tool approval, the spend-ceiling contract, the full placeholder list and --at's zone requirement in CLI help. Guide: the 16,000-character payload limit and 409 for a paused routine, the 4,000-char failed-check excerpt, what history trimming keeps, the complete audit list, the overlap-queue skip, and the native cap being fresh-session-only. Also marks P4.5 shipped in the conductor plan, and tidies a duplicate assignment and an indent. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
rsharath
approved these changes
Oct 16, 2026
Drop routine:manage. Holding routines takes the scopes a routine itself uses — session:create + session:send (reading included, so read-only watchers still can't see prompts, checks or payloads), plus session:approve for one that pre-approves work. Delegated tokens (teammates' share tokens, agents) still can't change routines, which is what kept standing authority out of shared hands. No ZeroID scope-profile change is needed, so Studio's embedded sign-in works with routines as is. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
- Remove RetryManager / src/daemon/retry.ts: deleted in af22d81; retry is the Claude Agent SDK's fallbackModel (session.fallbackModel / CODEOID_FALLBACK_MODEL). - Rewrite "Retry with fallback": no daemon-side backoff, error categorization, 3-attempt fallback, or indefinite unattended retry exists. - Scopes: 17, not 8; canonical source is packages/protocol/src/scopes.ts (src/protocol/* are re-export shims). List the missing scopes in Auth Model. - Backends: add llamacpp to the ProviderRegistry list, providers/ dir list, Tech Stack, and Sessions. - Scrollback buffer defaults: 5000 entries / 20MB, not 500 / 1MB. - `codeoid new <name> [workdir]`: workdir is optional when --worktree is given. Co-Authored-By: Claude Opus 5.5 <[email protected]>
The docs-drift routine's count was one short; its list was complete. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This was referenced Oct 16, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #54.
What this adds
Routines: saved agent tasks that codeoid runs on a cron or interval schedule, once at a set time, from their own API endpoint, or on demand.
The design goal is that most scheduled runs cost nothing:
silent: no agent starts and nothing is spent.[SILENT], and you hear nothing.Every run is an ordinary session. You can open it from the terminal, the web UI or your phone, then approve, continue or fork it.
The daemon enforces the limits, not the model:
maxBudgetUsd);API trigger.
POST /routines/<id>/firetakes a per-routinert_…token, which can be revoked, is rate-limited, and is stored only as a hash. Its body (up to 16,000 characters) reaches the agent as{{payload}}, delimited as data.Outcomes:
routine_endhook fires for every run.Full guide: docs/routines.md. It covers usage, the safety model, how this compares with Claude Code routines, Cursor Automations and Codex automations, and known limits.
What it does that the cloud products don't:
Authority model
previewneedssession:createandsession:send, reading included, so read-only watchers can't see prompts, checks or payloads.session:approve. That means an autonomous routine, or one with a check (a host shell command nobody approves at run time). Pausing never needs it.routine:<name>. Each one starts autonomous only for the run itself: the session steps down to guarded the moment the run ends or parks.Code tour
src/daemon/routines/:schedule.ts: croner 10.0.1 (exact pin, MIT, no deps), IANA zones, phase-anchored intervals, stable jitter, strict ISO for--at.store.ts: SQLite tables. A compare-and-set onnext_fire_atis the tick lock. History is trimmed, but never today's runs, the last summary, or the last handled run.service.ts: scheduler, admission, check gate, run loop, retention, management API, behind aRoutineHostseam.http.ts: the fire endpoint.wire.ts.src/daemon/session-manager.ts:#routineHandle: all authorization.#makeRoutineHost: sessions, worktrees, push, hooks.Session.lastAssistantMessageandCanonicalHistoryAccumulator.lastTextBlock: the run summary is the agent's final message.maxBudgetUsdis plumbed throughTurnOptsto the Claude provider;error_max_budget_usdmaps toBUDGET_STOP_MESSAGE.routine.*messages and results;SessionInfo.managedBy: "routine";"routine".codeoid routine add|edit|ls|show|runs|run [--wait]|pause|resume|rm|cancel|token|preview(src/cli.ts,src/terminal/routine-format.ts).routines.*,CODEOID_ROUTINES_ENABLED,CODEOID_ROUTINES_MAX_CONCURRENT, plus a Settings group.Verification
Tests:
The full suite passes: 3249 pass, 0 fail, 19 skipped.
bun run typecheck(root, protocol, core), the web typecheck andbiome checkare all clean.Routine suites: 92 tests.
routine-schedule: cron/DST, intervals, once.routine-service: a scripted host with a fake clock, covering every scheduling, gating, budget, outcome, retention and authority-left-behind rule.routine-manager: a real SessionManager on a mock backend, covering scopes, ownership, delegation, the HTTP trigger, worktrees and the hook.routine-format: CLI parsing and output.Protocol and schema cases for every routine message, in addition to those 92.
Mutation-checked guards. I broke each of these in turn and confirmed a test fails:
The CAS and process-group guards first survived mutation; I added tests that catch them.
Run live against real Claude, through the real
codeoid routineCLI, on an isolated local-mode daemon:silent, $0;succeededwith a correct summary;[SILENT]reply →silent;--every 1mschedule firing on its own;curlto the API endpoint → 202; a bad token → 401; anOriginheader → 403;needs_attentionand the file not created. Aftercodeoid approve, the file was written and the run's cost and summary updated;--max-cost 0.01→over_budget;routine_endhook payload;--atrejected;The live runs found five fixes, all included:
Dogfooded on this repo. These routines ran against
main:ci-watch: silent at $0, because no PR is failing.stale-issues: guarded, with the issue list and history from the check. It found 8 open issues that look resolved, 3 with high confidence (Wire multi-provider selection into runtime (Gemini/OpenAI selectable, Claude default) #40, perf: cache MemoryEngine vector matrix instead of reloading per recall #5, Protocol hardening (audit, medium): inbound zod validation, unknown-type default, WS payload/backpressure limits #15). The list is for a maintainer to act on; nothing was closed.docs-drift: autonomous, in a worktree. It found and committed 6 real CLAUDE.md errors: the retry module that no longer exists, 17 scopes rather than 8, the missing llamacpp backend, scrollback defaults, and two more. That commit is in this PR (96e1e99). Its scope count was one short (there are 18), corrected in 256222a.Dogfooding also found that the 16 KB check-output cap was too small; it is now 64 KB, with an honest truncation note.
Audit rounds:
Round 1 (correctness, security, cross-repo impact):
All fixed in 18156d5. Cross-repo: no consumer breaks (details below).
Round 2:
All fixed in d9c9f96.
Round 3: correctness was clean ("no Critical, High or Medium I would fix before merge"); security found no merge blockers. Its Lows and the send-only docs were folded into 98aad5f.
Final audit (whole PR, every claim in this body and the docs checked against the code): nothing to change in the code; "ready for human review once" 4 doc and comment fixes went in. Those, plus 20 polish items, are in bd76bb0.
What did not run:
kind: "routine". Cross-repo review found the app routes bysessionIdand ignoreskind.enabled_repos.Follow-ups (not in this PR)
/mcp/controland the conductor: feat: routines in the web UI, the conductor, and /mcp/control #383.session:approveshould have its tool calls run guarded. This is not new with this PR, but routines make autonomous sessions more common.codeoid loginkey is the key's identity. Consider keying on the human principal, if ZeroID carries it.@highflame/codeoid-protocolto the next minor version on release.🤖 Generated with Claude Code