Skip to content

feat(schedules): run prompts on a cron schedule - #1105

Closed
0xfa1e wants to merge 3 commits into
agegr:mainfrom
0xfa1e:feat/scheduled-tasks
Closed

0xfa1e wants to merge 3 commits into
agegr:mainfrom
0xfa1e:feat/scheduled-tasks

Conversation

@0xfa1e

@0xfa1e 0xfa1e commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

Adds a Scheduled tasks section to Settings: a prompt, a working directory, and either a 5-field cron expression or a one-shot time.

How a run happens

Each run goes through startRpcSession, the same entry point POST /api/agent/new uses. A scheduled run is therefore an ordinary session: it appears in the session list, streams events, and can be resumed or branched like any other. Nothing new stores its output.

Settings › Schedule ──► schedules.json ──► tick (30s) ──► startRpcSession ──► session

Why the tick is server-side

instrumentation-node.ts starts the interval, and a browser timer was never an option: a tab's timer dies when the tab closes, and the point of a schedule is to fire whether or not anyone is watching.

One trap worth calling out, because it fails silently: the interval must not be unref()d. An unref'd interval reports as started while never firing, so the schedule looks healthy and does nothing. lib/schedule-runner.test.mjs pins this so it cannot come back.

Schedule input

lib/schedule-cron.ts is a dependency-free 5-field cron parser (ranges, steps, lists, names, the DOM/DOW OR rule). Natural-language phrases the UI offers are translated to cron client-side and validated server-side; anything unrecognised is refused rather than guessed at.

Retention

A finished one-shot task is swept after three days. The run is not lost — it lives on as a session, which is where the output is actually looked for — so keeping the scheduling row only grows schedules.json and the sidebar forever. Repeating tasks are never pruned; they are still doing work.

Also included

Seven fixed-width tabs overflowed the Settings header and put a scrollbar under the tab row. The dialog is widened to 1200px and the tabs now share the header evenly, so every gap is identical.

Checks

  • npx tsc --noEmit — clean
  • npm run lint — clean
  • npm test — 2253 passing, 0 failing
  • npm run build — both routes compile
  • Verified end to end in a browser: a one-shot task fired unattended and produced a session

Notes for review

  • Not covered by the existing extension/subagent mechanisms: those are scoped to a live session, while a schedule has to outlive every session and the browser.
  • cwd comes from the project open when the task is created; it is stored per task and never inferred at run time.

0xfa1e added 3 commits October 8, 2026 18:33
Adds a Scheduled tasks section to Settings: a prompt, a working
directory, and either a 5-field cron expression or a one-shot time.
Each run goes through startRpcSession, the same entry point
POST /api/agent/new uses, so a scheduled run is an ordinary session
that appears in the session list and streams events like any other.

The tick lives in instrumentation-node.ts rather than the browser: a
timer in a tab dies when the tab closes, and the point of a schedule
is to fire whether or not anyone is watching. An interval must stay
ref'd — an unref'd one reports as started while never firing, so
schedule-runner.test.mjs pins that.

Finished one-shot tasks are swept after three days. The run itself is
not lost: it lives on as a session, which is where the user looks for
it, so keeping the scheduling row only grew schedules.json forever.

Also widens the Settings dialog to 1200px and lets the section tabs
share the header evenly. Seven fixed-width tabs overflowed the header
and put a scrollbar under the tab row.
`flex: 1 1 auto` gives each tab its own content width, so once the
128px cap stops binding — at 1024px and narrower — the tabs shrink
proportionally and a longer label stays visibly wider than a shorter
one. A zero basis makes them share the row evenly at every viewport
width. Measured: 1920/1440/1280/1024/900/800 all render one identical
width per tab and one identical gap.
Only the last assistant message of a turn rendered a time. Every
intermediate tool-call step passed showTimestamp={false} — both inline
and, in the final-answer split, from the process-details group — and the
streaming tail was hidden as well, so the usage row under a bash block
showed token counts with no time on one turn and a time on the next.
Those conditions are gone: each assistant message renders the timestamp
the session file already stores, so the row under every tool call ends
with its own time.

formatTime used toLocaleTimeString with the ambient locale, which
renders AM/PM for en-US style locales. Format getHours/getMinutes
directly instead: pinned 24-hour output everywhere the timestamp is
shown, including user messages, and midnight stays 00:00 instead of the
24:00 that hour12:false can produce.
@agegr

agegr commented Oct 9, 2026

Copy link
Copy Markdown
Owner

Thanks for this — the write-up is clear, and running each schedule through startRpcSession so a run is just an ordinary session is a nice touch.

I'm going to decline it. Pi Web aims to stay a light web wrapper around pi, and a server-side scheduler (cron parser, tick loop, schedules.json, retention rules, a new Settings section) is a new subsystem with its own lifecycle that we'd have to own. Scheduling fits better outside Pi Web, for example a system cron job or a pi extension that runs pi on a timer.

Closing as not planned. Thanks again for the effort.

@agegr agegr closed this Oct 9, 2026
@0xfa1e
0xfa1e deleted the feat/scheduled-tasks branch October 10, 2026 07:26
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.

2 participants