Skip to content

(MOT-4503) feat(cron,console): schedules page for the cron worker - #840

Open
rohitg00 wants to merge 8 commits into
feat/console-consistencyfrom
feat/cron-schedules-page
Open

(MOT-4503) feat(cron,console): schedules page for the cron worker#840
rohitg00 wants to merge 8 commits into
feat/console-consistencyfrom
feat/cron-schedules-page

Conversation

@rohitg00

@rohitg00 rohitg00 commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Cron ships a console page. Every schedule on the rig is listed with its cadence in plain language, its next UTC run and how many times it has run, next to the cron jobs workers registered for themselves. New routines are described in a sentence and registered by an agent in a session of their own.

Screenshot 2026-08-19 at 18 48 34

What lands

Schedules. Agent-owned cron subscriptions across every session the page knows about, not just the conversation in front of you: status, cadence, target, next run, runs. Filter by All / Active / Ending soon / Finished, search across label, expression, target and subscription id, open any row for the full record.

System bindings. The cron triggers workers registered for themselves, with their owning worker, so a schedule nobody remembers writing has a name attached.

Describe a routine. The composer sends the request to a harness agent in a session created for that schedule and titled after the request. The console's chat pane follows that session as soon as it exists, so the registration is watchable while it runs instead of reported after it ends.

A form for exact control. Preset or hand-written expression, delivery as a conversation wake or a function call (target function, fixed payload, event pointer), an optional run limit. The cron field reads the expression back in plain language with the next three literal UTC times, so a six-field slip shows up while typing rather than at the first fire.

Replace and unregister. Replacement registers the new subscription first and retires the old one only when a different id comes back, so a failed replacement leaves the schedule standing. Unregister addresses the session that owns the subscription.

Why an agent registers the schedule

The cron worker has no service functions: it owns the cron trigger type, and session-scoped subscriptions belong to the harness, which intercepts engine::register_trigger inside a turn. A page cannot register a conversation wake on its own behalf, so the schedule is created by the agent that will answer it.

Two things make that reliable rather than hopeful:

  • The permission is narrow. The two calls a registration makes are granted individually with approval::approve-always, sequentially because both edit one settings record, and any call already held is released. The alternative — putting the session in full-permission mode — would leave a real chat running unattended forever.
  • The argument shape is fixed in the harness, not in a prompt. config is typed as an untyped value, so the schema the model sees says nothing about its shape and models serialise it twice: "{\"expression\":\"0 0 8 * * Mon\"}". Three different models did this; one failed four attempts in a row. engine::register_trigger now decodes a string that parses to an object and leaves anything else untouched, so the trigger type still reports real errors.

The weekday trap

The Rust cron crate counts Sunday as 1, one ahead of the Unix convention. A schedule written 0 30 9 * * 1 for "Monday" fires on Sunday. The page asks agents to write weekday names, its cadence and next-run readers understand them, and the convention is now documented in the cron skill (which the harness injects) and the worker README.

Conformance

Composed from @iii-dev/console-ui: page chrome, the semantic table family inside TableViewport/TableFrame with TableRow interactive selected, Tabs, SegmentedControl, List/ListItem, Select, the searchable Selector for the function catalog, Input (including the search field, with the glyph inside it), CodeEditor, Dialog, DropdownMenu, StatusDot, StatusPanel, EmptyState, Skeleton, IconButton, the uiClasses.field recipe on every form, and the 16 px icon recipe. Sans chrome in natural case; mono only for expressions, ids and counts. Below 760 px the tables become shared lists and the detail pane becomes a full-width page. docs/sops/console-ui-conformance.md gains the cron row and records the one local control (a multi-line input, which the library does not have yet — eval and iii-directory carry the same copy).

Reads

Schedules are found by listing sessions filtered on the page's own metadata, paged with the cursor, then read per session with bounded concurrency — rather than pulling the default page of every conversation on the rig and filtering client-side.

Verified on a live rig

Engine 0.22.1, console pane and 420 px phone width, light and dark:

  • schedules created from the composer, each in its own session, with the chat pane following it while the turn ran
  • 0 0 8 * * Mon reads as "Mon at 08:00 UTC" and next-runs on a Monday; 0 0 17 * * 5 reads as Thursday, matching the crate; Mon-Fri and sunset read as unknown instead of guessing
  • unregister confirmed against harness::triggers::list: the subscription is gone from the session that owned it
  • replacement observed doing the right thing in both directions — refusing to retire the original when the replacement registration failed, and retiring it once a different id came back
  • registration reached the engine with no human hold once the grants were in place

Depends on

Nothing under console/ is authored here. Every console file in the diff arrives with the #823 merge; the four commits on this branch touch only cron/**, harness/src/functions/subscribe.rs, docs/sops/console-ui-conformance.md and the pnpm files. Reviewing commit by commit shows the real change. The diff collapses to cron-only once both dependencies land and this retargets to main.

Next

Ideas worth taking after this, roughly in value order: a last-run outcome on every row (needs somewhere to record it), one merged countdown agenda across schedules and bindings, fire-once-now from a row, pause without unregistering, and a distinct state for "fired but could not deliver".

Fixes MOT-4503

The cron worker ships an injectable console page. Schedules are a table, not
a list: status, name, target, cadence, next run and fire count, with the
detail beside it — the shape an operator scans when they want to know what is
about to run and what it will call.

Built from the shared primitives rather than local markup: PageShell and
PageHeader, PageBody with PageMain and PageSidebar, the Table family,
SegmentedControl for the status filter, StatusDot, EmptyState, StatusPanel,
Skeleton, DropdownMenu and Dialog. The stylesheet only carries layout, scoped
to the worker wrapper, in Console tokens.

Status is derived from what a subscription actually reports — its fire count,
its cap, its expiry — so it reads Active, Ending soon, or Finished. There is
no paused state in the harness surface and no per-fire outcome, so neither is
shown; inventing them would put a claim on screen the data cannot support.

A pane narrower than 760px drills in rather than splitting: the table is
replaced by the detail with a labelled back action, and the focus trap and
Escape handling follow it.

Scheduling names a model. A conversation that has never taken a turn has none
to inherit, and harness::send rejected the request with a raw handler error.
The console's own last pick is used when there is one, and when there is not
the page says which control to use instead.
`config` is typed as an untyped value, so the schema the model sees says
nothing about its shape, and models routinely serialise it twice: the field
arrives as `"{\"expression\":\"0 0 8 * * Mon\"}"` and the trigger type rejects
a registration that was correct in substance. Observed on three different
models registering the same cron, four attempts in a row on one of them.

Decode a string that parses to an object, leave anything else untouched so the
owning trigger type still reports the real error, and say in the field's doc
that this is an object rather than a string.
The page now reads every schedule the rig owns, not only the open
conversation's: sessions are listed by the metadata this page stamps, paged,
and read with bounded concurrency. Creating a routine sends a harness turn in
a session of its own and the console follows that session as soon as it
exists, so the registration is watchable while it runs rather than reported
after it ends.

Registration is granted, not waved through: the two calls a schedule makes are
approved individually and any hold placed before the grants landed is
released, instead of putting the session into full-permission mode where every
later message would run unattended.

Rebuilt on the shared library after comparing against the console conformance
inventory: page chrome, table family, tabs, segmented filter, list fallback
below 760 px, the field recipe on every form, the shared searchable selector
for the function catalog, and the shared input for search with the glyph
inside it. Icons come from the shared 16 px recipe.

Weekday handling was wrong in two directions. The Rust cron crate counts
Sunday as 1, so a numeric weekday meant as Monday fires a day early; the page
now asks for names and the cadence and next-run readers understand them.
Ranges and unknown tokens read as unknown rather than guessing. The trap is
documented in the cron skill and README, where agents and operators meet it.

Replacement keeps its guarantee: the new subscription is registered first, and
the old one is retired only when a different id comes back.
…-page

# Conflicts:
#	console/web/src/hooks/use-conversations.test.ts
The form now answers "what did I just write" while it is being written: the
cron field reports the expression in plain language and the next three literal
UTC times underneath it. A six-field slip looks right and normally reveals
itself at the first fire; this shows it immediately. Shapes the reader does not
model, like weekday ranges, show nothing rather than a confident guess.

The count of occurrences is called runs, in the table, the list and the detail
pane. A trigger is the registration that stands until it is removed; what the
column counts is how many times it has run.
@vercel

vercel Bot commented Aug 19, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
workers Ready Ready Preview Aug 19, 2026 5:33pm
workers-tech-spec Ready Ready Preview Aug 19, 2026 5:33pm

Request Review

@coderabbitai

coderabbitai Bot commented Aug 19, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c33cc365-31e7-4c7e-8536-571efd1e20f0

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

skill-check — worker

0 verified, 61 skipped (no docs/).

Layer Result
structure
vale
ai
render

Four for four. Nicely done.

@rohitg00
rohitg00 requested a review from sergiofilhowz August 19, 2026 17:45
@rohitg00
rohitg00 marked this pull request as ready for review August 19, 2026 17:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant