@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.
Files changed (143) hide show
  1. package/dist/edge/http/actions.js +2 -2
  2. package/dist/edge/http/actor.js +10 -0
  3. package/dist/edge/http/app.js +21 -0
  4. package/dist/edge/http/checkpoints.js +14 -6
  5. package/dist/edge/http/external-reviews.js +95 -0
  6. package/dist/edge/http/projects.js +36 -18
  7. package/dist/edge/http/reviews.js +53 -18
  8. package/dist/edge/http/revisions.js +5 -1
  9. package/dist/edge/http/settings.js +9 -1
  10. package/dist/edge/http/starts.js +41 -0
  11. package/dist/edge/http/studio.js +5 -130
  12. package/dist/edge/mcp/attribution.js +38 -0
  13. package/dist/edge/mcp/call-route.js +203 -0
  14. package/dist/edge/mcp/excluded-routes.js +95 -0
  15. package/dist/edge/mcp/file-tools.js +141 -0
  16. package/dist/edge/mcp/http.js +79 -0
  17. package/dist/edge/mcp/instructions.js +17 -0
  18. package/dist/edge/mcp/library-tools.js +143 -0
  19. package/dist/edge/mcp/remake-tools.js +90 -0
  20. package/dist/edge/mcp/review-tools.js +64 -0
  21. package/dist/edge/mcp/route-tool.js +61 -0
  22. package/dist/edge/mcp/server.js +48 -0
  23. package/dist/edge/mcp/state-shapes.js +60 -0
  24. package/dist/edge/mcp/tools-channels.js +153 -0
  25. package/dist/edge/mcp/tools-library.js +151 -0
  26. package/dist/edge/mcp/tools-planning.js +137 -0
  27. package/dist/edge/mcp/tools-projects.js +167 -0
  28. package/dist/edge/mcp/tools-publishing.js +148 -0
  29. package/dist/edge/mcp/tools-runs.js +94 -0
  30. package/dist/edge/mcp/tools-schedules.js +143 -0
  31. package/dist/edge/mcp/tools-setup.js +234 -0
  32. package/dist/edge/mcp/tools-storage.js +99 -0
  33. package/dist/edge/mcp/tools.js +21 -0
  34. package/dist/extension/slopify-studio-chrome.zip +0 -0
  35. package/dist/extension/slopify-studio-firefox.zip +0 -0
  36. package/dist/kernel/db/migrations/0055-external-reviews.sql +56 -0
  37. package/dist/kernel/db/migrations/0056-agent-actions.sql +20 -0
  38. package/dist/kernel/db/migrations/0057-agent-action-once.sql +5 -0
  39. package/dist/kernel/db/migrations/0058-revision-work-admission.sql +3 -0
  40. package/dist/kernel/db/migrations/0059-external-review-canceled.sql +29 -0
  41. package/dist/kernel/db/migrations/0060-reservation-work.sql +4 -0
  42. package/dist/kernel/db/migrations/0061-project-control-counts.sql +4 -0
  43. package/dist/kernel/db/migrations/0062-told-failures.sql +8 -0
  44. package/dist/kernel/pipeline.js +6 -0
  45. package/dist/kernel/runner/graph.js +37 -2
  46. package/dist/kernel/runner/index.js +6 -3
  47. package/dist/main.js +29 -13
  48. package/dist/patch-notes/3.17.0.md +10 -0
  49. package/dist/patch-notes/3.18.0.md +13 -0
  50. package/dist/patch-notes/index.json +12 -0
  51. package/dist/slices/admission/admitted-work.js +53 -0
  52. package/dist/slices/admission/heads.js +5 -0
  53. package/dist/slices/admission/repo.js +47 -29
  54. package/dist/slices/admission/schema.js +1 -0
  55. package/dist/slices/admission/stage-writes.js +21 -0
  56. package/dist/slices/admission/start.js +2 -1
  57. package/dist/slices/admission/state-words.js +110 -0
  58. package/dist/slices/agents/attention-work.js +233 -0
  59. package/dist/slices/agents/attention.js +168 -0
  60. package/dist/slices/agents/connection.js +15 -0
  61. package/dist/slices/agents/model.js +29 -0
  62. package/dist/slices/agents/repo.js +66 -0
  63. package/dist/slices/cancel/index.js +4 -1
  64. package/dist/slices/control/index.js +2 -1
  65. package/dist/slices/episodes/summarize.js +2 -1
  66. package/dist/slices/estimate/index.js +6 -2
  67. package/dist/slices/images/run.js +2 -1
  68. package/dist/slices/library/transfer.js +69 -0
  69. package/dist/slices/narration/run.js +2 -1
  70. package/dist/slices/notifications/address.js +40 -0
  71. package/dist/slices/notifications/notifier.js +43 -10
  72. package/dist/slices/notifications/rules.js +111 -45
  73. package/dist/slices/notifications/server.js +14 -0
  74. package/dist/slices/notifications/settings.js +1 -1
  75. package/dist/slices/notifications/subject.js +30 -0
  76. package/dist/slices/notifications/told.js +33 -0
  77. package/dist/slices/play-drafts/readiness.js +9 -2
  78. package/dist/slices/play-drafts/schema.js +2 -1
  79. package/dist/slices/rebuild/narration-words.js +76 -0
  80. package/dist/slices/rebuild/recipe-model.js +1 -0
  81. package/dist/slices/rebuild/recipe-reviews.js +44 -23
  82. package/dist/slices/rebuild/recovery-repo.js +0 -20
  83. package/dist/slices/rebuild/recovery-selection.js +14 -3
  84. package/dist/slices/rebuild/recovery.js +5 -2
  85. package/dist/slices/rebuild/remake-outdated.js +27 -0
  86. package/dist/slices/rebuild/repo.js +7 -2
  87. package/dist/slices/rebuild/review-redo.js +12 -0
  88. package/dist/slices/rebuild/runtime-agent-review.js +121 -0
  89. package/dist/slices/rebuild/runtime-export-inputs.js +26 -11
  90. package/dist/slices/rebuild/runtime-review.js +12 -2
  91. package/dist/slices/rebuild/runtime-run.js +11 -2
  92. package/dist/slices/rebuild/runtime-store.js +32 -5
  93. package/dist/slices/reruns/index.js +2 -1
  94. package/dist/slices/research/run.js +2 -1
  95. package/dist/slices/reviews/external-model.js +21 -0
  96. package/dist/slices/reviews/external-release.js +38 -0
  97. package/dist/slices/reviews/external-repo.js +109 -0
  98. package/dist/slices/reviews/external-rows.js +68 -0
  99. package/dist/slices/reviews/external-waiting.js +75 -0
  100. package/dist/slices/reviews/holds.js +144 -0
  101. package/dist/slices/reviews/model.js +14 -0
  102. package/dist/slices/reviews/repo.js +27 -10
  103. package/dist/slices/reviews/rules.js +5 -3
  104. package/dist/slices/reviews/verdict.js +18 -8
  105. package/dist/slices/revisions/repo.js +3 -0
  106. package/dist/slices/revisions/schema.js +1 -0
  107. package/dist/slices/schedules/agenda.js +47 -17
  108. package/dist/slices/schedules/prepare.js +6 -0
  109. package/dist/slices/schedules/schema.js +13 -1
  110. package/dist/slices/storage/consequences.js +27 -0
  111. package/dist/slices/storage/portable.js +3 -0
  112. package/dist/slices/storage/project-files.js +66 -0
  113. package/dist/slices/studio/settings.js +0 -35
  114. package/dist/slices/studio/stats.js +2 -34
  115. package/dist/slices/subtitles/prepare.js +2 -1
  116. package/dist/slices/uploads/repo.js +12 -6
  117. package/dist/slices/uploads/rules.js +5 -0
  118. package/dist/slices/video/write-export.js +1 -1
  119. package/dist/slices/video/write-subtitles.js +1 -1
  120. package/dist/tutorials/Agents-MCP.md +168 -0
  121. package/dist/tutorials/Calendar.md +2 -1
  122. package/dist/tutorials/First-Launch-and-Welcome.md +1 -0
  123. package/dist/tutorials/Home-and-Projects.md +16 -9
  124. package/dist/tutorials/Home.md +1 -0
  125. package/dist/tutorials/Notifications.md +25 -6
  126. package/dist/tutorials/Project-Page.md +8 -2
  127. package/dist/tutorials/Publishing-to-YouTube.md +1 -0
  128. package/dist/tutorials/Recovery-and-Retries.md +17 -3
  129. package/dist/tutorials/Reviews-and-Checkpoints.md +22 -5
  130. package/dist/tutorials/Schedules.md +1 -1
  131. package/dist/tutorials/Settings-Reference.md +15 -0
  132. package/dist/tutorials/Troubleshooting.md +3 -1
  133. package/dist/tutorials/Your-First-Short.md +1 -0
  134. package/dist/tutorials/_Sidebar.md +1 -0
  135. package/dist/web/assets/{index-VbpUYLAp.css → index-BKbZIxAq.css} +1 -1
  136. package/dist/web/assets/index-Drth_MfQ.js +325 -0
  137. package/dist/web/assets/{pdf-C6KhxeKQ.js → pdf-CQFUyijy.js} +1 -1
  138. package/dist/web/index.html +2 -2
  139. package/package.json +1 -1
  140. package/dist/slices/studio/performance.js +0 -83
  141. package/dist/slices/studio/report-stats.js +0 -39
  142. package/dist/slices/studio/report.js +0 -172
  143. 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
+ ];
@@ -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
+ );
@@ -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 paused = controller.signal.reason === pauseReason;
130
- conclude(stage, paused ? "pending" : "canceled", paused ? null : "canceled by user");
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 { projectById, projectPaused, projectTrashed } from "./slices/admission/repo.js";
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 { createRunNotifier } from "./slices/notifications/notifier.js";
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 { readNotificationUrl } from "./slices/notifications/settings.js";
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 = createRunNotifier({
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
- return review.kind === "held" ? review : checkpointDecisionForWork(execution, work);
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
  }