fix(tasks): harden loop reconciliation against renames and malformed files
Review fixes for the markdown loop feature: - Loop-owned tasks now adopt by loop file path, not task name, so renaming a loop (frontmatter name or UI rename) renames the task in place instead of leaving a stale duplicate that keeps running the old definition; orphan duplicates of the same file are unscheduled. - Unparseable loop files are reported to the scheduler as definition:null entries: a task whose file still exists is kept with its last good definition, and only a genuinely removed file unschedules it. Transiently malformed files (mid-edit, bad merge) no longer delete tasks or their runtime state. - Adoption preserves UI-only execution fields (goalEnabled, goalTokenBudget, permissionAutoAccept, variant) that the portable format does not define. - DELETE on a loop-sourced task now returns 400 with guidance to remove the loop file, instead of being silently undone by the next reconcile. - Loops default to enabled: false; discovery of repository content never auto-executes scheduled sessions unless the file explicitly enables them. Regression tests for each fix; DOCUMENTATION.md updated.
This commit is contained in:
@@ -21,7 +21,9 @@
|
||||
* Field mapping (see packages/ui/src/lib/scheduledTasksApi.ts):
|
||||
* name -> task.name
|
||||
* schedule -> task.schedule.kind "cron" + task.schedule.cron
|
||||
* enabled -> task.enabled (default true)
|
||||
* enabled -> task.enabled (default false — loops only run when the file
|
||||
* explicitly enables them, so discovery never auto-executes
|
||||
* repository content)
|
||||
* model -> split into task.execution.providerID / task.execution.modelID
|
||||
* agent -> task.execution.agent (optional)
|
||||
* timezone -> task.schedule.timezone (optional, defaults to the server zone)
|
||||
@@ -116,7 +118,7 @@ export const parseLoopDefinition = (filePath) => {
|
||||
|
||||
return {
|
||||
name,
|
||||
enabled: typeof frontmatter.enabled === 'boolean' ? frontmatter.enabled : true,
|
||||
enabled: typeof frontmatter.enabled === 'boolean' ? frontmatter.enabled : false,
|
||||
schedule: {
|
||||
kind: 'cron',
|
||||
cron,
|
||||
@@ -170,13 +172,21 @@ export const discoverLoopFiles = (projectPath) => {
|
||||
/**
|
||||
* Discover and parse all loops for a project. Project-scope loops shadow
|
||||
* user-scope loops with the same name; among project files the nearest
|
||||
* ancestor wins. Malformed files are skipped with a warning.
|
||||
* ancestor wins.
|
||||
*
|
||||
* Unparseable files are reported as `{ scope, filePath, definition: null }`
|
||||
* entries instead of being dropped: the scheduler must distinguish "file is
|
||||
* gone" (unschedule its task) from "file exists but is currently malformed"
|
||||
* (keep its task with the last good definition until the file is fixed).
|
||||
* Malformed files never block valid ones in the same or other scopes.
|
||||
*/
|
||||
export const discoverLoops = (projectPath) => {
|
||||
const byName = new Map();
|
||||
const loops = [];
|
||||
for (const { filePath, scope } of discoverLoopFiles(projectPath)) {
|
||||
const definition = parseLoopDefinition(filePath);
|
||||
if (!definition) {
|
||||
loops.push({ scope, filePath, definition: null });
|
||||
continue;
|
||||
}
|
||||
const existing = byName.get(definition.name);
|
||||
@@ -185,5 +195,8 @@ export const discoverLoops = (projectPath) => {
|
||||
}
|
||||
byName.set(definition.name, { scope, filePath, definition });
|
||||
}
|
||||
return [...byName.values()];
|
||||
for (const entry of byName.values()) {
|
||||
loops.push(entry);
|
||||
}
|
||||
return loops;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user