@gentbajko/slopify 3.16.0 → 3.18.0
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 -18
- 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/http/studio.js +5 -130
- 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.17.0.md +10 -0
- package/dist/patch-notes/3.18.0.md +13 -0
- package/dist/patch-notes/index.json +12 -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/consequences.js +27 -0
- package/dist/slices/storage/portable.js +3 -0
- package/dist/slices/storage/project-files.js +66 -0
- package/dist/slices/studio/settings.js +0 -35
- package/dist/slices/studio/stats.js +2 -34
- 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 +8 -2
- 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 +15 -0
- package/dist/tutorials/Troubleshooting.md +3 -1
- package/dist/tutorials/Your-First-Short.md +1 -0
- package/dist/tutorials/_Sidebar.md +1 -0
- package/dist/web/assets/{index-VbpUYLAp.css → index-BKbZIxAq.css} +1 -1
- package/dist/web/assets/index-Drth_MfQ.js +325 -0
- package/dist/web/assets/{pdf-C6KhxeKQ.js → pdf-CQFUyijy.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/slices/studio/performance.js +0 -83
- package/dist/slices/studio/report-stats.js +0 -39
- package/dist/slices/studio/report.js +0 -172
- package/dist/web/assets/index-Dvz8bsiD.js +0 -325
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { formBody } from "./route-tool.js";
|
|
2
|
+
const storage = [
|
|
3
|
+
{
|
|
4
|
+
name: "get_files_folder",
|
|
5
|
+
route: "GET /api/storage/files",
|
|
6
|
+
title: "Read the files folder",
|
|
7
|
+
description: "Where projects, backups and exports are kept.",
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
name: "move_files_folder",
|
|
11
|
+
route: "POST /api/storage/files/move",
|
|
12
|
+
title: "Move the files folder",
|
|
13
|
+
description: "Body: {target: the new folder's full path; it must be empty or new}.",
|
|
14
|
+
confirm: "This moves every project and backup file to the new folder; Slopify refuses a folder that is not empty.",
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
name: "get_storage_usage",
|
|
18
|
+
route: "GET /api/storage",
|
|
19
|
+
title: "Read disk use",
|
|
20
|
+
description: "Settings → Backup & storage: disk used by projects (each with its outputs, working files and what can be freed), staged files and the trash.",
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: "list_trash",
|
|
24
|
+
route: "GET /api/trash",
|
|
25
|
+
title: "Read the trash",
|
|
26
|
+
description: "Deleted projects, prompts, intros and outros, templates and schedules, kept 30 days.",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: "restore_from_trash",
|
|
30
|
+
route: "POST /api/trash/:kind/:id/restore",
|
|
31
|
+
title: "Restore from the trash",
|
|
32
|
+
description: "Puts the item back; a restored project picks up where it was.",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: "restore_from_trash_many",
|
|
36
|
+
route: "POST /api/trash/bulk/restore",
|
|
37
|
+
title: "Restore several from the trash",
|
|
38
|
+
description: "Restore for the ticked rows. Body: {items: [{kind, id}]}.",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: "delete_from_trash",
|
|
42
|
+
route: "DELETE /api/trash/:kind/:id",
|
|
43
|
+
title: "Delete for good",
|
|
44
|
+
description: "Deletes one trashed item for good, like Delete now in the trash.",
|
|
45
|
+
confirm: "The project and every file it produced are removed from disk (a deleted schedule keeps its run history on Schedules). Nothing removed here can be restored. This cannot be undone.",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: "delete_from_trash_many",
|
|
49
|
+
route: "POST /api/trash/bulk/delete",
|
|
50
|
+
title: "Delete several for good",
|
|
51
|
+
description: "Delete now for the ticked rows. Body: {items: [{kind, id}]}.",
|
|
52
|
+
confirm: "The projects and every file they produced are removed from disk (deleted schedules keep their run history on Schedules). Nothing removed here can be restored. This cannot be undone.",
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
const onboarding = [
|
|
56
|
+
{
|
|
57
|
+
name: "get_onboarding",
|
|
58
|
+
route: "GET /api/onboarding",
|
|
59
|
+
title: "Read the welcome state",
|
|
60
|
+
description: "Starter packs and what the welcome offers.",
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: "install_starter_pack",
|
|
64
|
+
route: "POST /api/onboarding/packs/:id",
|
|
65
|
+
title: "Install a starter pack",
|
|
66
|
+
description: "Adds a starter pack's prompts and templates.",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: "make_quick_short",
|
|
70
|
+
route: "POST /api/onboarding/short",
|
|
71
|
+
title: "Make a quick short",
|
|
72
|
+
description: `${formBody("the welcome's Make a short")} It starts a paid run.`,
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: "make_full_video_draft",
|
|
76
|
+
route: "POST /api/onboarding/full-video",
|
|
77
|
+
title: "Draft a full video",
|
|
78
|
+
description: formBody("the welcome's Make a full video"),
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: "get_samples",
|
|
82
|
+
route: "GET /api/onboarding/sample",
|
|
83
|
+
title: "Read the sample projects",
|
|
84
|
+
description: "Which bundled sample projects are in the projects list.",
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: "restore_samples",
|
|
88
|
+
route: "POST /api/onboarding/sample/restore",
|
|
89
|
+
title: "Restore the samples",
|
|
90
|
+
description: "Puts the bundled samples back as they shipped.",
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: "copy_sample",
|
|
94
|
+
route: "POST /api/onboarding/sample/copy",
|
|
95
|
+
title: "Copy a sample",
|
|
96
|
+
description: "Makes an editable copy of a sample, like Make my own copy.",
|
|
97
|
+
},
|
|
98
|
+
];
|
|
99
|
+
export const storageTools = [...storage, ...onboarding];
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { channelTools } from "./tools-channels.js";
|
|
2
|
+
import { libraryTools } from "./tools-library.js";
|
|
3
|
+
import { planningTools } from "./tools-planning.js";
|
|
4
|
+
import { projectTools } from "./tools-projects.js";
|
|
5
|
+
import { publishingTools } from "./tools-publishing.js";
|
|
6
|
+
import { runTools } from "./tools-runs.js";
|
|
7
|
+
import { scheduleTools } from "./tools-schedules.js";
|
|
8
|
+
import { setupTools } from "./tools-setup.js";
|
|
9
|
+
import { storageTools } from "./tools-storage.js";
|
|
10
|
+
// Every API route an agent may call, as a tool; `excluded-routes.ts` names the rest and why.
|
|
11
|
+
export const routeTools = [
|
|
12
|
+
...projectTools,
|
|
13
|
+
...publishingTools,
|
|
14
|
+
...runTools,
|
|
15
|
+
...planningTools,
|
|
16
|
+
...scheduleTools,
|
|
17
|
+
...channelTools,
|
|
18
|
+
...libraryTools,
|
|
19
|
+
...setupTools,
|
|
20
|
+
...storageTools,
|
|
21
|
+
];
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
-- A review an external agent answers through MCP: what the built-in reviewer would be shown,
|
|
2
|
+
-- kept while it waits (picture files by their stored path, found again where they are when
|
|
3
|
+
-- read); the agent's verdict, or its passing at the deadline. One row per review step
|
|
4
|
+
-- fingerprint, like review_verdicts.
|
|
5
|
+
CREATE TABLE external_reviews (
|
|
6
|
+
id TEXT PRIMARY KEY,
|
|
7
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
8
|
+
revision_id TEXT NOT NULL,
|
|
9
|
+
work_id TEXT NOT NULL,
|
|
10
|
+
piece_id TEXT NOT NULL,
|
|
11
|
+
work_key TEXT NOT NULL,
|
|
12
|
+
item_key TEXT NOT NULL,
|
|
13
|
+
stage TEXT NOT NULL CHECK(stage IN ('article','images','narration','thumbnail','shorts')),
|
|
14
|
+
item_fingerprint TEXT NOT NULL,
|
|
15
|
+
review_fingerprint TEXT NOT NULL,
|
|
16
|
+
material TEXT NOT NULL CHECK(json_valid(material)),
|
|
17
|
+
state TEXT NOT NULL CHECK(state IN ('waiting','answered','auto-passed','superseded')),
|
|
18
|
+
waiting_since TEXT NOT NULL,
|
|
19
|
+
deadline_at TEXT NOT NULL,
|
|
20
|
+
verdict TEXT CHECK(verdict IS NULL OR json_valid(verdict)),
|
|
21
|
+
answered_by TEXT CHECK(answered_by IN ('person','agent')),
|
|
22
|
+
answered_at TEXT,
|
|
23
|
+
UNIQUE(project_id,review_fingerprint)
|
|
24
|
+
);
|
|
25
|
+
CREATE INDEX external_reviews_waiting ON external_reviews(state, deadline_at);
|
|
26
|
+
-- Verdicts say who gave them and who acted on them, and a review nobody answered in time is
|
|
27
|
+
-- its own outcome. SQLite cannot change a CHECK in place, so the table is copied, as 0025 did.
|
|
28
|
+
CREATE TABLE review_verdicts_agent_upgrade (
|
|
29
|
+
id TEXT PRIMARY KEY,
|
|
30
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
31
|
+
revision_id TEXT NOT NULL,
|
|
32
|
+
item_key TEXT NOT NULL,
|
|
33
|
+
stage TEXT NOT NULL CHECK(stage IN ('article','images','narration','thumbnail','shorts')),
|
|
34
|
+
item_fingerprint TEXT NOT NULL,
|
|
35
|
+
review_fingerprint TEXT NOT NULL,
|
|
36
|
+
passed INTEGER NOT NULL CHECK(passed IN (0,1)),
|
|
37
|
+
reasons TEXT NOT NULL,
|
|
38
|
+
outcome TEXT NOT NULL CHECK(outcome IN ('passed','flagged','redo','auto-passed')),
|
|
39
|
+
attempt INTEGER NOT NULL CHECK(attempt >= 1),
|
|
40
|
+
action TEXT CHECK(action IN ('overruled','redone')),
|
|
41
|
+
action_at TEXT,
|
|
42
|
+
action_by TEXT CHECK(action_by IN ('person','agent')),
|
|
43
|
+
redo_state TEXT CHECK(redo_state IN ('pending','started','failed')),
|
|
44
|
+
redo_error TEXT,
|
|
45
|
+
answered_by TEXT NOT NULL DEFAULT 'model' CHECK(answered_by IN ('model','agent','person','nobody')),
|
|
46
|
+
created_at TEXT NOT NULL,
|
|
47
|
+
UNIQUE(project_id,review_fingerprint)
|
|
48
|
+
);
|
|
49
|
+
INSERT INTO review_verdicts_agent_upgrade (id,project_id,revision_id,item_key,stage,item_fingerprint,review_fingerprint,passed,reasons,outcome,attempt,action,action_at,action_by,redo_state,redo_error,answered_by,created_at)
|
|
50
|
+
SELECT id,project_id,revision_id,item_key,stage,item_fingerprint,review_fingerprint,passed,reasons,outcome,attempt,action,action_at,
|
|
51
|
+
CASE WHEN action IS NULL THEN NULL ELSE 'person' END,redo_state,redo_error,'model',created_at
|
|
52
|
+
FROM review_verdicts ORDER BY rowid;
|
|
53
|
+
DROP TABLE review_verdicts;
|
|
54
|
+
ALTER TABLE review_verdicts_agent_upgrade RENAME TO review_verdicts;
|
|
55
|
+
CREATE INDEX review_verdicts_item ON review_verdicts(project_id,item_key);
|
|
56
|
+
CREATE INDEX review_verdicts_redo ON review_verdicts(redo_state);
|
|
@@ -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,10 @@
|
|
|
1
|
+
# Slopify 3.17.0
|
|
2
|
+
|
|
3
|
+
Released 10 October 2026.
|
|
4
|
+
|
|
5
|
+
- **YouTube numbers are gone.** Studio's export only carried daily figures for the few videos its chart showed, so the numbers were incomplete. The **YouTube numbers** tab in Channels is removed. So are the views and CTR on the Projects list and the numbers line under each video in **On YouTube**. The extension no longer exports the Analytics view every day or visits each video's Analytics, and the popup's **Read Studio now** is gone. **Library → A/B results** stays: once a day the extension still reads a running A/B test's result from the long video's Details page. (Extension 1.4.0.)
|
|
6
|
+
|
|
7
|
+
## Upgrading from 3.16.0
|
|
8
|
+
|
|
9
|
+
- **Nothing becomes outdated.**
|
|
10
|
+
- The extension loaded from Slopify's own folder switches to 1.4.0 by itself. The store version updates through the store.
|
|
@@ -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.
|
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"id": "3.18.0",
|
|
4
|
+
"title": "Slopify 3.18.0",
|
|
5
|
+
"version": "3.18.0",
|
|
6
|
+
"date": "2026-10-11"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"id": "3.17.0",
|
|
10
|
+
"title": "Slopify 3.17.0",
|
|
11
|
+
"version": "3.17.0",
|
|
12
|
+
"date": "2026-10-10"
|
|
13
|
+
},
|
|
2
14
|
{
|
|
3
15
|
"id": "3.16.0",
|
|
4
16
|
"title": "Slopify 3.16.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
|
}
|