@saadnvd1/agent-os 0.3.534 → 0.3.535
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -2
- package/components/Schedules/ScheduleForm.tsx +20 -2
- package/lib/db/migrations.ts +16 -0
- package/lib/db/schedules-move.ts +220 -0
- package/package.json +1 -1
- package/scripts/revert-schedules-move.ts +24 -0
package/README.md
CHANGED
|
@@ -969,8 +969,12 @@ name, a time (presets for hourly, daily, weekdays and weekly, or any
|
|
|
969
969
|
five-field cron expression, in America/Chicago unless you pick another zone),
|
|
970
970
|
a project and a prompt, and starts one of:
|
|
971
971
|
|
|
972
|
-
- **Task**:
|
|
973
|
-
|
|
972
|
+
- **Task** and **Session**: now recipes (Settings > Recipes) with a timer.
|
|
973
|
+
Migration 81 turned each existing recurring task or session schedule into a
|
|
974
|
+
recipe, with its run history; the Schedules form no longer offers them. A
|
|
975
|
+
one-off that still has one of these kinds is left as it was. To undo the
|
|
976
|
+
move, stop AgentOS and run
|
|
977
|
+
`npx tsx scripts/revert-schedules-move.ts --db <path to agent-os.db>`.
|
|
974
978
|
- **Orchestrator**: the prompt, posted to the workspace's orchestrator as a
|
|
975
979
|
message from "Schedule <name>", marked as a scheduled prompt so it never
|
|
976
980
|
counts as an approval.
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
wall,
|
|
35
35
|
type PresetKind,
|
|
36
36
|
} from "@/lib/schedules/cron";
|
|
37
|
+
import { recipesUiActions } from "@/stores/recipesUi";
|
|
37
38
|
import { parseWhen, relativeTime } from "@/lib/schedules/when";
|
|
38
39
|
|
|
39
40
|
const KINDS: { value: ScheduleKind; label: string; hint: string }[] = [
|
|
@@ -59,6 +60,10 @@ const KINDS: { value: ScheduleKind; label: string; hint: string }[] = [
|
|
|
59
60
|
},
|
|
60
61
|
];
|
|
61
62
|
|
|
63
|
+
// New jobs of these shapes are recipes; the kinds stay only to edit a
|
|
64
|
+
// schedule that still has one (a one-off, say).
|
|
65
|
+
const LEGACY_KINDS: ScheduleKind[] = ["task", "session"];
|
|
66
|
+
|
|
62
67
|
type Preset = PresetKind | "custom" | "once";
|
|
63
68
|
|
|
64
69
|
const PRESETS: { value: Preset; label: string }[] = [
|
|
@@ -171,7 +176,7 @@ export function ScheduleForm({
|
|
|
171
176
|
);
|
|
172
177
|
const [name, setName] = useState(existing?.name ?? draft?.name ?? "");
|
|
173
178
|
const [kind, setKind] = useState<ScheduleKind>(
|
|
174
|
-
existing?.kind ?? draft?.kind ?? "
|
|
179
|
+
existing?.kind ?? draft?.kind ?? "orchestrator"
|
|
175
180
|
);
|
|
176
181
|
const [targetId, setTargetId] = useState(
|
|
177
182
|
existing?.target_session_id ?? draft?.targetSessionId ?? ""
|
|
@@ -329,7 +334,9 @@ export function ScheduleForm({
|
|
|
329
334
|
<Segmented
|
|
330
335
|
label="What it starts"
|
|
331
336
|
value={kind}
|
|
332
|
-
options={KINDS
|
|
337
|
+
options={KINDS.filter(
|
|
338
|
+
(k) => !LEGACY_KINDS.includes(k.value) || existing?.kind === k.value
|
|
339
|
+
)}
|
|
333
340
|
onChange={(k) => {
|
|
334
341
|
setKind(k);
|
|
335
342
|
if (preset === "every" && !frequent(k)) setPreset("daily");
|
|
@@ -338,6 +345,17 @@ export function ScheduleForm({
|
|
|
338
345
|
<p className="text-muted-foreground text-xs">
|
|
339
346
|
{KINDS.find((k) => k.value === kind)?.hint}
|
|
340
347
|
</p>
|
|
348
|
+
<p className="text-muted-foreground text-xs">
|
|
349
|
+
To start a task or session on a timer, use a{" "}
|
|
350
|
+
<button
|
|
351
|
+
type="button"
|
|
352
|
+
className="text-foreground underline underline-offset-2"
|
|
353
|
+
onClick={() => recipesUiActions.open(workspaceId ?? null)}
|
|
354
|
+
>
|
|
355
|
+
recipe
|
|
356
|
+
</button>
|
|
357
|
+
.
|
|
358
|
+
</p>
|
|
341
359
|
</Field>
|
|
342
360
|
|
|
343
361
|
{!workspaceId && (
|
package/lib/db/migrations.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
TABLES_WATCHED_80,
|
|
9
9
|
} from "./changes";
|
|
10
10
|
import type Database from "better-sqlite3";
|
|
11
|
+
import { moveSchedulesToRecipes } from "./schedules-move";
|
|
11
12
|
|
|
12
13
|
interface Migration {
|
|
13
14
|
id: number;
|
|
@@ -1957,6 +1958,21 @@ const migrations: Migration[] = [
|
|
|
1957
1958
|
installChangeTriggers(db, TABLES_WATCHED_80);
|
|
1958
1959
|
},
|
|
1959
1960
|
},
|
|
1961
|
+
{
|
|
1962
|
+
id: 81,
|
|
1963
|
+
name: "schedules_into_recipes",
|
|
1964
|
+
up: (db) => {
|
|
1965
|
+
// Live task and session schedules become recipes with a timer, and
|
|
1966
|
+
// their runs become recipe runs (lib/db/schedules-move). Reversible:
|
|
1967
|
+
// scripts/revert-schedules-move.ts.
|
|
1968
|
+
const cols = (
|
|
1969
|
+
db.prepare(`PRAGMA table_info(schedules)`).all() as { name: string }[]
|
|
1970
|
+
).map((c) => c.name);
|
|
1971
|
+
if (!cols.includes("recipe_id") || !cols.includes("migrated_from_kind"))
|
|
1972
|
+
throw new Error("migration 80 hasn't run");
|
|
1973
|
+
moveSchedulesToRecipes(db);
|
|
1974
|
+
},
|
|
1975
|
+
},
|
|
1960
1976
|
];
|
|
1961
1977
|
|
|
1962
1978
|
// `upTo`: stop after this id (tests that start from an older database).
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
// Migration 81: live task and session schedules become recipes with a timer
|
|
2
|
+
// (lib/recipes), and their runs become recipe runs. `migrated_from_kind` on
|
|
3
|
+
// the schedule row is what makes it reversible: revertSchedulesMove.
|
|
4
|
+
//
|
|
5
|
+
// A one-off (run_at) and an archived schedule are left as they are: a timer
|
|
6
|
+
// repeats, and history of archived ones stays where it was.
|
|
7
|
+
|
|
8
|
+
import { randomUUID } from "crypto";
|
|
9
|
+
import type Database from "better-sqlite3";
|
|
10
|
+
|
|
11
|
+
interface ScheduleRow {
|
|
12
|
+
id: string;
|
|
13
|
+
workspace_id: string;
|
|
14
|
+
project_id: string | null;
|
|
15
|
+
name: string;
|
|
16
|
+
prompt: string;
|
|
17
|
+
kind: "task" | "session";
|
|
18
|
+
created_by_session_id: string | null;
|
|
19
|
+
created_at: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface RunRow {
|
|
23
|
+
id: number;
|
|
24
|
+
trigger: string;
|
|
25
|
+
session_id: string;
|
|
26
|
+
created_at: string;
|
|
27
|
+
task_status: string | null;
|
|
28
|
+
pr_number: number | null;
|
|
29
|
+
pr_url: string | null;
|
|
30
|
+
session_exists: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const TRIGGER: Record<string, string> = {
|
|
34
|
+
schedule: "timer",
|
|
35
|
+
"catch-up": "catch-up",
|
|
36
|
+
manual: "manual",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const FINAL: Record<string, string> = {
|
|
40
|
+
merged: "succeeded",
|
|
41
|
+
done: "succeeded",
|
|
42
|
+
dropped: "failed",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// The name is unique among a workspace's live recipes, ignoring case.
|
|
46
|
+
function freeName(
|
|
47
|
+
db: Database.Database,
|
|
48
|
+
workspaceId: string,
|
|
49
|
+
name: string
|
|
50
|
+
): string {
|
|
51
|
+
const taken = db.prepare(
|
|
52
|
+
`SELECT 1 FROM recipes WHERE workspace_id = ? AND archived_at IS NULL AND name = ? COLLATE NOCASE`
|
|
53
|
+
);
|
|
54
|
+
if (!taken.get(workspaceId, name)) return name;
|
|
55
|
+
for (let n = 1; ; n++) {
|
|
56
|
+
const candidate = `${name} (schedule${n > 1 ? ` ${n}` : ""})`;
|
|
57
|
+
if (!taken.get(workspaceId, candidate)) return candidate;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function moveSchedulesToRecipes(db: Database.Database): {
|
|
62
|
+
schedules: number;
|
|
63
|
+
runs: number;
|
|
64
|
+
} {
|
|
65
|
+
const sessionCols = (
|
|
66
|
+
db.prepare(`PRAGMA table_info(sessions)`).all() as { name: string }[]
|
|
67
|
+
).map((c) => c.name);
|
|
68
|
+
const pr = sessionCols.includes("pr_number")
|
|
69
|
+
? "s.pr_number, s.pr_url"
|
|
70
|
+
: "NULL AS pr_number, NULL AS pr_url";
|
|
71
|
+
const hasTaskStatus = sessionCols.includes("task_status");
|
|
72
|
+
|
|
73
|
+
const schedules = db
|
|
74
|
+
.prepare(
|
|
75
|
+
`SELECT id, workspace_id, project_id, name, prompt, kind, created_by_session_id, created_at
|
|
76
|
+
FROM schedules
|
|
77
|
+
WHERE archived_at IS NULL AND kind IN ('task','session')
|
|
78
|
+
AND run_at IS NULL AND project_id IS NOT NULL
|
|
79
|
+
ORDER BY created_at, id`
|
|
80
|
+
)
|
|
81
|
+
.all() as ScheduleRow[];
|
|
82
|
+
|
|
83
|
+
const insertRecipe = db.prepare(
|
|
84
|
+
`INSERT INTO recipes (id, workspace_id, project_id, name, kind, brief, model, view, created_by_session_id, updated_by_session_id, created_at)
|
|
85
|
+
VALUES (?, ?, ?, ?, ?, ?, NULL, 'chat', ?, ?, ?)`
|
|
86
|
+
);
|
|
87
|
+
const insertRevision = db.prepare(
|
|
88
|
+
`INSERT INTO recipe_revisions (recipe_id, brief, settings, edited_by_session_id) VALUES (?, ?, ?, ?)`
|
|
89
|
+
);
|
|
90
|
+
const switchSchedule = db.prepare(
|
|
91
|
+
`UPDATE schedules SET kind = 'recipe', recipe_id = ?, migrated_from_kind = ?, updated_at = datetime('now') WHERE id = ?`
|
|
92
|
+
);
|
|
93
|
+
const runsOf = db.prepare(
|
|
94
|
+
`SELECT r.id, r.trigger, r.session_id, r.created_at,
|
|
95
|
+
${hasTaskStatus ? "s.task_status" : "NULL"} AS task_status, ${pr},
|
|
96
|
+
s.id IS NOT NULL AS session_exists
|
|
97
|
+
FROM schedule_runs r LEFT JOIN sessions s ON s.id = r.session_id
|
|
98
|
+
WHERE r.schedule_id = ? AND r.session_id IS NOT NULL
|
|
99
|
+
ORDER BY r.id`
|
|
100
|
+
);
|
|
101
|
+
const insertRun = db.prepare(
|
|
102
|
+
`INSERT INTO recipe_runs (recipe_id, seq, trigger, schedule_run_id, prompt, status, session_id, pr_number, pr_url, summary, started_at, created_at)
|
|
103
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
let movedRuns = 0;
|
|
107
|
+
const move = db.transaction(() => {
|
|
108
|
+
for (const s of schedules) {
|
|
109
|
+
const recipeId = randomUUID();
|
|
110
|
+
insertRecipe.run(
|
|
111
|
+
recipeId,
|
|
112
|
+
s.workspace_id,
|
|
113
|
+
s.project_id,
|
|
114
|
+
freeName(db, s.workspace_id, s.name),
|
|
115
|
+
s.kind,
|
|
116
|
+
s.prompt,
|
|
117
|
+
s.created_by_session_id,
|
|
118
|
+
s.created_by_session_id,
|
|
119
|
+
s.created_at
|
|
120
|
+
);
|
|
121
|
+
insertRevision.run(
|
|
122
|
+
recipeId,
|
|
123
|
+
s.prompt,
|
|
124
|
+
JSON.stringify({
|
|
125
|
+
kind: s.kind,
|
|
126
|
+
model: null,
|
|
127
|
+
view: "chat",
|
|
128
|
+
auto_done: true,
|
|
129
|
+
max_minutes: null,
|
|
130
|
+
}),
|
|
131
|
+
s.created_by_session_id
|
|
132
|
+
);
|
|
133
|
+
switchSchedule.run(recipeId, s.kind, s.id);
|
|
134
|
+
|
|
135
|
+
const runs = runsOf.all(s.id) as RunRow[];
|
|
136
|
+
runs.forEach((r, i) => {
|
|
137
|
+
// One live run per recipe: only the latest may still be running
|
|
138
|
+
// (settle judges it); an earlier one with no recorded end is
|
|
139
|
+
// closed as cancelled rather than guessed a result.
|
|
140
|
+
const final = r.task_status ? FINAL[r.task_status] : undefined;
|
|
141
|
+
const last = i === runs.length - 1;
|
|
142
|
+
const status = final ?? (last ? "running" : "cancelled");
|
|
143
|
+
insertRun.run(
|
|
144
|
+
recipeId,
|
|
145
|
+
i + 1,
|
|
146
|
+
TRIGGER[r.trigger] ?? "timer",
|
|
147
|
+
r.id,
|
|
148
|
+
s.prompt,
|
|
149
|
+
status,
|
|
150
|
+
r.session_id,
|
|
151
|
+
r.pr_number,
|
|
152
|
+
r.pr_url,
|
|
153
|
+
status === "cancelled"
|
|
154
|
+
? "Ran before recipes; its outcome wasn't recorded"
|
|
155
|
+
: null,
|
|
156
|
+
r.created_at,
|
|
157
|
+
r.created_at
|
|
158
|
+
);
|
|
159
|
+
movedRuns++;
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
move();
|
|
164
|
+
return { schedules: schedules.length, runs: movedRuns };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Undo the move: each moved schedule gets its old kind (and the brief as it
|
|
168
|
+
// stands now) back, and the recipes the move made go, with their runs and
|
|
169
|
+
// revisions. The runs stay in schedule_runs, which the move never touched.
|
|
170
|
+
// Migration 81 stays recorded as applied, so a restart doesn't redo it.
|
|
171
|
+
export function revertSchedulesMove(db: Database.Database): {
|
|
172
|
+
schedules: number;
|
|
173
|
+
} {
|
|
174
|
+
const moved = db
|
|
175
|
+
.prepare(
|
|
176
|
+
`SELECT id, recipe_id, migrated_from_kind FROM schedules WHERE migrated_from_kind IS NOT NULL`
|
|
177
|
+
)
|
|
178
|
+
.all() as {
|
|
179
|
+
id: string;
|
|
180
|
+
recipe_id: string | null;
|
|
181
|
+
migrated_from_kind: string;
|
|
182
|
+
}[];
|
|
183
|
+
const recipe = db.prepare(
|
|
184
|
+
`SELECT brief, project_id FROM recipes WHERE id = ?`
|
|
185
|
+
);
|
|
186
|
+
const restore = db.prepare(
|
|
187
|
+
`UPDATE schedules SET kind = ?, recipe_id = NULL, migrated_from_kind = NULL,
|
|
188
|
+
prompt = COALESCE(?, prompt), project_id = COALESCE(?, project_id), updated_at = datetime('now')
|
|
189
|
+
WHERE id = ?`
|
|
190
|
+
);
|
|
191
|
+
const drop = (table: string) =>
|
|
192
|
+
db.prepare(`DELETE FROM ${table} WHERE recipe_id = ?`);
|
|
193
|
+
const [dropRuns, dropRevisions] = [
|
|
194
|
+
drop("recipe_runs"),
|
|
195
|
+
drop("recipe_revisions"),
|
|
196
|
+
];
|
|
197
|
+
const dropRecipe = db.prepare(`DELETE FROM recipes WHERE id = ?`);
|
|
198
|
+
|
|
199
|
+
db.transaction(() => {
|
|
200
|
+
for (const s of moved) {
|
|
201
|
+
const r = s.recipe_id
|
|
202
|
+
? (recipe.get(s.recipe_id) as
|
|
203
|
+
| { brief: string; project_id: string }
|
|
204
|
+
| undefined)
|
|
205
|
+
: undefined;
|
|
206
|
+
restore.run(
|
|
207
|
+
s.migrated_from_kind,
|
|
208
|
+
r?.brief ?? null,
|
|
209
|
+
r?.project_id ?? null,
|
|
210
|
+
s.id
|
|
211
|
+
);
|
|
212
|
+
if (s.recipe_id) {
|
|
213
|
+
dropRuns.run(s.recipe_id);
|
|
214
|
+
dropRevisions.run(s.recipe_id);
|
|
215
|
+
dropRecipe.run(s.recipe_id);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
})();
|
|
219
|
+
return { schedules: moved.length };
|
|
220
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Undoes migration 81: schedules that became recipes get their old kind
|
|
3
|
+
* (task or session) and brief back, and the recipes the move made are
|
|
4
|
+
* removed with their runs. Stop AgentOS first; it reads the database too.
|
|
5
|
+
*
|
|
6
|
+
* npx tsx scripts/revert-schedules-move.ts --db <path to agent-os.db>
|
|
7
|
+
*
|
|
8
|
+
* Migration 81 stays recorded as applied, so a restart won't redo the move.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import Database from "better-sqlite3";
|
|
12
|
+
import { revertSchedulesMove } from "../lib/db/schedules-move";
|
|
13
|
+
|
|
14
|
+
const args = process.argv.slice(2);
|
|
15
|
+
const dbPath = args[args.indexOf("--db") + 1];
|
|
16
|
+
if (!args.includes("--db") || !dbPath || dbPath.startsWith("--")) {
|
|
17
|
+
console.error("usage: revert-schedules-move.ts --db <path>");
|
|
18
|
+
process.exit(2);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const db = new Database(dbPath, { fileMustExist: true, timeout: 10000 });
|
|
22
|
+
const { schedules } = revertSchedulesMove(db);
|
|
23
|
+
console.log(`Restored ${schedules} schedule(s) to their old kind.`);
|
|
24
|
+
db.close();
|