feat: an hour-precision clock that says so, and a cron tool that names the real timezone
Nightly Build / build (push) Successful in 7m30s

The datetime block claimed second precision it never had. It is built once per
request and a turn can run for minutes, so `17:54:31` is a lie by the time the
model reads it — and the model, having no way to know, wrote cron expressions
from it.

Rounding was already there but configurable (`round_minutes`, shipped at 60)
and justified by the prompt cache. That justification was false: the block is
the LAST system message, after the whole conversation, so the cached prefix is
identical from turn to turn whatever the timestamp says. Rounding buys nothing
for caching today.

So the knob goes and the granularity becomes part of the contract: always
truncated to the hour, stated in words, with a pointer to `date` for the cases
that need the minute. `DatetimeConfig` keeps only `enabled`.

- truncation happens in the DISPLAYED zone, not on the UTC epoch: +05:30 zones
  would otherwise render 20:30 — an hour off and not on an hour boundary, which
  reads as precise again.
- the weekday is spelled out. "next Tuesday" is a far more common ask than the
  minute, and weekday-from-date is exactly the arithmetic models get wrong.

Also fixes a real bug found on the way: `execute_task` told the model, twice,
that cron expressions are evaluated in Europe/London — hardcoded, while
TaskManager uses the configured timezone. On a non-UK box every scheduled job
was written against the wrong clock. The description now names the zone the
scheduler actually uses (`TaskManager::timezone_name`), and the assistant's
AGENT.md stops repeating the literal.

CLAUDE.md: record that the instance is in production. The greenfield licence has
expired — schema changes need a versioning mechanism, and per-user SQLCipher
files mean it cannot be a boot-time sweep.
This commit is contained in:
2026-08-02 22:20:52 +01:00
parent 85536755ee
commit da0830aefa
8 changed files with 106 additions and 62 deletions
+5 -3
View File
@@ -39,13 +39,15 @@ pub struct LlmConfig {
}
/// Controls date/time injection in the dynamic tail of each LLM request.
///
/// The injected time is **always** truncated to the hour, and the block says so:
/// see [`crate::loop_adapters::system`]. There is deliberately no rounding knob —
/// the granularity is part of what the model is told, not an instance setting.
#[derive(Debug, Clone, Deserialize)]
pub struct DatetimeConfig {
/// Inject the current date/time into the LLM context. Default: true.
#[serde(default = "default_true")]
pub enabled: bool,
/// When set, round the injected time down to the nearest N-minute boundary.
pub round_minutes: Option<u32>,
/// IANA timezone name to use when formatting the injected timestamp.
/// Populated at startup from the global `timezone` config field.
#[serde(skip)]
@@ -54,7 +56,7 @@ pub struct DatetimeConfig {
impl Default for DatetimeConfig {
fn default() -> Self {
Self { enabled: true, round_minutes: None, timezone: None }
Self { enabled: true, timezone: None }
}
}