Durable cron occurrences¶
Timer dispatch records a frozen occurrence, its pending history entry and the schedule's next clock value in one SQLite transaction. A failed transaction advances neither the clock nor execution. The occurrence ID derives from the job ID and scheduled UTC instant. Concurrent schedulers and restart recovery converge on the same protected scheduler claim before queue admission.
The frozen request includes agent source/configuration, input, schedule policy, identity credential, session mode and delivery configuration. Each session ID comes from the occurrence ID. Clock/counter changes do not change retry identity. Jitter is deterministic for that occurrence and elapsed time is retained across restart. A new future occurrence receives a different ID.
Manual triggers¶
# Generate once and retain with the request.
INVOCATION_ID=$(cat /proc/sys/kernel/random/uuid)
curl -X POST "http://localhost:8080/api/v1/schedules/$JOB_ID/trigger" \
-H "Authorization: Bearer $SYMBIONT_API_TOKEN" \
-H "Idempotency-Key: $INVOCATION_ID"
The caller must have administrative authority. The UUID is bound to the verified
caller, job and frozen execution request. Another caller, job, policy or source
cannot reuse it. Responses use the existing scheduler retry states:
queued, verified saved completion/failure, in_progress, unresolved, or
conflict. A missing, malformed or repeated header is refused before intent
creation. A manual trigger uses the job's persisted input.
A service or queue refusal is not proof that no intent was accepted: a prepared intent may already be durable, or a partial claim may remain unresolved. Retain and reuse the original UUID. A new UUID represents new work.
Trusted SDK callers use trigger_identified. trigger_now intentionally creates
a new ID and awaits actual completion and cleanup; calling it again is new work.
Install AgentPin and schedule policy guards using new_with_guards, before any
recovery or timer dispatch. Builder setters affect future checks but cannot
retroactively guard work started during construction.
The attached shell supports /cron run <job-id> [invocation-id]. It displays the
UUID and an exact retry/status command for both responses and transport errors.
Omitting the UUID starts new work; retain it for retries. /cron list displays
complete job IDs and /cron history <job-id> includes persisted audit references.
Recovery and operator outcomes¶
| Persisted state | Recovery behavior |
|---|---|
| Prepared occurrence, no execution claim | Rechecks current execution guards and admits the frozen intent once. |
| Claim still owned by a live process | Leaves it in progress. |
| Verified saved execution, stale schedule history | Repairs history from the original result and audit; performs no new effects. |
| Claim abandoned without a verified result | Records Unresolved, disables the job and moves it to DeadLetter. |
| Started occurrence whose claim is missing | Records Unresolved; does not manufacture a replacement claim. |
| Legacy pending/running history without an occurrence identity | Preserves it as Unresolved and disables its job. |
History exposes the occurrence UUID as run_id. admission_audit.run_id identifies
the protected execution journal; execution contains the saved result, usage and
shared budget when available. Pending means accepted durable intent, Running
means admission began, and Succeeded requires completed execution. An admission
audit reference can exist before its journal contains a Started record; missing
or incomplete evidence cannot establish success.
Unresolved history prevents timers, manual new work and resume for that job.
Inspect the original signed journal using symbi audit inspect and verify external
effects before deciding on remediation. Changing job status or deleting a lease
is not reconciliation. Use operator reconciliation
to persist a signed assessment bound to the original evidence. History becomes
Reconciled with a separate receipt; the job remains paused for explicit resume.
The original occurrence cannot execute again. Keep the retained occurrence, claim
and journal evidence; automatic replay cannot resolve uncertainty.
Per-job/global cron caps count durable prepared/running occurrences. Recovery rotates through bounded pages so live owners cannot indefinitely hide later intents. Shared sandbox capacity remains an additional gate. Retained occurrence storage is bounded to 4,096 records, 1 MiB per frozen request and 64 MiB of request bytes. Exhaustion refuses new intents; deleting old identities to free space would discard retry protection. Archival with durable deduplication is future work.
Project ownership and migration¶
The default is now <execution-project>/.symbiont/cron_jobs.db. Both its parent
directory and database are private runtime authority (0700 directory, 0600 files
on Unix). Linked files and unsafe SQLite sidecars are refused. The .symbiont
path is excluded from governed per-operation filesystem grants.
Each store binds to one canonical execution project. A runtime from another
project refuses to use it. An explicit CronSchedulerConfig.job_store_path must
also be private and excluded from all worker mounts. Merely making a directory
0700 does not protect it from a worker granted that directory under the same UID.
Offline symbi cron commands use the same project store. cron add loads one
bounded regular source file from agents/, validates its selected policy and
sandbox, and persists the complete source. Linked or ambiguous sources are
refused. Offline resume also checks the retained unresolved-history barrier.
The former global $XDG_DATA_HOME/symbi/cron_jobs.db is left untouched. It is not
automatically imported or executed. To migrate, stop its old scheduler, preserve
the database and journals, inspect which project owns each definition, then
configure an appropriate protected store explicitly. The first explicit binding
claims that database for its selected project; later mismatches are refused.
Existing unfinished legacy history becomes unresolved during migration.
DSL source schedules loaded by symbi up use stable source-file/schedule-name
identities. Reopening an unchanged definition preserves its clock, disabled or
terminal state and history. Changed definitions are reported for explicit review;
the persisted definition remains authoritative. Renaming a source file or schedule
creates a new identity and does not retire the old job. Remove or pause superseded
jobs deliberately after inspecting their outcomes.
Validation and scope¶
scripts/test-cron-occurrences.py uses the shipping HTTP service, a local scripted
provider and actual Docker workers. It checks useful outputs, concurrent manual
retries, timer identity, restart recovery, policy/caller conflicts, and a SIGKILL
after output publication. A second crash occurs during deterministic jitter after
intent persistence; the fixture advances elapsed jitter time before recovery.
A stale-history fixture verifies recovery from a retained signed completion.
Observers and audit keys remain outside worker file grants.
Focused tests also exercise transaction rollback, concurrent SQLite connections, legacy migration, project ownership, immutable source registration, startup guards and retained policy refusals. These tests do not establish exactly-once behavior for arbitrary external services or an adaptive model escape rate. Other execution routes, interrupted file-publication recovery and provider reservation recovery still need work. Operator decisions are supported through the separate signed reconciliation workflow.