Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
4253432
chore(schedule): move schedule types into tinyflows-schedule
senamakel Oct 16, 2026
17c3f07
feat(bus): add schedule types for bus scheduling
senamakel Oct 16, 2026
ecd28cb
feat(schedule): add frequency, records, requests, and store types
senamakel Oct 16, 2026
331cdc7
feat(schedule): add wire format tests for schedule messages
senamakel Oct 16, 2026
0a257a5
feat(schedule): add chrono dependency and wire test module
senamakel Oct 16, 2026
3678ac2
refactor(schedule): move shared schedule types into tinyflows-bus
senamakel Oct 16, 2026
31c2586
refactor(schedule): re-export describe_gap from the bus contract
senamakel Oct 16, 2026
fb00916
feat(schedule): add persistent schedule store
senamakel Oct 16, 2026
6df8740
test(schedule): add member tests module and share test helpers
senamakel Oct 16, 2026
6484f25
test(schedule): return server connection from proxy helper
senamakel Oct 16, 2026
51fda3a
test(schedule): add probe for unknown member
senamakel Oct 16, 2026
981c8e5
test(schedule): tighten unknown member assertion
senamakel Oct 16, 2026
f905371
docs(schedule): document schedule bus contract v3
senamakel Oct 16, 2026
7e3aa5e
Merge origin/main into cron-bus-cutover
senamakel Oct 16, 2026
2eecfc6
chore: files changed crates/tinyflows-module/Cargo.toml,crates/tinyfl…
senamakel Oct 16, 2026
2442cd7
chore: files changed crates/tinyflows-module/src/schedule_callbacks.rs
senamakel Oct 16, 2026
b145107
feat(import): add schedule-read feature exposing legacy cron reader
senamakel Oct 16, 2026
6160d5a
feat(module): static-link entry, long run-callback timeouts, bus-type…
stevent95-star Oct 16, 2026
ccb79ef
fix(schedule): reject elapsed one-shot schedules in next_run
senamakel Oct 16, 2026
62c3e78
refactor(schedule_service): collapse nested if into let-chain
senamakel Oct 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Schedule bus contract v3: hosts need only `tinyflows-bus`

- **Moved** the schedule wire records (`Schedule`, `CronJob`, `CronRun`,
`CronJobPatch`, `JobOrigin`, `DeliveryConfig`, `DeliveryStatus`, `JobPolicy`,
`JobType`, `SessionTarget`, `ActiveHours`, `delivery_mode`), `AgentJobSpec`,
and the callback records `Attempt`/`RunRecord`/`Completion` from
`tinyflows-schedule` into `tinyflows_bus::schedule` (`tinyflows-bus` gains
`chrono`). `tinyflows-schedule` re-exports them at the same paths; the JSON
wire shape is unchanged (`wire_tests` pass unchanged and now also pin the bus).
The two `Schedule` deserializer `debug` logs were dropped with the move.
- **Added** the typed `StoreOperation`, `ImportedJob`, `TooFrequent`,
`MIN_AGENT_JOB_INTERVAL` and request types to the contract, and `members::*`
constants for every Schedule member.
- **Added** module members `NextRun`, `ValidateSchedule`, `RunsCloserThan`,
`NormalizeExpression`, `ScheduleCronExpression`, `DefaultDelivery`,
`CheckOriginRequirements`, `CheckPatch`, `TruncateCronOutput`, and
`ScheduleStore` operations `set_max_active_jobs`, `import_job`, `import_run`.
- **Changed** Schedule `CONTRACT_VERSION` 2 -> 3.

### Script spawner seam (`host-caps`)

`caps::host::script` now launches every `code`/`shell` script through a
Expand Down
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/tinyflows-bus/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ publish = false
description = "Transport-free TinyBus contract for workflow authoring"

[dependencies]
chrono.workspace = true
serde.workspace = true
serde_json.workspace = true
82 changes: 82 additions & 0 deletions crates/tinyflows-bus/src/schedule/frequency.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
//! Cadence-floor vocabulary shared by schedule validation and its hosts.
use chrono::{DateTime, Duration as ChronoDuration, Utc};
use serde::{Deserialize, Serialize};
use std::fmt;

/// The shortest interval a host should allow between runs of an agent job.
/// Enforced by the module's `ValidateSchedule` (agent mode) member.
pub const MIN_AGENT_JOB_INTERVAL: ChronoDuration = ChronoDuration::minutes(5);

/// Why a schedule runs more often than a threshold allows. Carries the
/// evidence, not only the verdict, so a log line or an error can quote it.
///
/// Wire shape: `{"fixed_interval": <ms>}` or
/// `{"consecutive_runs": {"first": RFC3339, "second": RFC3339}}`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TooFrequent {
/// A fixed `every_ms` interval shorter than the threshold.
FixedInterval(#[serde(with = "millis")] ChronoDuration),
/// Two consecutive cron occurrences closer together than the threshold.
ConsecutiveRuns {
/// First of the pair.
first: DateTime<Utc>,
/// Second of the pair.
second: DateTime<Utc>,
},
}

mod millis {
use chrono::Duration;
use serde::{Deserialize, Deserializer, Serializer};

pub fn serialize<S: Serializer>(gap: &Duration, s: S) -> Result<S::Ok, S::Error> {
s.serialize_i64(gap.num_milliseconds())
}
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Duration, D::Error> {
Duration::try_milliseconds(i64::deserialize(d)?)
.ok_or_else(|| serde::de::Error::custom("gap out of range"))
}
}

impl TooFrequent {
/// The offending gap.
pub fn gap(&self) -> ChronoDuration {
match self {
Self::FixedInterval(gap) => *gap,
Self::ConsecutiveRuns { first, second } => *second - *first,
}
}
}

impl fmt::Display for TooFrequent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::FixedInterval(gap) => write!(f, "fires every {}", describe_gap(*gap)),
Self::ConsecutiveRuns { first, second } => write!(
f,
"fires at {} and again at {}, {} apart",
first.format("%Y-%m-%d %H:%M:%S UTC"),
second.format("%Y-%m-%d %H:%M:%S UTC"),
describe_gap(*second - *first)
),
}
}
}

/// `4 minutes`, `30 seconds`, `1 minute 30 seconds` — whole seconds only.
pub fn describe_gap(gap: ChronoDuration) -> String {
fn count(n: i64, unit: &str) -> String {
if n == 1 {
format!("{n} {unit}")
} else {
format!("{n} {unit}s")
}
}
let seconds = gap.num_seconds();
match (seconds / 60, seconds % 60) {
(0, s) => count(s, "second"),
(m, 0) => count(m, "minute"),
(m, s) => format!("{} {}", count(m, "minute"), count(s, "second")),
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,26 @@
//! shape without making hosts link scheduler or storage implementation crates.
use serde::{Deserialize, Serialize};

mod frequency;
mod records;
mod requests;
mod store_types;
pub mod types;

pub use frequency::{MIN_AGENT_JOB_INTERVAL, TooFrequent, describe_gap};
pub use records::{Attempt, Completion, RunRecord};
pub use requests::{
CheckOriginRequest, CheckPatchRequest, NextRunRequest, RunsCloserThanRequest,
ValidateScheduleRequest,
};
pub use store_types::{
AgentJobSpec, ImportedJob, MAX_CRON_OUTPUT_BYTES, StoreOperation, TRUNCATED_OUTPUT_MARKER,
};
pub use types::{
ActiveHours, CronJob, CronJobPatch, CronRun, DeliveryConfig, DeliveryStatus, JobOrigin,
JobPolicy, JobType, Schedule, SessionTarget, delivery_mode,
};

/// The module has one bus identity and separately served authoring/schedule objects.
pub const BUS_NAME: &str = crate::BUS_NAME;
/// Separate scheduler interface in the shared module.
Expand All @@ -11,20 +31,73 @@ pub const OBJECT_PATH: &str = "/ai/tinyhumans/tinyflows/Schedule";
/// Host interface carrying the context-bound runtime ports.
pub const CALLBACK_INTERFACE: &str = "ai.tinyhumans.tinyflows.ScheduleHost";
/// Version of the scheduler wire vocabulary. 2 removed the module-owned
/// `sqlite` store backend; storage is the host's document port only.
pub const CONTRACT_VERSION: u32 = 2;
/// `sqlite` store backend; storage is the host's document port only. 3 moved
/// the job/run/policy records and the callback records into this crate
/// (`types`, `Attempt`/`RunRecord`/`Completion`, `AgentJobSpec`,
/// `StoreOperation`) and added the schedule-maths and validation members and
/// the `set_max_active_jobs`/`import_job`/`import_run` store operations.
pub const CONTRACT_VERSION: u32 = 3;
/// Exact advertised scheduler member set.
pub const METHODS: &[&str] = &[
"ScheduleStore",
"StartScheduler",
"StopScheduler",
"TickScheduler",
"DrainScheduler",
"ExecuteJob",
"ParseHumanDelay",
"ValidateDelivery",
members::SCHEDULE_STORE,
members::START_SCHEDULER,
members::STOP_SCHEDULER,
members::TICK_SCHEDULER,
members::DRAIN_SCHEDULER,
members::EXECUTE_JOB,
members::PARSE_HUMAN_DELAY,
members::VALIDATE_DELIVERY,
members::NEXT_RUN,
members::VALIDATE_SCHEDULE,
members::RUNS_CLOSER_THAN,
members::NORMALIZE_EXPRESSION,
members::SCHEDULE_CRON_EXPRESSION,
members::DEFAULT_DELIVERY,
members::CHECK_ORIGIN_REQUIREMENTS,
members::CHECK_PATCH,
members::TRUNCATE_CRON_OUTPUT,
];

/// Stable scheduler member names, in `METHODS` order.
pub mod members {
/// `(StoreRequest) -> JSON`; see `STORE_OPERATIONS`.
pub const SCHEDULE_STORE: &str = "ScheduleStore";
/// `(StartScheduler) -> handle: string`.
pub const START_SCHEDULER: &str = "StartScheduler";
/// `(handle: string) -> ()`.
pub const STOP_SCHEDULER: &str = "StopScheduler";
/// `(handle: string) -> usize`; manual-tick schedulers only.
pub const TICK_SCHEDULER: &str = "TickScheduler";
/// `(handle: string) -> ()`; manual-tick schedulers only.
pub const DRAIN_SCHEDULER: &str = "DrainScheduler";
/// `(scope, CallbackAddress, config, job, run_id) -> Attempt`.
pub const EXECUTE_JOB: &str = "ExecuteJob";
/// `(delay: string) -> milliseconds: i64`.
pub const PARSE_HUMAN_DELAY: &str = "ParseHumanDelay";
/// `(DeliveryConfig, Option<JobOrigin>, Option<Vec<String>>) -> ()`.
pub const VALIDATE_DELIVERY: &str = "ValidateDelivery";
/// `(NextRunRequest) -> DateTime<Utc>`.
pub const NEXT_RUN: &str = "NextRun";
/// `(ValidateScheduleRequest) -> ()`.
pub const VALIDATE_SCHEDULE: &str = "ValidateSchedule";
/// `(RunsCloserThanRequest) -> Option<TooFrequent>`.
pub const RUNS_CLOSER_THAN: &str = "RunsCloserThan";
/// `(expression: string) -> string`: POSIX crontab weekdays into the
/// `cron` crate's numbering; errors on a malformed expression.
pub const NORMALIZE_EXPRESSION: &str = "NormalizeExpression";
/// `(Schedule) -> Option<string>`: the cron expression, if it has one.
pub const SCHEDULE_CRON_EXPRESSION: &str = "ScheduleCronExpression";
/// `(Option<JobOrigin>) -> DeliveryConfig`: origin delivery with an
/// origin, proactive without.
pub const DEFAULT_DELIVERY: &str = "DefaultDelivery";
/// `(CheckOriginRequest) -> ()`.
pub const CHECK_ORIGIN_REQUIREMENTS: &str = "CheckOriginRequirements";
/// `(CheckPatchRequest) -> ()`.
pub const CHECK_PATCH: &str = "CheckPatch";
/// `(output: string) -> string`, bounded to `MAX_CRON_OUTPUT_BYTES`.
pub const TRUNCATE_CRON_OUTPUT: &str = "TruncateCronOutput";
}

/// A host callback object bound to the owning agent/tenant context.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CallbackAddress {
Expand Down Expand Up @@ -117,7 +190,7 @@ pub struct StoreRequest {
pub operation: serde_json::Value,
}

/// Supported `operation.op` values. Other fields use existing job/spec/patch
/// Supported `operation.op` values; `StoreOperation` is their typed form. Other fields use job/spec/patch
/// JSON shapes. Run timestamps are RFC3339 and durations are milliseconds.
pub const STORE_OPERATIONS: &[&str] = &[
"add",
Expand All @@ -137,6 +210,9 @@ pub const STORE_OPERATIONS: &[&str] = &[
"record_run",
"delete_queued_runs",
"list_runs",
"set_max_active_jobs",
"import_job",
"import_run",
];

/// Context-bound document callback interface. The only member is `Document`;
Expand All @@ -148,3 +224,10 @@ pub const STORE_OPERATIONS: &[&str] = &[
pub const DOCUMENT_INTERFACE: &str = "ai.tinyhumans.tinyflows.ScheduleDocuments";
/// Exact document callback member used by the module.
pub const DOCUMENT_METHOD: &str = "Document";

#[cfg(test)]
#[path = "mod_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "wire_tests.rs"]
mod wire_tests;
Loading
Loading