Skip to content

fix(shared): stop fired one-shot crons resolving to next year - #386

Open
tommy0103 wants to merge 1 commit into
LodyAI:mainfrom
tommy0103:fix/one-shot-cron-fire-anchor
Open

fix(shared): stop fired one-shot crons resolving to next year#386
tommy0103 wants to merge 1 commit into
LodyAI:mainfrom
tommy0103:fix/one-shot-cron-fire-anchor

Conversation

@tommy0103

Copy link
Copy Markdown
Contributor

Related issue

Closes #385

Problem / pressure

The scheduled-tasks chip above the composer showed a one-shot cron as "fires in ~364 days" long after it had fired and been auto-deleted by the runtime. The deriver anchors a one-shot's single fire time at the owning history entry's endedAt; cron-fire follow-up turns are runtime-internal steers that keep extending the same entry, so endedAt can land past the fire minute and the resolver skips to the next matching year — a future timestamp the fired-row filter can never hide.

Summary

  • history-apply.ts stamps the four scheduling tools' tool_call items with recordedAtMs (first-persisted wall-clock sighting); merges keep the first stamp so replayed or retried updates cannot move it.
  • scheduled-tasks-from-history.ts resolves a one-shot cron by preferring the runtime-committed nextFireAt line persisted in the tool output, then recordedAtMs, then the turn's start; endedAt drops to last resort. Wakeups anchor at recordedAtMs too.
  • ai.ts / message-schemas.ts add the optional recordedAtMs field; schema.ts and apps/cli/src/lib/AGENTS.md document the updated contract.

Before / after

Before After
Fired one-shot anchored at a merged entry's endedAt past its fire minute resolves to next year; chip shows "~364 days" forever One-shot resolves to the runtime-committed nextFireAt (or the call's recordedAtMs), lands in the past once fired, and the row hides
Wakeup fire time = turn end + delay (drifts by the turn's length) Wakeup fire time = the call's recorded time + delay
Scheduling tool calls carry no creation timestamp recordedAtMs persisted at first sighting, stable across replays

Test plan

  • packages/shared: pnpm run typecheck clean; full vitest run 1025 passed, including new cases in tests/scheduled-tasks-from-history.test.ts (committed nextFireAt wins over a late endedAt; recordedAtMs anchor; turn-start legacy fallback; recurring jobs ignore nextFireAt; wakeup anchoring) and tests/acp-history-apply.test.ts (stamp written once and never moved; non-scheduling tools unstamped).
  • apps/cli: typecheck clean; tests/acp-history.test.ts 37 passed.
  • packages/components: typecheck clean; tests/scheduled-tasks-panel-fire-time.test.ts 3 passed (existing anchor contract preserved).
  • End-to-end: ran the deriver over a real session document that exhibited the bug — the phantom row now resolves to its committed past fire time and is filtered out, and the one genuinely pending one-shot resolves to the exact committed fire time.
  • oxlint on changed files: 0 errors; prettier --check clean; check:public-boundary, check:platform-boundaries, lint:i18n all pass.
  • Skipped: full-repo pnpm check (heavy CI matrix); no UI code changed.

Context handoff

Instructions for reviewing agents

  • Review focus: packages/shared/src/scheduled-tasks-from-history.ts (anchor priority and the nextFireAt parse) and history-apply.ts stampSchedulingToolCall plus its merge preservation.
  • Decisions to challenge: parsing nextFireAt from the persisted output text instead of extending the ACP contract; preferring turn-start over endedAt as the legacy anchor; stamping at first sighting rather than at tool completion.
  • Plausible failures / evidence gaps: agents whose CronCreate output has no nextFireAt: line fall back to the turn-start anchor, which can mis-resolve unpinned interval one-shots (e.g. */5 * * * * with recurring: false) into the turn window; replay imports stamp import time as recordedAtMs.

Authoring context

  • User goal / directives: Diagnose a scheduled-tasks chip showing "~364 days" for a fired one-shot cron (initially suspected to be a timezone bug), then fix it and file the upstream issue and PR.
  • Constraints / non-goals: Keep the fix inside the public Lody repo and provider-neutral (no per-agent ACP extension changes); no UI changes; no git mutations beyond the fix branch were requested.
  • Risk-bearing decisions: recordedAtMs is a new optional field on persisted tool_call items (additive, backward compatible); the legacy anchor order changed from end-first to start-first, deliberately trading interval one-shot accuracy for never rolling date-pinned one-shots into the next year.
  • Destructive or irreversible behavior: None; derivation stays a pure read over history, and the new stamp only adds a field — old history without it follows the fallback chain.
  • Deliberately not done or tested: No live-runtime RPC for cron state (would require per-provider extension work); no new UI tests, since the panel contract (resolveFireMs) is unchanged and covered by existing suites.
  • Unknowns / confidence: High confidence for Kimi/Claude-shape outputs (verified against a real affected session document); moderate for other providers' CronCreate output formats, which the anchor fallback still covers.

The scheduled-tasks panel derives pending tasks from the Cron*/ScheduleWakeup
tool_call items in history and anchors a one-shot cron's single fire time at
the owning turn's endedAt. Cron-fire follow-up turns are runtime-internal
steers, so one history entry can aggregate several runtime turns and its
endedAt keeps advancing — past the one-shot's fire minute. The resolver then
skips to the next matching year, and a job that already fired (and was
auto-deleted by the scheduler) shows "fires in 364 days" forever, because the
phantom fire time is in the future and the fired-row filter can never catch it.

- history-apply stamps scheduling tool calls with recordedAtMs, the
  first-persisted wall-clock sighting; merges keep the first stamp so
  replayed/retried updates cannot move it.
- The deriver resolves a one-shot cron by preferring the runtime-committed
  nextFireAt line from the persisted output (exact, and also heals existing
  history), then recordedAtMs, then the turn's START — endedAt is now the
  last resort. Wakeups anchor at recordedAtMs as well.

Model: kimi-code/k3
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Scheduled-tasks panel shows a fired one-shot cron as "fires in ~364 days"

1 participant