Skip to content

Commit 2afdad8

Browse files
committed
feat: repair stale schedule timing
1 parent 7a8ee1b commit 2afdad8

15 files changed

Lines changed: 535 additions & 107 deletions

README.md

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -151,13 +151,13 @@ clawhub install swarmclaw
151151

152152
[Browse on ClawHub](https://clawhub.ai/skills/swarmclaw)
153153

154-
## v1.9.17 Highlights
154+
## v1.9.23 Highlights
155155

156-
Agent configuration history is now visible in the agent editor, so operators can review recent saved versions and restore prior settings without leaving the agent workflow.
156+
Schedule reliability is now more deterministic for recurring autonomous work, especially after restarts or stale stored timing state.
157157

158-
- **Agent sheet history.** Advanced agent settings list recent saved versions with timestamp, actor, and provider/model snapshot.
159-
- **One-click restore.** Restoring a prior version uses the existing config-version restore API, refreshes agent state, and closes the sheet to avoid stale form data.
160-
- **Regression coverage.** New tests cover config-version list/restore routes and UI summary formatting.
158+
- **Cron drift repair.** Active schedules repair stale future cron slots before they skip the nearest run.
159+
- **Stable stagger.** Staggered schedules keep a deterministic per-schedule offset.
160+
- **Mission continuity.** Schedule-created board tasks keep a persistent mission link across recurring runs.
161161

162162
## Hosted Deploys
163163

@@ -409,6 +409,15 @@ Operational docs: https://swarmclaw.ai/docs/observability
409409

410410
## Releases
411411

412+
### v1.9.23 Highlights
413+
414+
Schedule reliability release: recurring work now repairs stale timing state before it can skip the nearest run, and scheduled board tasks keep mission context across repeat launches.
415+
416+
- **Cron drift repair.** Active cron schedules repair missing or invalid `nextRunAt` values and stale future cron slots before the scheduler decides whether work is due.
417+
- **Tick-time advancement.** Cron and interval schedules now advance from the scheduler tick time instead of the process wall clock, making restart and catch-up behavior deterministic.
418+
- **Stable stagger.** Schedule stagger offsets are deterministic per schedule, avoiding thundering-herd launches without moving a saved next-run target on every recompute.
419+
- **Mission continuity.** Schedule-created board tasks attach to a persistent mission link, so recurring runs share the same operational context.
420+
412421
### v1.9.22 Highlights
413422

414423
Research tools release: agents now get direct `web_extract` and `web_crawl` tools alongside `web_search`, `web_fetch`, and the unified `web` tool.

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@swarmclawai/swarmclaw",
3-
"version": "1.9.22",
3+
"version": "1.9.23",
44
"description": "Build and run autonomous AI agents with OpenClaw, Hermes, multiple model providers, orchestration, delegation, memory, skills, schedules, and chat connectors.",
55
"main": "electron-dist/main.js",
66
"license": "MIT",
@@ -88,7 +88,7 @@
8888
"test:cli": "node --test src/cli/*.test.js bin/*.test.js scripts/electron-after-pack.test.mjs scripts/electron-signing-config.test.mjs scripts/ensure-sandbox-browser-image.test.mjs scripts/postinstall.test.mjs scripts/run-next-build.test.mjs scripts/run-next-typegen.test.mjs",
8989
"test:setup": "tsx --test src/app/api/setup/check-provider/route.test.ts src/lib/server/provider-model-discovery.test.ts src/components/auth/setup-wizard/utils.test.ts src/components/auth/setup-wizard/types.test.ts src/hooks/setup-done-detection.test.ts src/lib/setup-defaults.test.ts src/lib/server/storage-auth.test.ts src/lib/server/storage-auth-docker.test.ts",
9090
"test:openclaw": "tsx --test src/lib/openclaw/openclaw-agent-id.test.ts src/lib/openclaw/openclaw-endpoint.test.ts src/lib/server/agents/agent-runtime-config.test.ts src/lib/server/build-llm.test.ts src/lib/server/connectors/connector-routing.test.ts src/lib/server/connectors/openclaw.test.ts src/lib/server/connectors/swarmdock.test.ts src/lib/server/gateway/protocol.test.ts src/lib/server/gateways/gateway-topology.test.ts src/lib/server/llm-response-cache.test.ts src/lib/server/mcp-conformance.test.ts src/lib/server/openclaw/agent-resolver.test.ts src/lib/server/openclaw/deploy.test.ts src/lib/server/openclaw/skills-normalize.test.ts src/lib/server/session-tools/openclaw-nodes.test.ts src/lib/server/session-tools/swarmdock.test.ts src/lib/server/tasks/task-quality-gate.test.ts src/lib/server/tasks/task-validation.test.ts src/lib/server/tool-capability-policy.test.ts src/lib/providers/openai.test.ts src/lib/providers/openclaw-exports.test.ts src/app/api/gateways/topology-route.test.ts src/app/api/openclaw/dashboard-url/route.test.ts",
91-
"test:runtime": "tsx --test src/lib/a2a/agent-card.test.ts src/lib/agent-planning-mode.test.ts src/lib/agent-config-history.test.ts src/lib/strip-internal-metadata.test.ts src/lib/provider-sets.test.ts src/lib/providers/opencode-cli.test.ts src/lib/providers/cli-provider-metadata.test.ts src/lib/providers/cli-utils.test.ts src/lib/providers/generic-cli.test.ts src/lib/server/agents/delegation-advisory.test.ts src/lib/server/cli-provider-readiness.test.ts src/lib/server/provider-health.test.ts src/lib/server/provider-diagnostics.test.ts src/lib/server/mcp-gateway-runtime.test.ts src/lib/server/mcp-connection-pool.test.ts src/lib/server/knowledge-sources.test.ts src/lib/server/extension-managed-resources.test.ts src/lib/server/eval/baseline.test.ts src/lib/server/eval/environment-plan.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/chat-execution/chat-turn-preparation.test.ts src/lib/server/chat-execution/iteration-timers.test.ts src/lib/server/chat-execution/post-stream-finalization.test.ts src/lib/server/chat-execution/prompt-sections.planning-mode.test.ts src/lib/server/chat-execution/reasoning-tag-scrubber.test.ts src/lib/server/chats/clear-undo-snapshots.test.ts src/lib/server/chats/session-context-pack.test.ts src/lib/server/connectors/email.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/runtime/queue-retry-policy.test.ts src/lib/server/runs/run-brief.test.ts src/lib/server/runs/run-handoff.test.ts src/lib/server/operations/operation-pulse.test.ts src/lib/server/schedules/schedule-history.test.ts src/lib/server/schedules/schedule-preview.test.ts src/lib/quality/release-readiness.test.ts src/lib/quality/architecture-health.test.ts src/lib/server/artifacts/artifact-resolver.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.test.ts src/lib/server/missions/mission-templates.test.ts src/lib/server/sharing/share-link-repository.test.ts src/lib/server/sharing/share-resolver.test.ts src/lib/server/tasks/task-execution-workspace.test.ts src/lib/server/tasks/task-execution-policy.test.ts src/lib/server/tasks/task-handoff.test.ts src/lib/server/tasks/task-service.test.ts src/lib/server/session-tools/execute.test.ts src/lib/server/session-tools/manage-tasks.test.ts src/lib/server/session-tools/web-crawl.test.ts src/lib/app/view-constants.test.ts src/lib/quality/quality-summary.test.ts src/app/api/approvals/route.test.ts src/app/api/agents/agents-route.test.ts src/app/api/tasks/tasks-route.test.ts src/app/api/tasks/task-workspace-route.test.ts src/app/api/chats/chat-route.test.ts src/app/api/chats/clear-route.test.ts src/app/api/chats/compact-route.test.ts src/app/api/chats/context-pack-route.test.ts src/app/api/chats/context-status-route.test.ts src/app/api/config-versions/config-versions-route.test.ts src/app/api/runs/run-handoff-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/extensions/managed-resources/route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/portability/export/route.test.ts src/app/api/portability/import/route.test.ts src/app/api/providers/[id]/route.test.ts src/app/api/schedules/preview/route.test.ts src/app/api/schedules/schedule-history-route.test.ts src/app/api/tts/route.test.ts",
91+
"test:runtime": "tsx --test src/lib/a2a/agent-card.test.ts src/lib/agent-planning-mode.test.ts src/lib/agent-config-history.test.ts src/lib/strip-internal-metadata.test.ts src/lib/provider-sets.test.ts src/lib/providers/opencode-cli.test.ts src/lib/providers/cli-provider-metadata.test.ts src/lib/providers/cli-utils.test.ts src/lib/providers/generic-cli.test.ts src/lib/server/agents/delegation-advisory.test.ts src/lib/server/cli-provider-readiness.test.ts src/lib/server/provider-health.test.ts src/lib/server/provider-diagnostics.test.ts src/lib/server/mcp-gateway-runtime.test.ts src/lib/server/mcp-connection-pool.test.ts src/lib/server/knowledge-sources.test.ts src/lib/server/extension-managed-resources.test.ts src/lib/server/eval/baseline.test.ts src/lib/server/eval/environment-plan.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/chat-execution/chat-turn-preparation.test.ts src/lib/server/chat-execution/iteration-timers.test.ts src/lib/server/chat-execution/post-stream-finalization.test.ts src/lib/server/chat-execution/prompt-sections.planning-mode.test.ts src/lib/server/chat-execution/reasoning-tag-scrubber.test.ts src/lib/server/chats/clear-undo-snapshots.test.ts src/lib/server/chats/session-context-pack.test.ts src/lib/server/connectors/email.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/runtime/queue-retry-policy.test.ts src/lib/server/runs/run-brief.test.ts src/lib/server/runs/run-handoff.test.ts src/lib/server/operations/operation-pulse.test.ts src/lib/server/schedules/schedule-history.test.ts src/lib/server/schedules/schedule-timing.test.ts src/lib/server/schedules/schedule-preview.test.ts src/lib/quality/release-readiness.test.ts src/lib/quality/architecture-health.test.ts src/lib/server/artifacts/artifact-resolver.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.test.ts src/lib/server/missions/mission-templates.test.ts src/lib/server/sharing/share-link-repository.test.ts src/lib/server/sharing/share-resolver.test.ts src/lib/server/tasks/task-execution-workspace.test.ts src/lib/server/tasks/task-execution-policy.test.ts src/lib/server/tasks/task-handoff.test.ts src/lib/server/tasks/task-service.test.ts src/lib/server/session-tools/execute.test.ts src/lib/server/session-tools/manage-tasks.test.ts src/lib/server/session-tools/web-crawl.test.ts src/lib/app/view-constants.test.ts src/lib/quality/quality-summary.test.ts src/app/api/approvals/route.test.ts src/app/api/agents/agents-route.test.ts src/app/api/tasks/tasks-route.test.ts src/app/api/tasks/task-workspace-route.test.ts src/app/api/chats/chat-route.test.ts src/app/api/chats/clear-route.test.ts src/app/api/chats/compact-route.test.ts src/app/api/chats/context-pack-route.test.ts src/app/api/chats/context-status-route.test.ts src/app/api/config-versions/config-versions-route.test.ts src/app/api/runs/run-handoff-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/extensions/managed-resources/route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/portability/export/route.test.ts src/app/api/portability/import/route.test.ts src/app/api/providers/[id]/route.test.ts src/app/api/schedules/preview/route.test.ts src/app/api/schedules/schedule-history-route.test.ts src/app/api/tts/route.test.ts",
9292
"test:builder": "tsx --test src/features/protocols/builder/utils/nodes-to-template.test.ts src/features/protocols/builder/utils/template-to-nodes.test.ts src/features/protocols/builder/validators/dag-validator.test.ts",
9393
"test:e2e": "node --import tsx scripts/browser-e2e-smoke.ts",
9494
"test:mcp:conformance": "node --import tsx ./scripts/mcp-conformance-check.ts",

src/components/schedules/schedule-console.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,8 @@ function historyActionLabel(action: ScheduleHistoryEntry['action']): string {
162162
return 'Skipped'
163163
case 'failed':
164164
return 'Failed'
165+
case 'repaired':
166+
return 'Repaired'
165167
default:
166168
return action
167169
}
@@ -171,6 +173,7 @@ function historyActionBadge(action: ScheduleHistoryEntry['action']): string {
171173
if (action === 'created' || action === 'restored' || action === 'run_started') return badgeClass('completed')
172174
if (action === 'failed') return badgeClass('failed')
173175
if (action === 'skipped' || action === 'archived') return badgeClass('paused')
176+
if (action === 'repaired') return badgeClass('running')
174177
return badgeClass('running')
175178
}
176179

src/lib/server/runtime/scheduler.test.ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,135 @@ describe('scheduler wake targeting', () => {
202202
assert.deepEqual(output.deliveryModes, ['silent'])
203203
})
204204

205+
it('repairs stale future cron next-run slots without launching a run', () => {
206+
const output = runSchedulerWithTempDataDir(`
207+
const storageMod = await import('@/lib/server/storage')
208+
const schedulerMod = await import('@/lib/server/runtime/scheduler')
209+
const storage = storageMod.default || storageMod
210+
const scheduler = schedulerMod.default || schedulerMod
211+
212+
const now = Date.parse('2026-05-06T07:30:00.000Z')
213+
const staleFuture = Date.parse('2026-05-12T08:00:00.000Z')
214+
215+
storage.saveSchedules({
216+
'sched-cron': {
217+
id: 'sched-cron',
218+
name: 'Daily status',
219+
agentId: 'agent-1',
220+
taskPrompt: 'Send the daily status.',
221+
scheduleType: 'cron',
222+
cron: '0 8 * * *',
223+
timezone: 'UTC',
224+
status: 'active',
225+
nextRunAt: staleFuture,
226+
createdAt: now - 10_000,
227+
updatedAt: now - 10_000,
228+
},
229+
})
230+
231+
await scheduler.runSchedulerTickForTests(now)
232+
const schedule = storage.loadSchedules()['sched-cron']
233+
234+
console.log(JSON.stringify({
235+
status: schedule.status,
236+
nextRunAt: schedule.nextRunAt,
237+
taskCount: Object.keys(storage.loadTasks()).length,
238+
historyAction: schedule.history?.[0]?.action || null,
239+
historyReason: schedule.history?.[0]?.metadata?.reason || null,
240+
}))
241+
`)
242+
243+
assert.equal(output.status, 'active')
244+
assert.equal(output.nextRunAt, Date.parse('2026-05-06T08:00:00.000Z'))
245+
assert.equal(output.taskCount, 0)
246+
assert.equal(output.historyAction, 'repaired')
247+
assert.equal(output.historyReason, 'stale_future')
248+
})
249+
250+
it('advances cron schedules from the scheduler tick time after firing', () => {
251+
const output = runSchedulerWithTempDataDir(`
252+
const storageMod = await import('@/lib/server/storage')
253+
const schedulerMod = await import('@/lib/server/runtime/scheduler')
254+
const heartbeatWakeMod = await import('@/lib/server/runtime/heartbeat-wake')
255+
const storage = storageMod.default || storageMod
256+
const scheduler = schedulerMod.default || schedulerMod
257+
const heartbeatWake = heartbeatWakeMod.default || heartbeatWakeMod
258+
259+
const now = Date.parse('2030-01-01T08:00:30.000Z')
260+
const dueAt = Date.parse('2030-01-01T08:00:00.000Z')
261+
262+
storage.saveAgents({
263+
'agent-1': {
264+
id: 'agent-1',
265+
name: 'Daily Agent',
266+
description: '',
267+
systemPrompt: '',
268+
provider: 'openai',
269+
model: 'gpt-test',
270+
threadSessionId: 'thread-main',
271+
createdAt: now - 10_000,
272+
updatedAt: now - 10_000,
273+
},
274+
})
275+
276+
storage.saveSessions({
277+
'thread-main': {
278+
id: 'thread-main',
279+
name: 'Daily Agent',
280+
cwd: process.env.WORKSPACE_DIR,
281+
user: 'tester',
282+
provider: 'openai',
283+
model: 'gpt-test',
284+
claudeSessionId: null,
285+
messages: [],
286+
createdAt: now - 10_000,
287+
lastActiveAt: now - 5_000,
288+
active: true,
289+
currentRunId: null,
290+
agentId: 'agent-1',
291+
shortcutForAgentId: 'agent-1',
292+
},
293+
})
294+
295+
storage.saveSchedules({
296+
'sched-cron': {
297+
id: 'sched-cron',
298+
name: 'Daily wake',
299+
agentId: 'agent-1',
300+
taskPrompt: 'Wake for the daily status.',
301+
taskMode: 'wake_only',
302+
message: 'Run the daily status.',
303+
scheduleType: 'cron',
304+
cron: '0 8 * * *',
305+
timezone: 'UTC',
306+
status: 'active',
307+
nextRunAt: dueAt,
308+
createdInSessionId: 'thread-main',
309+
createdAt: now - 10_000,
310+
updatedAt: now - 10_000,
311+
},
312+
})
313+
314+
await scheduler.runSchedulerTickForTests(now)
315+
const schedule = storage.loadSchedules()['sched-cron']
316+
const wakes = heartbeatWake.snapshotPendingHeartbeatWakesForTests()
317+
318+
console.log(JSON.stringify({
319+
status: schedule.status,
320+
nextRunAt: schedule.nextRunAt,
321+
runNumber: schedule.runNumber,
322+
historyAction: schedule.history?.[0]?.action || null,
323+
wakeCount: wakes.length,
324+
}))
325+
`)
326+
327+
assert.equal(output.status, 'active')
328+
assert.equal(output.nextRunAt, Date.parse('2030-01-02T08:00:00.000Z'))
329+
assert.equal(output.runNumber, 1)
330+
assert.equal(output.historyAction, 'run_started')
331+
assert.equal(output.wakeCount, 1)
332+
})
333+
205334
it('reuses a persistent mission for scheduled task runs', () => {
206335
const output = runSchedulerWithTempDataDir(`
207336
const storageMod = await import('@/lib/server/storage')

0 commit comments

Comments
 (0)