Design: tinyclaw cron surface (#3147) + jmap relocation (#3198)
Date: 2026-08-12. Discipline: disciplined-design. Research: research-tinyclaw-cron-and-jmap.md.
Design goals
- #3198: eliminate the only workspace-escaping path dep; consume
haystack_jmapfrom the terraphim registry; move the crate out of terraphim-private. Zero behaviour change to the email channel (only the constructor signature + search limit). - #3147: give users (human CLI + agent loop + skills) a scheduling
surface over the existing
CronStore, without new dependencies and without coupling to the down orchestrator.
3198 — jmap relocation
Cargo.toml (tinyclaw)
# BEFORE
jmap_client = { path = "../../../terraphim-private/crates/haystack_jmap" }
# AFTER (replace the comment block + line)
haystack_jmap = { version = "1.20.2", registry = "terraphim" }email.rs changes
use ; // was use jmap_client::…
pub use ;
// connect()
let client = new.await?;
if self.config.jmap_session_url.is_empty
// search_emails — bounded
const SEARCH_LIMIT: u32 = 20;
Some => Ok,Tests: struct literals unchanged (field-identical). Add one test for
connect() failing on empty session URL (no network).
terraphim-private
git rm -r crates/haystack_jmap + commit + push. haystack_core stays
(consumed by atlassian/discourse/grepapp in private).
3147 — scheduling surface
Components
-
ScheduleTool(src/tools/scheduler.rs, registered as"schedule"):create {prompt, schedule, skills?, deliver?, model?}→Schedule::parse(rejects invalid cron with clear message) → buildCronJob::new(prompt, schedule)+ optional fields → store. Returns{op, id, schedule, status: "created"}.list→CronStore::load_all→{op, count, jobs:[…]}.delete {id}→ remove by id →{op, id, status: "deleted"}.- Store key
"tinyclaw_schedules";DeviceStorage::arc_instance()at construction (with_storage(storage, key)builder for tests;from_configusesarc_instance()with graceful degradation). - Registered in
create_default_registry_with_paritywhen[scheduler] enabled = true(config sectionSchedulerConfig { enabled, store_key }).
-
CLI (
main.rs):
Handlers build CronStore from DeviceStorage::arc_instance() (await),
reuse the same logic as ScheduleTool (shared helper functions so CLI and
tool can't drift: create_job(store, prompt, schedule, …),
list_jobs(store), delete_job(store, id) in src/tools/scheduler.rs).
-
SkillStep::Schedule(src/skills/types.rs):
Schedule ,Executor (src/skills/executor.rs):
- new field
cron_store: Option<CronStore>+with_cron_store()builder. - dispatch: build
CronJobwhosepromptisformat!("run skill {skill} with inputs {inputs}")(or better: storeskill+inputsin the job'sskillsvec + prompt text), schedule fromSchedule::parse(cron), persist. Error if store unset. step_type_name+ tests updated.
Wiring
main.rsagent/gateway path: constructCronStoreonce (like dashboard does) and pass to bothScheduleTool(via registry factory) andSkillExecutor::with_cron_store.create_default_registry/_with_paritysignatures:ScheduleToolneeds the storage — addstorage: Option<Arc<DeviceStorage>>param to the parity registry factory (or construct from config inside; keep the factory async already). Decision:create_default_registry_with_paritygainsstorage: Option<Arc<DeviceStorage>>;from_configpath callsarc_instance()itself. Minimal churn: current callers passNone.
Test plan
tests/scheduler_contracts.rs: create (valid cron + invalid cron rejected), list round-trip, delete, persistence across store re-creation (memory-only DeviceStorage).- Executor:
SkillStep::Schedulewith store → job appears in store; without store → clear error. - CLI: covered via the shared helpers (no subprocess tests; keep CLI thin).
Acceptance mapping
| #3147 criterion | How met |
|---|---|
| "schedule daily-report skill every day at 09:00" → schedule ID | schedule create --prompt … --schedule "0 9 * * *" returns {id} |
| schedule list shows active schedules | ScheduleTool list + CLI |
| schedule delete <id> removes | delete |
| Invalid cron rejected with clear message | Schedule::parse error |
| Integration test create/list/delete | scheduler_contracts.rs |
| Orchestrator not running: fails fast | n/a — we persist locally (documented deviation) |
| Duplicate name / timezone | cron is UTC; no names in v1 (id-based), documented |
| #3198 criterion | How met |
|---|---|
| No workspace-escaping path dep | haystack_jmap from terraphim registry |
| Fresh clone builds | registry dep; terraphim-private not needed |
| Crate removed from private | git rm crates/haystack_jmap in terraphim-private |