Motivation
T3 Code only starts agent runs when the user sends a message. A lot of real agent work is time-shaped instead:
- nightly maintenance — "bump dependencies every night", "run the test suite each morning and report failures"
- delayed follow-ups — "check back on this deploy in 30 minutes"
- recurring chores per project — "summarize open TODOs every weekday at 9:00"
The closest primitive today is thread snooze: time-based visibility whose wake is client-derived (web sets a local timeout off snoozedUntil). The contracts comment around thread.unsnoozed explicitly anticipates future wake conditions, but nothing currently wires time to starting a turn. If no client is open — or the server restarts — nothing happens. Wake logic needs to become server-authoritative.
Proposed MVP
Stay event-sourced, mirroring the snooze precedent end-to-end:
- Contracts:
task.schedule / task.cancel commands; task.scheduled / task.fired / task.cancelled events. Schedule spec v1: one-shot at OR interval every anchored at creation (no cron expressions yet).
- Decider: validate due times; fire via deterministic commandIds (
server:task-fire:<taskId>:<dueAt>) so crash-retry collapses into the existing idempotent command receipts.
- Scheduler layer: modeled on
ProviderSessionReaper (Effect Schedule.spaced loop, manual-tick seam for tests). Each tick finds due tasks in the projection and dispatches fire through the engine. A reactor consumes task.fired and dispatches a normal thread.turn.start, reusing the existing bootstrap.createThread path when no thread is attached.
- Read model:
projection_tasks table, tasks.list query, capability gate following threadSnooze.
- Web UI: create / cancel / list scheduled tasks per project. Mobile inherits the commands automatically via
client-runtime; screens can follow later.
Non-goals for v1
- cron syntax (presets + fixed intervals cover the common cases)
- event-based triggers ("when PR merges") — separate follow-up
- multi-fire catch-up semantics (missed fires during downtime coalesce into one)
Will open a PR referencing this issue.
Motivation
T3 Code only starts agent runs when the user sends a message. A lot of real agent work is time-shaped instead:
The closest primitive today is thread snooze: time-based visibility whose wake is client-derived (web sets a local timeout off
snoozedUntil). The contracts comment aroundthread.unsnoozedexplicitly anticipates future wake conditions, but nothing currently wires time to starting a turn. If no client is open — or the server restarts — nothing happens. Wake logic needs to become server-authoritative.Proposed MVP
Stay event-sourced, mirroring the snooze precedent end-to-end:
task.schedule/task.cancelcommands;task.scheduled/task.fired/task.cancelledevents. Schedule spec v1: one-shotatOR intervaleveryanchored at creation (no cron expressions yet).server:task-fire:<taskId>:<dueAt>) so crash-retry collapses into the existing idempotent command receipts.ProviderSessionReaper(EffectSchedule.spacedloop, manual-tick seam for tests). Each tick finds due tasks in the projection and dispatches fire through the engine. A reactor consumestask.firedand dispatches a normalthread.turn.start, reusing the existingbootstrap.createThreadpath when no thread is attached.projection_taskstable,tasks.listquery, capability gate followingthreadSnooze.client-runtime; screens can follow later.Non-goals for v1
Will open a PR referencing this issue.