Research: tinyclaw cron surface (#3147) + jmap crate relocation (#3198)
Date: 2026-08-12. Discipline: disciplined-research.
3147 — cron scheduling surface for TinyClaw
Existing state (already built in Wave 3, epic #3160)
crates/terraphim_tinyclaw/src/cron/ is a complete cron subsystem:
job.rs—CronJob(id, name, prompt,Schedule, skills, deliver, repeat, state, enabled, next/last run, model/provider overrides, script),Scheduleenum withparse()accepting 4 formats:every <dur>, RFC3339 timestamp, 5/6-field cron (padded with leading0seconds for thecroncrate), relative delays (30m,2h,1d). Invalid cron →CronError::InvalidSchedule.store.rs—CronStoreoverterraphim_persistence::DeviceStorage(fastest_op, opendal), same pattern as the subagent registry.scheduler.rs—CronSchedulerwithJobExecutortrait,tick(), pause, repeat exhaustion.CronErrorenum.
The dashboard (src/dashboard/) already exposes the Hermes cron REST
contract: POST /api/cron/fire, GET/POST /api/cron/jobs,
GET/DELETE /api/cron/jobs/{id} — ported from web_server.py + cron/jobs.py.
DashboardState owns a CronStore (memory-only for hermetic tests).
What #3147 actually needs (delta)
The issue text predates the Wave-3 cron subsystem and assumes nothing exists. What's genuinely missing:
- CLI:
terraphim-tinyclaw schedule create/list/delete— noSchedulesubcommand inCommands(main.rs) today. - Agent-loop tool:
ScheduleTool(src/tools/scheduler.rs) so the model can create recurring schedules in conversation. SkillStep::Schedulevariant (src/skills/types.rs) + executor dispatch.
Orchestrator coupling — decision
The issue says "validate with terraphim_orchestrator::is_cron_schedule_valid"
and "persist via terraphim_orchestrator cron mechanism". Findings:
terraphim_orchestratoris excluded from the workspace (Cargo.tomlexclude list, line ~30) and consumed from theterraphimregistry. The localcrates/terraphim_orchestrator/dir is a residual (no workspace root;cargo checkthere fails "failed to find a workspace root"). It also has two versions in Cargo.lock (1.20.2 terraphim registry + 1.20.3 crates.io) —cargo check -p terraphim_orchestratoris ambiguous. Adding it as a tinyclaw dep is a heavyweight, ambiguous dependency for one function.is_cron_schedule_validwrapsparse_cron(cron-crateScheduleparse, 5/6/7-field). TinyClaw's ownSchedule::parsealready validates cron via the samecron = "0.13"crate and is strictly more useful (4 formats).- The orchestrator process is down on bigbox; its cron persistence is a
config file / in-memory
last_cron_fire— nothing durable TinyClaw could push to right now.
Decision: implement the surface over TinyClaw's own CronStore
(terraphim_persistence-backed, same storage as dashboard), validate with
Schedule::parse, and document the deviation. The dashboard REST surface
(/api/cron/jobs) is the persistence/management API; CLI + tool + skill step
are clients of the same store. This matches the established tinyclaw pattern
(dashboard cron was already built this way) and needs zero new deps.
3198 — jmap crate relocation
The problem
crates/terraphim_tinyclaw/Cargo.toml line ~116:
jmap_client = { path = "../../../terraphim-private/crates/haystack_jmap" }
— the only path dep escaping the workspace into terraphim-private.
Breaks fresh clones / CI / Docker (private repo not present).
Findings
- The separate crates repository already exists:
terraphim-service(Gitea + github) — "Service + middleware + haystack layer — extracted from terraphim". It containscrates/haystack_jmapANDcrates/haystack_core, both workspace members. - Both are already published:
- terraphim registry:
haystack_jmap 1.20.2,haystack_core 1.20.3(verified via Gitea packages API + local sparse-index cache). - crates.io:
haystack_jmap 1.20.4,haystack_core 1.20.3.
- terraphim registry:
- The terraphim-registry
haystack_jmap 1.20.2manifest resolveshaystack_core ^1.19.3+terraphim_typesfrom the same registry (sparse index cache dump verified —registry: Noneentries, i.e. same registry), rest from crates.io. - API drift between the private copy (lib
jmap_client, v1.0.0) and the crates-repo line (libhaystack_jmap):JMAPClient::new(access_token)(private) vsnew(access_token, session_url)search_emails(query)vssearch_emails(query, limit)- structs
Email/EmailAddress/BodyValue/BodyPartare identical; service addsemail_to_document(). - lib/package rename:
jmap_client→haystack_jmap.
- tinyclaw usage (src/channels/email.rs):
Email,JMAPClientimports;EmailConfig { jmap_access_token, smtp_host, from_address, allow_from };connect()callsJMAPClient::new(token);search_emails(query); re-exportsBodyValue,Email as JmapEmail,EmailAddress as JmapEmailAddress; tests constructEmail/EmailAddress/BodyValueliterals directly (field-identical, so they survive the rename). - terraphim-private consumers: only
haystack_jmapitself references it (grep of crates/*/Cargo.toml).haystack_coreis used by atlassian, discourse, grepapp, jmap in private — so haystack_core stays in private (it's published separately; private's haystack_atlassian/discourse still need the path dep). Onlyhaystack_jmapmoves out.
Plan
- tinyclaw Cargo.toml: replace the private path dep with
haystack_jmap = { version = "1.20.2", registry = "terraphim" }(registry-cached; verified resolvable). Remove the private-path comment. - Update
src/channels/email.rs:use haystack_jmap::{Email, JMAPClient}+ re-exports fromhaystack_jmap.EmailConfiggainsjmap_session_url: String(default"https://api.fastmail.com/jmap/session"? — must be configurable; default empty is fine, connect errors on empty).connect():JMAPClient::new(token, &session_url).search_emails(query): add limit — keep the channel method signature(query)but callclient.search_emails(query, 20)(bounded default); or thread a limit through. Keep simple: constantSEARCH_LIMIT: u32 = 20.
- Remove
crates/haystack_jmapfrom terraphim-private (git rm + commit + push) — the "move".haystack_corestays (private haystack consumers). - Verify:
cargo check -p terraphim_tinyclaw+ full test suite; the email.rs tests build struct literals — should compile unchanged. - Update the Cargo.toml comment block (lines ~108-116) to point at the terraphim registry instead of the private path.
- Close #3198 with verification comment.
Risks
jmap_session_urlis a new required-ish config field; keepDefaultempty so existing configs still parse, andconnect()returns a clear error if empty (channel is not enabled by default).- Version pin:
1.20.2is the terraphim-registry version; crates.io has 1.20.4 but the terraphim registry is the internal source of truth used by all other workspace crates (terraphim_types = registry terraphim). Stick with registry 1.20.2; bump later if the service repo publishes 1.20.4 there.
Open items for #3147 implementation
ScheduleToolops:create {prompt, schedule, skills?, deliver?, model?},list,delete {id}— mirrors dashboard cron CRUD. Store key:"tinyclaw_schedules"(dashboard uses"dashboard_cron_jobs"— separate keyspace, same store type).- CLI:
Commands::Schedule { command: ScheduleCommands }withCreate {prompt, schedule, skill?, deliver?},List,Delete {id}. - SkillStep::Schedule
{cron, skill, inputs}— executor needs aCronStore; currentlySkillExecutorhas only storage_dir + tool_registry. Add optionalcron_store: Option<CronStore>+with_cron_store()builder;executereturns a clear error when unset ("scheduler not configured"). Executor is constructed in main.rs / agent_loop — wire the store there.