From ce6912fe40cbd5aabfaed14f3b149ee587fc7d70 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Sun, 9 Aug 2026 20:32:30 +0300 Subject: [PATCH] fix(tasks): sync markdown loops when listing tasks --- .../docs/content/docs/scheduled-tasks.mdx | 2 +- .../lib/scheduled-tasks/DOCUMENTATION.md | 3 +- .../web/server/lib/scheduled-tasks/service.js | 2 +- .../lib/scheduled-tasks/service.test.js | 30 +++++++++++++++++++ 4 files changed, 34 insertions(+), 3 deletions(-) diff --git a/packages/docs/content/docs/scheduled-tasks.mdx b/packages/docs/content/docs/scheduled-tasks.mdx index 4f5a6554..ed2fe202 100644 --- a/packages/docs/content/docs/scheduled-tasks.mdx +++ b/packages/docs/content/docs/scheduled-tasks.mdx @@ -25,7 +25,7 @@ Check **Run as goal** to make the run pursue its prompt to completion instead of ## Loops: scheduled tasks as markdown files -A **loop** is a scheduled task defined as a portable markdown file you can commit to your repo. Drop a file into `.agents/loops/` and the task appears on the next sync — no dialog needed: +A **loop** is a scheduled task defined as a portable markdown file you can commit to your repo. Drop a file into `.agents/loops/` and open the Scheduled Tasks list to sync it — no server restart needed: ```markdown --- diff --git a/packages/web/server/lib/scheduled-tasks/DOCUMENTATION.md b/packages/web/server/lib/scheduled-tasks/DOCUMENTATION.md index 4be03824..a7fa66c0 100644 --- a/packages/web/server/lib/scheduled-tasks/DOCUMENTATION.md +++ b/packages/web/server/lib/scheduled-tasks/DOCUMENTATION.md @@ -21,10 +21,11 @@ Server-owned scheduled task runtime and routes for OpenChamber-only automation. - `packages/web/server/lib/scheduled-tasks/loops.js` - Discovery of `.agents/loops/*.md` (project scope, ancestors up to the worktree root) and `~/.agents/loops/*.md` (user scope) - Frontmatter parsing into scheduled-task definitions - - `syncProject` reconciles discovered loops with the persisted task list on every project sync (startup, task save/delete) + - `syncProject` reconciles discovered loops with the persisted task list on every project sync (startup, task list load, task save/delete) - `packages/web/server/lib/scheduled-tasks/routes.js` - Scheduled task CRUD endpoints + - Listing tasks reconciles loop files first, so opening the Scheduled Tasks UI discovers file additions, edits, and removals without a server restart - Manual run endpoint - OpenChamber events SSE stream endpoint diff --git a/packages/web/server/lib/scheduled-tasks/service.js b/packages/web/server/lib/scheduled-tasks/service.js index 4d94251a..506550f9 100644 --- a/packages/web/server/lib/scheduled-tasks/service.js +++ b/packages/web/server/lib/scheduled-tasks/service.js @@ -50,7 +50,7 @@ export const createScheduledTaskService = (dependencies) => { const list = async (projectID) => { await findProjectByID(projectID); - return projectConfigRuntime.listScheduledTasks(projectID); + return scheduledTasksRuntime.syncProject(projectID); }; const upsert = async (projectID, taskInput) => { diff --git a/packages/web/server/lib/scheduled-tasks/service.test.js b/packages/web/server/lib/scheduled-tasks/service.test.js index a90c1b64..61643157 100644 --- a/packages/web/server/lib/scheduled-tasks/service.test.js +++ b/packages/web/server/lib/scheduled-tasks/service.test.js @@ -34,6 +34,36 @@ const loopTask = { execution: { prompt: 'digest', providerID: 'openai', modelID: 'gpt-4.1' }, }; +describe('scheduled-task service list', () => { + it('reconciles loop files before returning tasks', async () => { + const syncedTasks = [loopTask]; + const { service, projectConfigRuntime, scheduledTasksRuntime } = createService({ + scheduledTasksRuntime: { + syncProject: vi.fn(async () => syncedTasks), + }, + }); + + await expect(service.list('project-test')).resolves.toBe(syncedTasks); + expect(scheduledTasksRuntime.syncProject).toHaveBeenCalledOnce(); + expect(scheduledTasksRuntime.syncProject).toHaveBeenCalledWith('project-test'); + expect(projectConfigRuntime.listScheduledTasks).not.toHaveBeenCalled(); + }); + + it('surfaces reconciliation failure instead of returning a stale list', async () => { + const syncError = new Error('loop reconciliation failed'); + const { service, projectConfigRuntime } = createService({ + scheduledTasksRuntime: { + syncProject: vi.fn(async () => { + throw syncError; + }), + }, + }); + + await expect(service.list('project-test')).rejects.toBe(syncError); + expect(projectConfigRuntime.listScheduledTasks).not.toHaveBeenCalled(); + }); +}); + describe('scheduled-task service remove', () => { it('rejects deleting a loop-sourced task while its loop file still exists', async () => { const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'oc-loop-delete-'));