@gentbajko/slopify 3.17.0 → 3.18.2
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/dist/edge/http/actions.js +2 -2
- package/dist/edge/http/actor.js +10 -0
- package/dist/edge/http/app.js +21 -0
- package/dist/edge/http/checkpoints.js +14 -6
- package/dist/edge/http/external-reviews.js +95 -0
- package/dist/edge/http/projects.js +36 -10
- package/dist/edge/http/reviews.js +53 -18
- package/dist/edge/http/revisions.js +5 -1
- package/dist/edge/http/settings.js +9 -1
- package/dist/edge/http/starts.js +41 -0
- package/dist/edge/mcp/attribution.js +38 -0
- package/dist/edge/mcp/call-route.js +203 -0
- package/dist/edge/mcp/excluded-routes.js +95 -0
- package/dist/edge/mcp/file-tools.js +141 -0
- package/dist/edge/mcp/http.js +79 -0
- package/dist/edge/mcp/instructions.js +17 -0
- package/dist/edge/mcp/library-tools.js +143 -0
- package/dist/edge/mcp/remake-tools.js +90 -0
- package/dist/edge/mcp/review-tools.js +64 -0
- package/dist/edge/mcp/route-tool.js +61 -0
- package/dist/edge/mcp/server.js +48 -0
- package/dist/edge/mcp/state-shapes.js +60 -0
- package/dist/edge/mcp/tools-channels.js +153 -0
- package/dist/edge/mcp/tools-library.js +151 -0
- package/dist/edge/mcp/tools-planning.js +137 -0
- package/dist/edge/mcp/tools-projects.js +167 -0
- package/dist/edge/mcp/tools-publishing.js +148 -0
- package/dist/edge/mcp/tools-runs.js +94 -0
- package/dist/edge/mcp/tools-schedules.js +143 -0
- package/dist/edge/mcp/tools-setup.js +234 -0
- package/dist/edge/mcp/tools-storage.js +99 -0
- package/dist/edge/mcp/tools.js +21 -0
- package/dist/extension/slopify-studio-chrome.zip +0 -0
- package/dist/extension/slopify-studio-firefox.zip +0 -0
- package/dist/kernel/db/migrations/0055-external-reviews.sql +56 -0
- package/dist/kernel/db/migrations/0056-agent-actions.sql +20 -0
- package/dist/kernel/db/migrations/0057-agent-action-once.sql +5 -0
- package/dist/kernel/db/migrations/0058-revision-work-admission.sql +3 -0
- package/dist/kernel/db/migrations/0059-external-review-canceled.sql +29 -0
- package/dist/kernel/db/migrations/0060-reservation-work.sql +4 -0
- package/dist/kernel/db/migrations/0061-project-control-counts.sql +4 -0
- package/dist/kernel/db/migrations/0062-told-failures.sql +8 -0
- package/dist/kernel/pipeline.js +6 -0
- package/dist/kernel/runner/graph.js +37 -2
- package/dist/kernel/runner/index.js +6 -3
- package/dist/main.js +29 -13
- package/dist/patch-notes/3.18.0.md +13 -0
- package/dist/patch-notes/3.18.1.md +11 -0
- package/dist/patch-notes/3.18.2.md +10 -0
- package/dist/patch-notes/index.json +18 -0
- package/dist/slices/admission/admitted-work.js +53 -0
- package/dist/slices/admission/heads.js +5 -0
- package/dist/slices/admission/repo.js +47 -29
- package/dist/slices/admission/schema.js +1 -0
- package/dist/slices/admission/stage-writes.js +21 -0
- package/dist/slices/admission/start.js +2 -1
- package/dist/slices/admission/state-words.js +110 -0
- package/dist/slices/agents/attention-work.js +233 -0
- package/dist/slices/agents/attention.js +168 -0
- package/dist/slices/agents/connection.js +15 -0
- package/dist/slices/agents/model.js +29 -0
- package/dist/slices/agents/repo.js +66 -0
- package/dist/slices/cancel/index.js +4 -1
- package/dist/slices/control/index.js +2 -1
- package/dist/slices/episodes/summarize.js +2 -1
- package/dist/slices/estimate/index.js +6 -2
- package/dist/slices/images/run.js +2 -1
- package/dist/slices/library/transfer.js +69 -0
- package/dist/slices/narration/run.js +2 -1
- package/dist/slices/notifications/address.js +40 -0
- package/dist/slices/notifications/notifier.js +43 -10
- package/dist/slices/notifications/rules.js +111 -45
- package/dist/slices/notifications/server.js +14 -0
- package/dist/slices/notifications/settings.js +1 -1
- package/dist/slices/notifications/subject.js +30 -0
- package/dist/slices/notifications/told.js +33 -0
- package/dist/slices/play-drafts/readiness.js +9 -2
- package/dist/slices/play-drafts/schema.js +2 -1
- package/dist/slices/rebuild/narration-words.js +76 -0
- package/dist/slices/rebuild/recipe-model.js +1 -0
- package/dist/slices/rebuild/recipe-reviews.js +44 -23
- package/dist/slices/rebuild/recovery-repo.js +0 -20
- package/dist/slices/rebuild/recovery-selection.js +14 -3
- package/dist/slices/rebuild/recovery.js +5 -2
- package/dist/slices/rebuild/remake-outdated.js +27 -0
- package/dist/slices/rebuild/repo.js +7 -2
- package/dist/slices/rebuild/review-redo.js +12 -0
- package/dist/slices/rebuild/runtime-agent-review.js +121 -0
- package/dist/slices/rebuild/runtime-export-inputs.js +26 -11
- package/dist/slices/rebuild/runtime-review.js +12 -2
- package/dist/slices/rebuild/runtime-run.js +11 -2
- package/dist/slices/rebuild/runtime-store.js +32 -5
- package/dist/slices/reruns/index.js +2 -1
- package/dist/slices/research/run.js +2 -1
- package/dist/slices/reviews/external-model.js +21 -0
- package/dist/slices/reviews/external-release.js +38 -0
- package/dist/slices/reviews/external-repo.js +109 -0
- package/dist/slices/reviews/external-rows.js +68 -0
- package/dist/slices/reviews/external-waiting.js +75 -0
- package/dist/slices/reviews/holds.js +144 -0
- package/dist/slices/reviews/model.js +14 -0
- package/dist/slices/reviews/repo.js +27 -10
- package/dist/slices/reviews/rules.js +5 -3
- package/dist/slices/reviews/verdict.js +18 -8
- package/dist/slices/revisions/repo.js +3 -0
- package/dist/slices/revisions/schema.js +1 -0
- package/dist/slices/schedules/agenda.js +47 -17
- package/dist/slices/schedules/prepare.js +6 -0
- package/dist/slices/schedules/schema.js +13 -1
- package/dist/slices/storage/arrange.js +21 -8
- package/dist/slices/storage/consequences.js +27 -0
- package/dist/slices/storage/portable.js +3 -0
- package/dist/slices/storage/project-files.js +66 -0
- package/dist/slices/storage/trim.js +6 -2
- package/dist/slices/subtitles/prepare.js +2 -1
- package/dist/slices/uploads/repo.js +12 -6
- package/dist/slices/uploads/rules.js +5 -0
- package/dist/slices/video/write-export.js +1 -1
- package/dist/slices/video/write-subtitles.js +1 -1
- package/dist/tutorials/Agents-MCP.md +168 -0
- package/dist/tutorials/Calendar.md +2 -1
- package/dist/tutorials/First-Launch-and-Welcome.md +1 -0
- package/dist/tutorials/Home-and-Projects.md +16 -9
- package/dist/tutorials/Home.md +1 -0
- package/dist/tutorials/Notifications.md +25 -6
- package/dist/tutorials/Project-Page.md +9 -3
- package/dist/tutorials/Publishing-to-YouTube.md +1 -0
- package/dist/tutorials/Recovery-and-Retries.md +17 -3
- package/dist/tutorials/Reviews-and-Checkpoints.md +22 -5
- package/dist/tutorials/Schedules.md +1 -1
- package/dist/tutorials/Settings-Reference.md +16 -1
- package/dist/tutorials/Troubleshooting.md +3 -1
- package/dist/tutorials/Where-Your-Files-Live.md +3 -3
- package/dist/tutorials/Your-First-Short.md +1 -0
- package/dist/tutorials/_Sidebar.md +1 -0
- package/dist/web/assets/{index-DpdXRssU.css → index-BKbZIxAq.css} +1 -1
- package/dist/web/assets/index-DASWFus0.js +326 -0
- package/dist/web/assets/{pdf-B42WW32R.js → pdf-CF97Fw5v.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-CbmBh8V-.js +0 -325
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
-- What an agent changed through MCP. A project's rows are its history ("by agent") and go with
|
|
2
|
+
-- it; a change that names no project (a schedule's topics, the Library, a setting) keeps none,
|
|
3
|
+
-- and Settings → Connections → Agents (MCP) lists it.
|
|
4
|
+
CREATE TABLE agent_actions (
|
|
5
|
+
id TEXT PRIMARY KEY,
|
|
6
|
+
project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
|
|
7
|
+
schedule_id TEXT,
|
|
8
|
+
tool TEXT NOT NULL,
|
|
9
|
+
title TEXT NOT NULL,
|
|
10
|
+
revision_id TEXT,
|
|
11
|
+
created_at TEXT NOT NULL
|
|
12
|
+
);
|
|
13
|
+
CREATE INDEX agent_actions_project ON agent_actions(project_id, created_at);
|
|
14
|
+
CREATE INDEX agent_actions_time ON agent_actions(created_at);
|
|
15
|
+
-- Who last started a project's work, the person or an agent: the run's notices say so.
|
|
16
|
+
CREATE TABLE project_starts (
|
|
17
|
+
project_id TEXT PRIMARY KEY REFERENCES projects(id) ON DELETE CASCADE,
|
|
18
|
+
actor TEXT NOT NULL CHECK(actor IN ('person','agent')),
|
|
19
|
+
started_at TEXT NOT NULL
|
|
20
|
+
);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
-- An agent's call sent again with the same idempotencyKey (a retry after a lost answer) gets its
|
|
2
|
+
-- first answer back: it is one change, so it is recorded once.
|
|
3
|
+
ALTER TABLE agent_actions ADD COLUMN idempotency_key TEXT;
|
|
4
|
+
CREATE UNIQUE INDEX agent_actions_once ON agent_actions(tool, COALESCE(project_id, ''), idempotency_key)
|
|
5
|
+
WHERE idempotency_key IS NOT NULL;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
-- Whether an admission has a failed sibling is asked for every idle project on each list,
|
|
2
|
+
-- calendar and needs_attention read (`reviews/holds.ts` nothingAdmitted): an index, not a scan.
|
|
3
|
+
CREATE INDEX revision_work_admission ON revision_work(admission_id) WHERE admission_id IS NOT NULL;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
-- Cancel closes a run's waiting reviews as canceled, an outcome of their own, rather than as
|
|
2
|
+
-- replaced: their items weren't made again, and taken up again their steps ask the same review
|
|
3
|
+
-- once more. SQLite cannot change a CHECK in place, so the table is copied, as 0055 did.
|
|
4
|
+
CREATE TABLE external_reviews_canceled_upgrade (
|
|
5
|
+
id TEXT PRIMARY KEY,
|
|
6
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
7
|
+
revision_id TEXT NOT NULL,
|
|
8
|
+
work_id TEXT NOT NULL,
|
|
9
|
+
piece_id TEXT NOT NULL,
|
|
10
|
+
work_key TEXT NOT NULL,
|
|
11
|
+
item_key TEXT NOT NULL,
|
|
12
|
+
stage TEXT NOT NULL CHECK(stage IN ('article','images','narration','thumbnail','shorts')),
|
|
13
|
+
item_fingerprint TEXT NOT NULL,
|
|
14
|
+
review_fingerprint TEXT NOT NULL,
|
|
15
|
+
material TEXT NOT NULL CHECK(json_valid(material)),
|
|
16
|
+
state TEXT NOT NULL CHECK(state IN ('waiting','answered','auto-passed','superseded','canceled')),
|
|
17
|
+
waiting_since TEXT NOT NULL,
|
|
18
|
+
deadline_at TEXT NOT NULL,
|
|
19
|
+
verdict TEXT CHECK(verdict IS NULL OR json_valid(verdict)),
|
|
20
|
+
answered_by TEXT CHECK(answered_by IN ('person','agent')),
|
|
21
|
+
answered_at TEXT,
|
|
22
|
+
UNIQUE(project_id,review_fingerprint)
|
|
23
|
+
);
|
|
24
|
+
INSERT INTO external_reviews_canceled_upgrade (id,project_id,revision_id,work_id,piece_id,work_key,item_key,stage,item_fingerprint,review_fingerprint,material,state,waiting_since,deadline_at,verdict,answered_by,answered_at)
|
|
25
|
+
SELECT id,project_id,revision_id,work_id,piece_id,work_key,item_key,stage,item_fingerprint,review_fingerprint,material,state,waiting_since,deadline_at,verdict,answered_by,answered_at
|
|
26
|
+
FROM external_reviews ORDER BY rowid;
|
|
27
|
+
DROP TABLE external_reviews;
|
|
28
|
+
ALTER TABLE external_reviews_canceled_upgrade RENAME TO external_reviews;
|
|
29
|
+
CREATE INDEX external_reviews_waiting ON external_reviews(state, deadline_at);
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
-- Whether a step is admitted for its project's current version is asked of every stage on each
|
|
2
|
+
-- list, calendar and needs_attention read (`admission/admitted-work.ts`): its reservations are
|
|
3
|
+
-- found by the step, not by a scan of every reservation of every version.
|
|
4
|
+
CREATE INDEX revision_work_reservations_work ON revision_work_reservations(work_id);
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
-- How many times a project was paused, and stopped by a restart, so "While you were away" tells
|
|
2
|
+
-- a pause or a stop that came again from the one it already saw (`web/src/notifications/away.ts`).
|
|
3
|
+
ALTER TABLE project_controls ADD COLUMN pauses INTEGER NOT NULL DEFAULT 0;
|
|
4
|
+
ALTER TABLE project_controls ADD COLUMN stops INTEGER NOT NULL DEFAULT 0;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
-- Each failed step the Notification URL was told of (`notifications/rules.ts` runNotices), by its
|
|
2
|
+
-- identity (`kind@finishedAt`), only while its project still has it: a restart reads it back, so
|
|
3
|
+
-- a failure is told once, and one never told is still told when the run ends.
|
|
4
|
+
CREATE TABLE told_failures (
|
|
5
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
6
|
+
failure_id TEXT NOT NULL,
|
|
7
|
+
PRIMARY KEY(project_id, failure_id)
|
|
8
|
+
);
|
package/dist/kernel/pipeline.js
CHANGED
|
@@ -28,6 +28,12 @@ export const stageStates = [
|
|
|
28
28
|
"provided",
|
|
29
29
|
"skipped",
|
|
30
30
|
];
|
|
31
|
+
// A failed step's identity: its step and when it failed, so one that fails again after Try again
|
|
32
|
+
// is another failure (`admission/repo.ts` stepFailuresByProject, `notifications/rules.ts`
|
|
33
|
+
// failuresOf).
|
|
34
|
+
export function failureId(kind, finishedAt) {
|
|
35
|
+
return `${kind}@${finishedAt ?? ""}`;
|
|
36
|
+
}
|
|
31
37
|
// A persisted pause overrides the stage-derived status. A completed run need not
|
|
32
38
|
// include a video: all selected stages being satisfied is enough. `partial` is a run whose
|
|
33
39
|
// main output was made while another step failed (the video is there, its thumbnail is
|
|
@@ -38,17 +38,24 @@ export function releases(kind, dependency, state) {
|
|
|
38
38
|
const headline = ["video", "audio", "article"];
|
|
39
39
|
// Stage state is derived; an explicit persisted pause takes precedence while calls
|
|
40
40
|
// drain. Completed independent images do not imply that a run was canceled. A stage
|
|
41
|
-
// waiting out a rate limit is still part of a run in progress
|
|
41
|
+
// waiting out a rate limit is still part of a run in progress, and so is one taken up again
|
|
42
|
+
// after a cancel: the run reads by what that stage waits on until it is done.
|
|
42
43
|
export function derive(stages, paused = false) {
|
|
43
44
|
if (paused)
|
|
44
45
|
return "paused";
|
|
45
46
|
if (stages.some((stage) => stage.state === "running" || waiting(stage))) {
|
|
46
47
|
return "running";
|
|
47
48
|
}
|
|
48
|
-
if (stages.some((stage) => stage.state === "canceled")
|
|
49
|
+
if (stages.some((stage) => stage.state === "canceled") &&
|
|
50
|
+
!stages.some((stage) => readmitted(stage, stages))) {
|
|
49
51
|
return "canceled";
|
|
50
52
|
}
|
|
51
53
|
if (stages.some((stage) => stage.state === "failed")) {
|
|
54
|
+
// A failed step ends the run only once nothing else can go on: while a step it doesn't
|
|
55
|
+
// depend on waits for the agent or the person, the run reads by that wait (`reviews/holds.ts`
|
|
56
|
+
// says which), the failed step still shown and listed as failed.
|
|
57
|
+
if (waitsForAnswer(stages))
|
|
58
|
+
return "pending";
|
|
52
59
|
return headlineMade(stages) ? "partial" : "failed";
|
|
53
60
|
}
|
|
54
61
|
if (stages.length > 0 && stages.every((stage) => satisfied(stage.state))) {
|
|
@@ -57,6 +64,34 @@ export function derive(stages, paused = false) {
|
|
|
57
64
|
// Includes a run created but not yet claimed and unfinished work after restart.
|
|
58
65
|
return "pending";
|
|
59
66
|
}
|
|
67
|
+
// A stage taken up again after a Cancel, by Retry, Resume or Continue the run: beside a canceled
|
|
68
|
+
// stage, it waits to run with its work admitted again, or that work failed. Cancel cancels every
|
|
69
|
+
// unfinished step, so failed work beside a canceled stage was taken up after it. While one is,
|
|
70
|
+
// the run reads by what it does: going on, or failed.
|
|
71
|
+
export function readmitted(stage, stages) {
|
|
72
|
+
return ((stage.state === "pending" || stage.state === "failed") &&
|
|
73
|
+
stage.admitted === true &&
|
|
74
|
+
stages.some((other) => other.state === "canceled"));
|
|
75
|
+
}
|
|
76
|
+
// Whether a step of the run waits for the agent, the person or its day: parked on a review its
|
|
77
|
+
// own inputs let it reach, so it goes on once answered.
|
|
78
|
+
export function waitsForAnswer(stages) {
|
|
79
|
+
return stages.some((stage) => movesOnWhenAnswered(stage, stages));
|
|
80
|
+
}
|
|
81
|
+
// A stage parked on a review that its own inputs let it reach, as the runner reads them for the
|
|
82
|
+
// project's sources: answered, it goes on.
|
|
83
|
+
function movesOnWhenAnswered(stage, stages) {
|
|
84
|
+
const sourceOf = (kind) => stages.find((other) => other.kind === kind)?.source ?? "generate";
|
|
85
|
+
return (stage.state === "pending" &&
|
|
86
|
+
stage.parked === true &&
|
|
87
|
+
dependenciesOf(stage.kind, {
|
|
88
|
+
thumbnail: sourceOf("thumbnail"),
|
|
89
|
+
video: sourceOf("video"),
|
|
90
|
+
}).every((kind) => {
|
|
91
|
+
const dependency = stages.find((other) => other.kind === kind);
|
|
92
|
+
return dependency === undefined || releases(stage.kind, kind, dependency.state);
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
60
95
|
function waiting(stage) {
|
|
61
96
|
return stage.state === "pending" && stage.retryAt !== undefined && stage.retryAt !== null;
|
|
62
97
|
}
|
|
@@ -3,6 +3,9 @@ import { derive, deps as graph, releases } from "./graph.js";
|
|
|
3
3
|
import { progressGate } from "./progress.js";
|
|
4
4
|
import { faultOf } from "./retry-policy.js";
|
|
5
5
|
const pauseReason = new Error("paused by user");
|
|
6
|
+
// A shutdown stops a stage without the person asking: it concludes pending and held, as a
|
|
7
|
+
// restart leaves it, for Continue the run to start again - never canceled.
|
|
8
|
+
const shutdownReason = new Error("Slopify is stopping.");
|
|
6
9
|
export function createRunner(deps) {
|
|
7
10
|
const inflight = new Map();
|
|
8
11
|
// ceiling: one entry per project ticked since boot, never evicted - a few dozen bytes
|
|
@@ -126,8 +129,8 @@ export function createRunner(deps) {
|
|
|
126
129
|
if (controller.signal.aborted) {
|
|
127
130
|
// A late rejection from the aborted call is how a stage learns it was canceled, so
|
|
128
131
|
// it is not logged as a fault.
|
|
129
|
-
const
|
|
130
|
-
conclude(stage,
|
|
132
|
+
const held = [pauseReason, shutdownReason].includes(controller.signal.reason);
|
|
133
|
+
conclude(stage, held ? "pending" : "canceled", held ? null : "canceled by user");
|
|
131
134
|
return;
|
|
132
135
|
}
|
|
133
136
|
const fault = faultOf(error);
|
|
@@ -256,7 +259,7 @@ export function createRunner(deps) {
|
|
|
256
259
|
abortAll: async () => {
|
|
257
260
|
shuttingDown = true;
|
|
258
261
|
for (const entry of inflight.values()) {
|
|
259
|
-
entry.controller.abort();
|
|
262
|
+
entry.controller.abort(shutdownReason);
|
|
260
263
|
}
|
|
261
264
|
await settled();
|
|
262
265
|
},
|
package/dist/main.js
CHANGED
|
@@ -41,7 +41,7 @@ import { createProviderQueue } from "./kernel/runner/queue.js";
|
|
|
41
41
|
import { standaloneImage, standaloneLlm } from "./kernel/runner/standalone.js";
|
|
42
42
|
import { readVersion } from "./kernel/version.js";
|
|
43
43
|
import { modelSources } from "./model-catalog.js";
|
|
44
|
-
import {
|
|
44
|
+
import { projectPaused, projectTrashed } from "./slices/admission/repo.js";
|
|
45
45
|
import { createBackupService } from "./slices/backups/service.js";
|
|
46
46
|
import { pumpQueue, queueWaiting } from "./slices/batch/index.js";
|
|
47
47
|
import { settleInterruptedCastImages } from "./slices/channels/cast-images.js";
|
|
@@ -50,10 +50,9 @@ import { checkpointDecisionForWork, recoverCheckpointWork, settleReleasedCheckpo
|
|
|
50
50
|
import { createEpisodeMemoryWatcher, } from "./slices/episodes/summarize.js";
|
|
51
51
|
import { resolveFont } from "./slices/fonts/index.js";
|
|
52
52
|
import { decodePeaks } from "./slices/narration/peaks.js";
|
|
53
|
-
import {
|
|
54
|
-
import { projectLink } from "./slices/notifications/rules.js";
|
|
53
|
+
import { projectLink } from "./slices/notifications/address.js";
|
|
55
54
|
import { createNotificationSender } from "./slices/notifications/send.js";
|
|
56
|
-
import {
|
|
55
|
+
import { createServerNotifier } from "./slices/notifications/server.js";
|
|
57
56
|
import { seedSamples } from "./slices/onboarding/sample.js";
|
|
58
57
|
import { templateById } from "./slices/project-templates/repo.js";
|
|
59
58
|
import { createNarrationRetries } from "./slices/rebuild/narration-retry.js";
|
|
@@ -61,9 +60,11 @@ import { recoverProject } from "./slices/rebuild/recovery.js";
|
|
|
61
60
|
import { claimWork, finishWork, maySubmit } from "./slices/rebuild/repo.js";
|
|
62
61
|
import { waitToRetry, wakeRetries } from "./slices/rebuild/retry.js";
|
|
63
62
|
import { createReviewRedos, reviewHold } from "./slices/rebuild/review-redo.js";
|
|
63
|
+
import { agentReviewHold } from "./slices/rebuild/runtime-agent-review.js";
|
|
64
64
|
import { materializeAdmittedWork } from "./slices/rebuild/runtime-materialize.js";
|
|
65
65
|
import { runRevisionInvocation } from "./slices/rebuild/runtime-run.js";
|
|
66
66
|
import { executionStandings, invocationReady, projectStandings, recordWorkProgress, } from "./slices/rebuild/runtime-store.js";
|
|
67
|
+
import { releaseDueReviews } from "./slices/reviews/external-release.js";
|
|
67
68
|
import { currentRevisionId } from "./slices/revisions/repo.js";
|
|
68
69
|
import { createLimitGate, resumeAfterRestart } from "./slices/run-cost/limits.js";
|
|
69
70
|
import { createStandaloneMeter, createUsageMeter } from "./slices/run-cost/meter.js";
|
|
@@ -210,14 +211,7 @@ export async function boot(config, options = {}) {
|
|
|
210
211
|
});
|
|
211
212
|
const eventDb = db;
|
|
212
213
|
const sendNotification = createNotificationSender(globalThis.fetch);
|
|
213
|
-
const notifier =
|
|
214
|
-
url: () => readNotificationUrl(eventDb),
|
|
215
|
-
subject: (projectId) => {
|
|
216
|
-
const project = projectById(eventDb, projectId);
|
|
217
|
-
return project === undefined
|
|
218
|
-
? undefined
|
|
219
|
-
: { title: project.title, makesVideo: project.config.sources.video !== "off" };
|
|
220
|
-
},
|
|
214
|
+
const notifier = createServerNotifier(eventDb, {
|
|
221
215
|
// Read when a notification goes out, by which time the server is listening.
|
|
222
216
|
link: (projectId) => projectLink(config.host, listeningPort, projectId),
|
|
223
217
|
send: sendNotification,
|
|
@@ -527,6 +521,7 @@ export async function boot(config, options = {}) {
|
|
|
527
521
|
...(dockerState === undefined ? {} : { installationPending: () => updater.locked() }),
|
|
528
522
|
db,
|
|
529
523
|
paths,
|
|
524
|
+
host: config.host,
|
|
530
525
|
hub,
|
|
531
526
|
runner,
|
|
532
527
|
updater,
|
|
@@ -569,6 +564,23 @@ export async function boot(config, options = {}) {
|
|
|
569
564
|
...(hostCli === undefined ? { cliLogin: readHostLogin } : {}),
|
|
570
565
|
});
|
|
571
566
|
const server = await listen(app, config, log);
|
|
567
|
+
// An agent's review past its two hours passes without review; the deadline is a row, so
|
|
568
|
+
// the ones a restart left waiting are settled here first.
|
|
569
|
+
const releaseReviews = () => {
|
|
570
|
+
try {
|
|
571
|
+
releaseDueReviews({
|
|
572
|
+
db: updateDb,
|
|
573
|
+
now: () => clock.now(),
|
|
574
|
+
emit: hub.emit,
|
|
575
|
+
tick: runner.tick,
|
|
576
|
+
log,
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
catch (error) {
|
|
580
|
+
log.write("error", "review.deadline", { detail: causedBy(error) });
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
releaseReviews();
|
|
572
584
|
const queueTimer = setInterval(() => {
|
|
573
585
|
const release = updater.beginMutation();
|
|
574
586
|
if (!release)
|
|
@@ -594,6 +606,7 @@ export async function boot(config, options = {}) {
|
|
|
594
606
|
return;
|
|
595
607
|
try {
|
|
596
608
|
wakeRetries(updateDb, clock, runner);
|
|
609
|
+
releaseReviews();
|
|
597
610
|
}
|
|
598
611
|
catch (error) {
|
|
599
612
|
log.write("error", "stage.retry", { detail: causedBy(error) });
|
|
@@ -790,7 +803,10 @@ export function wireRunner({ onFinished, audioPreviews, db, paths, clock, ids, l
|
|
|
790
803
|
// A review waiting to send its item back holds the item's dependents like a checkpoint.
|
|
791
804
|
decide: (work) => {
|
|
792
805
|
const review = reviewHold(execution, work);
|
|
793
|
-
|
|
806
|
+
if (review.kind === "held")
|
|
807
|
+
return review;
|
|
808
|
+
const agent = agentReviewHold(execution, work);
|
|
809
|
+
return agent.kind === "held" ? agent : checkpointDecisionForWork(execution, work);
|
|
794
810
|
},
|
|
795
811
|
approve: (projectId, checkpointId, identity) => approveCheckpoint(db, {
|
|
796
812
|
...identity,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Slopify 3.18.0
|
|
2
|
+
|
|
3
|
+
Released 11 October 2026.
|
|
4
|
+
|
|
5
|
+
- **AI agents can run Slopify.** A new **Settings → Connections → Agents (MCP)** tab shows the address `http://127.0.0.1:6969/mcp` and the setup line for Claude Code or Codex, with Copy. An agent on this computer can then start, check and fix runs, answer reviews, edit the Library and plan schedules, with the same checks, costs and refusals as the buttons. It needs no key; web pages from other addresses are refused. Agents stop at **Ready to upload** and never publish. What an agent changes is marked **by agent** in the project's history and in notifications, and the tab lists the latest 20 changes. The prompt **attend** walks an agent through everything that needs attention.
|
|
6
|
+
- **External agent reviews.** In a template's Reviews, **Who reviews** can be **External agent (MCP)**. The step then waits for your agent ("Waiting for the agent"), the first verdict wins, and a review nobody answers passes after 2 hours ("Passed without review").
|
|
7
|
+
- **Clearer states.** Every screen uses the same words: Waiting for your review, Waiting for the agent, Prepared, Stopped and Kept as is. A run stopped by a restart reads **Stopped** and is listed under Needs you with **Continue the run**. Projects has one **Running** filter that matches Home's "See all N running".
|
|
8
|
+
- **Fewer, better failure notices.** A failed step is told right away only while something else waits for you, the agent or its day; otherwise you get the end-of-run notice with its chime. A restart no longer repeats a notice or loses one. **While you were away** counts failed steps.
|
|
9
|
+
|
|
10
|
+
## Upgrading from 3.17.0
|
|
11
|
+
|
|
12
|
+
- **Nothing becomes outdated.** The database updates itself to schema 62 on the first start.
|
|
13
|
+
- Agents are off until you connect one; nothing changes for runs you start yourself.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Slopify 3.18.1
|
|
2
|
+
|
|
3
|
+
Released 11 October 2026.
|
|
4
|
+
|
|
5
|
+
- **Your images are kept with your video.** Every image the video shows now sits in the project's `Upload/Images/` folder, in order, and each short's images sit beside that short in `Upload/Shorts/<n> - <title>/`. An older version's images go to `Upload/Images/Earlier/<date>/` instead of `History/`. They used to be under `Working/` and `History/`.
|
|
6
|
+
- **Cleanup no longer deletes images.** **Keep outputs only**, **Free space**, **Delete old versions** and **Clean up everything** keep every image the video and shorts used, from every version. Before, they deleted them as working files, and making them again never gives the same pictures.
|
|
7
|
+
|
|
8
|
+
## Upgrading from 3.18.0
|
|
9
|
+
|
|
10
|
+
- On the first start, finished projects move their images from `Working/` and `History/` into `Upload/` by themselves. Nothing is copied, so this takes no extra space.
|
|
11
|
+
- Images an earlier cleanup already deleted can't be brought back by this update. A backup made before the cleanup still has them; otherwise remaking the images makes new ones.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Slopify 3.18.2
|
|
2
|
+
|
|
3
|
+
Released 11 October 2026.
|
|
4
|
+
|
|
5
|
+
- **Each image is named by its scene.** In a project's Images, every image is titled by its own scene, for example "Explorers scramble along a rock shelf above a fossil sea…", instead of the prompt's opening, which was the same on every image.
|
|
6
|
+
- **The full-size view says the prompt once.** The scene is on the top bar and the whole prompt is under the picture, instead of the whole prompt in both places.
|
|
7
|
+
|
|
8
|
+
## Upgrading from 3.18.1
|
|
9
|
+
|
|
10
|
+
- Nothing becomes outdated; existing images get their titles at once.
|
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"id": "3.18.2",
|
|
4
|
+
"title": "Slopify 3.18.2",
|
|
5
|
+
"version": "3.18.2",
|
|
6
|
+
"date": "2026-10-11"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"id": "3.18.1",
|
|
10
|
+
"title": "Slopify 3.18.1",
|
|
11
|
+
"version": "3.18.1",
|
|
12
|
+
"date": "2026-10-11"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"id": "3.18.0",
|
|
16
|
+
"title": "Slopify 3.18.0",
|
|
17
|
+
"version": "3.18.0",
|
|
18
|
+
"date": "2026-10-11"
|
|
19
|
+
},
|
|
2
20
|
{
|
|
3
21
|
"id": "3.17.0",
|
|
4
22
|
"title": "Slopify 3.17.0",
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// A step `w` a run admitted (`recipe_context`) for its project's current version, of a stage not
|
|
2
|
+
// finished: it still has to run, though a restart, a pause or an agent's review held it, and
|
|
3
|
+
// Continue the run or the agent's answer starts it again. A saved edit's step nobody admitted
|
|
4
|
+
// yet has no `recipe_context`, and a step reserved only by an older version is a leftover.
|
|
5
|
+
export function admittedUnfinished(w) {
|
|
6
|
+
return `${w}.recipe_context IS NOT NULL
|
|
7
|
+
AND EXISTS (SELECT 1 FROM stages s WHERE s.project_id=${w}.project_id AND s.id=${w}.stage_id
|
|
8
|
+
AND s.state NOT IN ('done','provided','skipped'))
|
|
9
|
+
AND EXISTS (SELECT 1 FROM revision_work_reservations r
|
|
10
|
+
JOIN project_heads h ON h.project_id=r.project_id AND h.revision_id=r.revision_id
|
|
11
|
+
WHERE r.work_id=${w}.id)`;
|
|
12
|
+
}
|
|
13
|
+
// Work `w` admitted and waiting to run (`admittedUnfinished`), as Retry, Resume or Continue
|
|
14
|
+
// leave a canceled run's step.
|
|
15
|
+
export function pendingAdmitted(w) {
|
|
16
|
+
return `${w}.state='pending' AND ${admittedUnfinished(w)}`;
|
|
17
|
+
}
|
|
18
|
+
// Whether stage `s` has work admitted (`admittedUnfinished`) in the stage's own state: the
|
|
19
|
+
// fact `derive` reads (`readmitted`) to tell a stage taken up again after a cancel. A finished
|
|
20
|
+
// stage, most of every library, is answered by its own row before any step is looked up.
|
|
21
|
+
export function admittedInItsState(s) {
|
|
22
|
+
return `(${s}.state NOT IN ('done','provided','skipped')
|
|
23
|
+
AND EXISTS (SELECT 1 FROM revision_work w WHERE w.project_id=${s}.project_id AND w.stage_id=${s}.id
|
|
24
|
+
AND w.state=${s}.state AND ${admittedUnfinished("w")}))`;
|
|
25
|
+
}
|
|
26
|
+
// The same fact for a project's work, as `${kind}:${state}` of each step admitted, for a caller
|
|
27
|
+
// that derives stage states from the work itself (`rebuild/runtime-store.ts`).
|
|
28
|
+
export function admittedStates(db, projectId) {
|
|
29
|
+
return new Set(db
|
|
30
|
+
.prepare(`SELECT DISTINCT w.kind, w.state FROM revision_work w WHERE w.project_id=? AND ${admittedUnfinished("w")}`)
|
|
31
|
+
.all(projectId)
|
|
32
|
+
.map((row) => `${String(row.kind)}:${String(row.state)}`));
|
|
33
|
+
}
|
|
34
|
+
// Whether stage `s` is parked on a review someone answers, so it can still move on: work of it
|
|
35
|
+
// waits for an External agent's review, or a checkpoint of the current version on its stage is
|
|
36
|
+
// not approved yet (the person's, or a schedule's own hold for its day). `derive` reads it
|
|
37
|
+
// beside a failed stage.
|
|
38
|
+
export function parkedOnReview(s) {
|
|
39
|
+
return `(EXISTS (SELECT 1 FROM revision_work w JOIN external_reviews e ON e.work_id=w.id
|
|
40
|
+
WHERE w.project_id=${s}.project_id AND w.stage_id=${s}.id AND e.state='waiting'
|
|
41
|
+
AND ${pendingAdmitted("w")})
|
|
42
|
+
OR EXISTS (SELECT 1 FROM review_checkpoints c
|
|
43
|
+
JOIN project_heads h ON h.project_id=c.project_id AND h.revision_id=c.revision_id
|
|
44
|
+
WHERE c.project_id=${s}.project_id AND c.stage=${s}.kind
|
|
45
|
+
AND c.state IN ('held','pending-review') AND c.approved_at IS NULL))`;
|
|
46
|
+
}
|
|
47
|
+
// The same fact for each stage of a project, by kind, for the runner's own standings.
|
|
48
|
+
export function parkedKinds(db, projectId) {
|
|
49
|
+
return new Set(db
|
|
50
|
+
.prepare(`SELECT s.kind FROM stages s WHERE s.project_id=? AND ${parkedOnReview("s")}`)
|
|
51
|
+
.all(projectId)
|
|
52
|
+
.map((row) => String(row.kind)));
|
|
53
|
+
}
|
|
@@ -13,6 +13,8 @@ const headRow = z.object({
|
|
|
13
13
|
created_at: z.string(),
|
|
14
14
|
updated_at: z.string(),
|
|
15
15
|
paused: z.number().default(0),
|
|
16
|
+
pauses: z.number(),
|
|
17
|
+
stops: z.number(),
|
|
16
18
|
sources: z.string(),
|
|
17
19
|
article_prompt: z.string().nullable(),
|
|
18
20
|
book: z.string().nullable(),
|
|
@@ -24,6 +26,7 @@ const listingConfigSchema = z.object({
|
|
|
24
26
|
});
|
|
25
27
|
const headSelect = `SELECT projects.id, projects.title, projects.format, projects.created_at,
|
|
26
28
|
projects.updated_at, COALESCE(project_controls.paused, 0) AS paused,
|
|
29
|
+
COALESCE(project_controls.pauses, 0) AS pauses, COALESCE(project_controls.stops, 0) AS stops,
|
|
27
30
|
json_extract(projects.config, '$.sources') AS sources,
|
|
28
31
|
json_extract(projects.config, '$.articlePrompt') AS article_prompt,
|
|
29
32
|
json_extract(projects.config, '$.voices.book') AS book
|
|
@@ -42,6 +45,8 @@ export function listProjectHeads(db) {
|
|
|
42
45
|
createdAt: row.created_at,
|
|
43
46
|
updatedAt: row.updated_at,
|
|
44
47
|
paused: row.paused === 1,
|
|
48
|
+
...(row.paused === 1 ? { pauseCount: row.pauses } : {}),
|
|
49
|
+
...(row.stops > 0 ? { stopCount: row.stops } : {}),
|
|
45
50
|
};
|
|
46
51
|
});
|
|
47
52
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { stageKinds, stageStates } from "../../kernel/pipeline.js";
|
|
2
|
+
import { failureId, stageKinds, stageStates } from "../../kernel/pipeline.js";
|
|
3
|
+
import { admittedInItsState, parkedOnReview } from "./admitted-work.js";
|
|
3
4
|
import { formats, stageSources } from "./model.js";
|
|
4
5
|
import { runConfigSchema } from "./schema.js";
|
|
5
6
|
export { runConfigSchema, runDraftSchema } from "./schema.js";
|
|
@@ -18,6 +19,9 @@ const standingRow = z.object({
|
|
|
18
19
|
state: z.enum(stageStates),
|
|
19
20
|
progress_current: z.number().nullable(),
|
|
20
21
|
progress_total: z.number().nullable(),
|
|
22
|
+
source: z.enum(stageSources),
|
|
23
|
+
admitted: z.number(),
|
|
24
|
+
parked: z.number(),
|
|
21
25
|
});
|
|
22
26
|
const stageRow = z.object({
|
|
23
27
|
id: z.string(),
|
|
@@ -33,21 +37,26 @@ const stageRow = z.object({
|
|
|
33
37
|
finished_at: z.string().nullable(),
|
|
34
38
|
failure_kind: z.string().nullable().optional(),
|
|
35
39
|
retry_at: z.string().nullable().optional(),
|
|
40
|
+
admitted: z.number(),
|
|
41
|
+
parked: z.number(),
|
|
36
42
|
});
|
|
37
43
|
export function insertProject(db, project) {
|
|
38
44
|
db.prepare("INSERT INTO projects (id, title, format, config, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)").run(project.id, project.title, project.format, JSON.stringify(project.config), project.createdAt, project.updatedAt);
|
|
39
45
|
if (project.paused === true)
|
|
40
46
|
setProjectPaused(db, project.id, true, project.updatedAt);
|
|
41
47
|
}
|
|
42
|
-
export function insertStage(db, stage) {
|
|
43
|
-
db.prepare("INSERT INTO stages (id, project_id, kind, source, state, attempt_count) VALUES (?, ?, ?, ?, ?, ?)").run(stage.id, stage.projectId, stage.kind, stage.source, stage.state, stage.attemptCount);
|
|
44
|
-
}
|
|
45
48
|
// A project in the trash (`project_trash`, Settings → Trash) is not found by any of these
|
|
46
49
|
// reads: it is listed nowhere, opens nowhere and takes no action until it is restored.
|
|
47
50
|
// `slices/trash` and `slices/storage/delete-project.ts` read the table directly.
|
|
48
51
|
export function liveProject(alias = "projects", column = "id") {
|
|
49
52
|
return `NOT EXISTS (SELECT 1 FROM project_trash WHERE project_trash.project_id = ${alias}.${column})`;
|
|
50
53
|
}
|
|
54
|
+
// The clause and its parameter that limit a query to one project, when `projectId` is given.
|
|
55
|
+
export function oneProject(alias, projectId) {
|
|
56
|
+
return projectId === undefined
|
|
57
|
+
? { sql: "", params: [] }
|
|
58
|
+
: { sql: ` AND ${alias}.project_id=?`, params: [projectId] };
|
|
59
|
+
}
|
|
51
60
|
export function projectTrashed(db, id) {
|
|
52
61
|
return db.prepare("SELECT 1 FROM project_trash WHERE project_id = ?").get(id) !== undefined;
|
|
53
62
|
}
|
|
@@ -76,12 +85,12 @@ export function listProjects(db) {
|
|
|
76
85
|
// What 07 Projects needs of every project's stages and no more: the state each one is in
|
|
77
86
|
// and how far through itself a running one is. One statement for the whole list rather
|
|
78
87
|
// than `stagesOf` per row, and five columns rather than the whole stage table, because
|
|
79
|
-
// the screen shows a status word and a meter and reads nothing else from a stage.
|
|
80
|
-
|
|
88
|
+
// the screen shows a status word and a meter and reads nothing else from a stage. One project's
|
|
89
|
+
// alone when `projectId` is given.
|
|
90
|
+
export function stageStandingsByProject(db, projectId) {
|
|
81
91
|
const grouped = new Map();
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
.all();
|
|
92
|
+
const select = `SELECT project_id, kind, state, progress_current, progress_total, source, ${admittedInItsState("stages")} AS admitted, ${parkedOnReview("stages")} AS parked FROM stages${projectId === undefined ? "" : " WHERE project_id=?"} ORDER BY rowid`;
|
|
93
|
+
const rows = projectId === undefined ? db.prepare(select).all() : db.prepare(select).all(projectId);
|
|
85
94
|
for (const row of rows) {
|
|
86
95
|
const parsed = standingRow.parse(row);
|
|
87
96
|
const standing = {
|
|
@@ -89,6 +98,9 @@ export function stageStandingsByProject(db) {
|
|
|
89
98
|
state: parsed.state,
|
|
90
99
|
progressCurrent: parsed.progress_current,
|
|
91
100
|
progressTotal: parsed.progress_total,
|
|
101
|
+
source: parsed.source,
|
|
102
|
+
...(parsed.admitted === 1 ? { admitted: true } : {}),
|
|
103
|
+
...(parsed.parked === 1 ? { parked: true } : {}),
|
|
92
104
|
};
|
|
93
105
|
const seen = grouped.get(parsed.project_id);
|
|
94
106
|
if (seen === undefined) {
|
|
@@ -100,30 +112,32 @@ export function stageStandingsByProject(db) {
|
|
|
100
112
|
}
|
|
101
113
|
return grouped;
|
|
102
114
|
}
|
|
115
|
+
const failedRow = z.object({
|
|
116
|
+
project_id: z.string(),
|
|
117
|
+
kind: z.enum(stageKinds),
|
|
118
|
+
finished_at: z.string().nullable(),
|
|
119
|
+
});
|
|
120
|
+
// Each project's failed steps by their identity (`kernel/pipeline.ts` failureId), in the
|
|
121
|
+
// run's order, so a failure is told apart from one before it.
|
|
122
|
+
export function stepFailuresByProject(db) {
|
|
123
|
+
const failures = new Map();
|
|
124
|
+
for (const row of db
|
|
125
|
+
.prepare("SELECT project_id, kind, finished_at FROM stages WHERE state='failed' ORDER BY rowid")
|
|
126
|
+
.all()) {
|
|
127
|
+
const failed = failedRow.parse(row);
|
|
128
|
+
failures.set(failed.project_id, [
|
|
129
|
+
...(failures.get(failed.project_id) ?? []),
|
|
130
|
+
failureId(failed.kind, failed.finished_at),
|
|
131
|
+
]);
|
|
132
|
+
}
|
|
133
|
+
return failures;
|
|
134
|
+
}
|
|
103
135
|
export function stagesOf(db, projectId) {
|
|
104
136
|
return db
|
|
105
|
-
.prepare(
|
|
137
|
+
.prepare(`SELECT *, ${admittedInItsState("stages")} AS admitted, ${parkedOnReview("stages")} AS parked FROM stages WHERE project_id = ? ORDER BY rowid`)
|
|
106
138
|
.all(projectId)
|
|
107
139
|
.map((row) => toStage(stageRow.parse(row)));
|
|
108
140
|
}
|
|
109
|
-
// The runner's claim: one statement, so two callers cannot both find the stage pending.
|
|
110
|
-
export function claimStage(db, stageId, at) {
|
|
111
|
-
const result = db
|
|
112
|
-
.prepare("UPDATE stages SET state = 'running', started_at = ?, finished_at = NULL, failure_reason = NULL WHERE id = ? AND state = 'pending' AND NOT EXISTS (SELECT 1 FROM project_controls WHERE project_id = stages.project_id AND paused = 1)")
|
|
113
|
-
.run(at, stageId);
|
|
114
|
-
return Number(result.changes) === 1;
|
|
115
|
-
}
|
|
116
|
-
export function finishStage(db, stageId, state, failureReason, at) {
|
|
117
|
-
db.prepare("UPDATE stages SET state = ?, failure_reason = ?, retry_at = NULL, finished_at = ? WHERE id = ?").run(state, failureReason, state === "pending" ? null : at, stageId);
|
|
118
|
-
}
|
|
119
|
-
// A stage put back to `pending` by a re-run, a cascade or a retry starts again from a clean row
|
|
120
|
-
// - no error text, no progress from the run before it, and a fresh attempt budget.
|
|
121
|
-
export function resetStage(db, stageId) {
|
|
122
|
-
db.prepare("UPDATE stages SET state = 'pending', failure_reason = NULL, failure_kind = NULL, retry_at = NULL, attempt_count = 0, progress_current = NULL, progress_total = NULL, started_at = NULL, finished_at = NULL WHERE id = ?").run(stageId);
|
|
123
|
-
}
|
|
124
|
-
export function setStageProgress(db, stageId, current, total) {
|
|
125
|
-
db.prepare("UPDATE stages SET progress_current = ?, progress_total = ? WHERE id = ?").run(current, total, stageId);
|
|
126
|
-
}
|
|
127
141
|
function toProject(row) {
|
|
128
142
|
return {
|
|
129
143
|
id: row.id,
|
|
@@ -141,7 +155,9 @@ export function projectPaused(db, projectId) {
|
|
|
141
155
|
?.paused === 1);
|
|
142
156
|
}
|
|
143
157
|
export function setProjectPaused(db, projectId, paused, at) {
|
|
144
|
-
db.prepare(
|
|
158
|
+
db.prepare(`INSERT INTO project_controls (project_id, paused, pauses) VALUES (?1, ?2, ?2)
|
|
159
|
+
ON CONFLICT(project_id) DO UPDATE SET pauses = pauses + (excluded.paused = 1 AND paused = 0),
|
|
160
|
+
paused = excluded.paused`).run(projectId, paused ? 1 : 0);
|
|
145
161
|
db.prepare("UPDATE projects SET updated_at = ? WHERE id = ?").run(at, projectId);
|
|
146
162
|
}
|
|
147
163
|
export function updateProjectConfig(db, projectId, config, at) {
|
|
@@ -171,5 +187,7 @@ function toStage(row) {
|
|
|
171
187
|
? {}
|
|
172
188
|
: { failureKind: row.failure_kind }),
|
|
173
189
|
...(row.retry_at === null || row.retry_at === undefined ? {} : { retryAt: row.retry_at }),
|
|
190
|
+
...(row.admitted === 1 ? { admitted: true } : {}),
|
|
191
|
+
...(row.parked === 1 ? { parked: true } : {}),
|
|
174
192
|
};
|
|
175
193
|
}
|
|
@@ -172,6 +172,7 @@ export const runDraftSchema = z.object({
|
|
|
172
172
|
provider: z.string(),
|
|
173
173
|
model: z.string(),
|
|
174
174
|
thinking: z.enum(thinkingModes).optional(),
|
|
175
|
+
reviewer: z.literal("agent").optional(),
|
|
175
176
|
retries: z.number().optional(),
|
|
176
177
|
stages: z.partialRecord(z.enum(reviewStages), z.object({ mode: z.enum(reviewModes), prompt: z.string().optional() })),
|
|
177
178
|
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function insertStage(db, stage) {
|
|
2
|
+
db.prepare("INSERT INTO stages (id, project_id, kind, source, state, attempt_count) VALUES (?, ?, ?, ?, ?, ?)").run(stage.id, stage.projectId, stage.kind, stage.source, stage.state, stage.attemptCount);
|
|
3
|
+
}
|
|
4
|
+
// The runner's claim: one statement, so two callers cannot both find the stage pending.
|
|
5
|
+
export function claimStage(db, stageId, at) {
|
|
6
|
+
const result = db
|
|
7
|
+
.prepare("UPDATE stages SET state = 'running', started_at = ?, finished_at = NULL, failure_reason = NULL WHERE id = ? AND state = 'pending' AND NOT EXISTS (SELECT 1 FROM project_controls WHERE project_id = stages.project_id AND paused = 1)")
|
|
8
|
+
.run(at, stageId);
|
|
9
|
+
return Number(result.changes) === 1;
|
|
10
|
+
}
|
|
11
|
+
export function finishStage(db, stageId, state, failureReason, at) {
|
|
12
|
+
db.prepare("UPDATE stages SET state = ?, failure_reason = ?, retry_at = NULL, finished_at = ? WHERE id = ?").run(state, failureReason, state === "pending" ? null : at, stageId);
|
|
13
|
+
}
|
|
14
|
+
// A stage put back to `pending` by a re-run, a cascade or a retry starts again from a clean row
|
|
15
|
+
// - no error text, no progress from the run before it, and a fresh attempt budget.
|
|
16
|
+
export function resetStage(db, stageId) {
|
|
17
|
+
db.prepare("UPDATE stages SET state = 'pending', failure_reason = NULL, failure_kind = NULL, retry_at = NULL, attempt_count = 0, progress_current = NULL, progress_total = NULL, started_at = NULL, finished_at = NULL WHERE id = ?").run(stageId);
|
|
18
|
+
}
|
|
19
|
+
export function setStageProgress(db, stageId, current, total) {
|
|
20
|
+
db.prepare("UPDATE stages SET progress_current = ?, progress_total = ? WHERE id = ?").run(current, total, stageId);
|
|
21
|
+
}
|
|
@@ -15,8 +15,9 @@ import { attachStagedFile, storeText } from "../storage/staging.js";
|
|
|
15
15
|
import { releaseStagedFile } from "../storage/staging-refs.js";
|
|
16
16
|
import { usesAmbientBed } from "../video/ambient-bed.js";
|
|
17
17
|
import { sourceOf, } from "./model.js";
|
|
18
|
-
import { insertProject
|
|
18
|
+
import { insertProject } from "./repo.js";
|
|
19
19
|
import { usesPronunciationGlossary, usesReference } from "./rules.js";
|
|
20
|
+
import { insertStage } from "./stage-writes.js";
|
|
20
21
|
import { detectSlots, render } from "./substitute.js";
|
|
21
22
|
// Provide → `provided` with its output attached; Off → `skipped`;
|
|
22
23
|
// everything else → `pending`.
|