@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
|
@@ -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`.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { waitsForAnswer } from "../../kernel/runner/graph.js";
|
|
2
|
+
// One word per state, everywhere a state is said: the MCP tools, and the web's lists and pages
|
|
3
|
+
// (`web/src/lib/state-words.ts` projectStateOf, Home's Needs you, the project's next action).
|
|
4
|
+
// Browser-safe.
|
|
5
|
+
// A project that has not started yet is Queued: it waits its turn, not for the person.
|
|
6
|
+
export const projectStateWords = {
|
|
7
|
+
running: "Running",
|
|
8
|
+
paused: "Paused",
|
|
9
|
+
pending: "Queued",
|
|
10
|
+
failed: "Failed",
|
|
11
|
+
partial: "Done with problems",
|
|
12
|
+
done: "Done",
|
|
13
|
+
canceled: "Canceled",
|
|
14
|
+
};
|
|
15
|
+
// A step of a run. A skipped step is one the run's settings turned off.
|
|
16
|
+
export const stageStateWords = {
|
|
17
|
+
pending: "Not started",
|
|
18
|
+
running: "Running",
|
|
19
|
+
done: "Done",
|
|
20
|
+
provided: "Provided",
|
|
21
|
+
failed: "Failed",
|
|
22
|
+
canceled: "Canceled",
|
|
23
|
+
skipped: "Off",
|
|
24
|
+
};
|
|
25
|
+
// What a held run waits for, on a step or on a project: the person's review at a checkpoint,
|
|
26
|
+
// an External agent's review, Continue the run once a restart stopped it, its scheduled day,
|
|
27
|
+
// or nobody once its leftover steps were set aside on purpose.
|
|
28
|
+
export const heldWord = "Waiting for your review";
|
|
29
|
+
export const waitingForAgentWord = "Waiting for the agent";
|
|
30
|
+
export const stoppedWord = "Stopped";
|
|
31
|
+
export const preparedWord = "Prepared";
|
|
32
|
+
export const keptAsIsWord = "Kept as is";
|
|
33
|
+
export const heldStateWords = [
|
|
34
|
+
heldWord,
|
|
35
|
+
waitingForAgentWord,
|
|
36
|
+
stoppedWord,
|
|
37
|
+
preparedWord,
|
|
38
|
+
keptAsIsWord,
|
|
39
|
+
];
|
|
40
|
+
// A run that ran and is held: a waiting project with some of its run done (`progress` is the
|
|
41
|
+
// run's share done, `progressOf`) or a step failed, or one stopped or parked on an agent's review
|
|
42
|
+
// before it got going. One with none done that waits for neither is Queued, waiting its turn.
|
|
43
|
+
export function heldRun(project) {
|
|
44
|
+
return (project.status === "pending" &&
|
|
45
|
+
(project.progress > 0 ||
|
|
46
|
+
project.stepFailed === true ||
|
|
47
|
+
project.stopped === true ||
|
|
48
|
+
project.waitingForAgent === true));
|
|
49
|
+
}
|
|
50
|
+
// Whether the person is the one it waits for (to approve, or to Continue the run): not when it
|
|
51
|
+
// is Kept as is, waits for an agent or for its scheduled day.
|
|
52
|
+
function heldForPerson(project) {
|
|
53
|
+
return project.setAside !== true && project.waitingForAgent !== true && project.prepared !== true;
|
|
54
|
+
}
|
|
55
|
+
export function heldStateWord(project) {
|
|
56
|
+
if (project.setAside === true)
|
|
57
|
+
return keptAsIsWord;
|
|
58
|
+
if (project.waitingForAgent === true)
|
|
59
|
+
return waitingForAgentWord;
|
|
60
|
+
if (project.stopped === true)
|
|
61
|
+
return stoppedWord;
|
|
62
|
+
if (project.prepared === true)
|
|
63
|
+
return preparedWord;
|
|
64
|
+
return heldWord;
|
|
65
|
+
}
|
|
66
|
+
// A waiting run parked on an External agent's review, which goes on by itself once the agent
|
|
67
|
+
// answers. Kept as is wins, as in `heldStateWord`.
|
|
68
|
+
export function parkedOnAgent(project) {
|
|
69
|
+
return (project.status === "pending" && project.waitingForAgent === true && project.setAside !== true);
|
|
70
|
+
}
|
|
71
|
+
// Going on by itself: running, or parked on an agent's review. Home's Running now and the
|
|
72
|
+
// Projects Running filter.
|
|
73
|
+
export function runningNow(project) {
|
|
74
|
+
return project.status === "running" || parkedOnAgent(project);
|
|
75
|
+
}
|
|
76
|
+
// Started and waiting its turn: pending with none of its run done, not stopped and not parked
|
|
77
|
+
// on an agent's review. Projects' Queued filter and Home's queued line under Running now.
|
|
78
|
+
export function queuedRun(project) {
|
|
79
|
+
return project.status === "pending" && !heldRun(project);
|
|
80
|
+
}
|
|
81
|
+
// An uploaded project already reads "done" from the projects route (`uploadedAndSettled`).
|
|
82
|
+
export function projectStateWord(project) {
|
|
83
|
+
return heldRun(project) ? heldStateWord(project) : projectStateWords[project.status];
|
|
84
|
+
}
|
|
85
|
+
// A run that ran and now waits for the person, held or paused: Home's Needs you, the Projects
|
|
86
|
+
// "Needs you" filter and its waiting-first sort.
|
|
87
|
+
export function waitsForPerson(project) {
|
|
88
|
+
return project.status === "paused" || (heldRun(project) && heldForPerson(project));
|
|
89
|
+
}
|
|
90
|
+
// A step failed and the run reads Pending by the wait beside it, nothing else running
|
|
91
|
+
// (`derive`): what `stepFailed` says on the Projects list, a project's page and the calendar.
|
|
92
|
+
// Unlike `stepFailedWhileWaiting`, false while a third step still runs.
|
|
93
|
+
export function readsByWaitBesideFailure(status, stages) {
|
|
94
|
+
return status === "pending" && stages.some((stage) => stage.state === "failed");
|
|
95
|
+
}
|
|
96
|
+
// A step failed while another waits for the agent, the person or its day (`waitsForAnswer`),
|
|
97
|
+
// whether or not a third still runs: what "Step failed" is told for (`notifications/rules.ts`
|
|
98
|
+
// runNotices) and While you were away counts. A failed step in a run with nothing waiting is
|
|
99
|
+
// told by the run's own end notice. Unlike `readsByWaitBesideFailure`, true while a third step
|
|
100
|
+
// still runs.
|
|
101
|
+
export function stepFailedWhileWaiting(status, stages) {
|
|
102
|
+
return ((status === "running" || status === "pending") &&
|
|
103
|
+
stages.some((stage) => stage.state === "failed") &&
|
|
104
|
+
waitsForAnswer(stages));
|
|
105
|
+
}
|
|
106
|
+
// A run listed as failed, on Home's Needs you and under the Projects Failed filter: it failed, or
|
|
107
|
+
// a step failed while the run still waits for the agent or the person (`stepFailed`).
|
|
108
|
+
export function failedRun(project) {
|
|
109
|
+
return project.status === "failed" || project.stepFailed === true;
|
|
110
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { liveProject } from "../admission/repo.js";
|
|
3
|
+
import { releaseCalendar } from "../studio/calendar.js";
|
|
4
|
+
export function rowsOf(rows, source, skip, entries) {
|
|
5
|
+
return rows.flatMap((value) => {
|
|
6
|
+
try {
|
|
7
|
+
return entries(value);
|
|
8
|
+
}
|
|
9
|
+
catch (error) {
|
|
10
|
+
skip(source, value, error);
|
|
11
|
+
return [];
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
const stageRow = z.object({
|
|
16
|
+
project_id: z.string(),
|
|
17
|
+
title: z.string(),
|
|
18
|
+
kind: z.string(),
|
|
19
|
+
failure_reason: z.string().nullable(),
|
|
20
|
+
finished_at: z.string().nullable(),
|
|
21
|
+
head: z.string().nullable(),
|
|
22
|
+
retrying: z.number(),
|
|
23
|
+
});
|
|
24
|
+
// A narration chunk that stopped matching its text is recorded again by Slopify itself
|
|
25
|
+
// (`rebuild/narration-retry.ts`): pending (also while it waits for a busy project), or started
|
|
26
|
+
// after the failure it answers. A short whose pick stopped fails the video stage the same way.
|
|
27
|
+
export function failedSteps(db, skip) {
|
|
28
|
+
const rows = db
|
|
29
|
+
.prepare(`SELECT s.project_id, p.title, s.kind, s.failure_reason, s.finished_at, h.revision_id AS head,
|
|
30
|
+
(s.kind IN ('audio','video') AND EXISTS (SELECT 1 FROM narration_retries n WHERE n.project_id=s.project_id
|
|
31
|
+
AND (n.state='pending' OR (n.state='started' AND n.updated_at>=COALESCE(s.finished_at,''))))) AS retrying
|
|
32
|
+
FROM stages s
|
|
33
|
+
JOIN projects p ON p.id=s.project_id
|
|
34
|
+
LEFT JOIN project_heads h ON h.project_id=s.project_id
|
|
35
|
+
WHERE s.state='failed' AND ${liveProject("s", "project_id")}`)
|
|
36
|
+
.all();
|
|
37
|
+
return rowsOf(rows, "failed steps", skip, (value) => {
|
|
38
|
+
const row = stageRow.parse(value);
|
|
39
|
+
const retrying = row.retrying === 1;
|
|
40
|
+
return [
|
|
41
|
+
{
|
|
42
|
+
kind: "failed-step",
|
|
43
|
+
since: row.finished_at ?? "",
|
|
44
|
+
projectId: row.project_id,
|
|
45
|
+
title: row.title,
|
|
46
|
+
stage: row.kind,
|
|
47
|
+
itemKey: null,
|
|
48
|
+
reason: row.failure_reason ??
|
|
49
|
+
"The step failed without saying why. Try it again with retry_stage.",
|
|
50
|
+
autoRetryInFlight: retrying,
|
|
51
|
+
agentCanAct: !retrying,
|
|
52
|
+
ids: {
|
|
53
|
+
id: row.project_id,
|
|
54
|
+
kind: row.kind,
|
|
55
|
+
...(row.head === null ? {} : { baseRevisionId: row.head }),
|
|
56
|
+
},
|
|
57
|
+
tools: [
|
|
58
|
+
"get_project",
|
|
59
|
+
"read_narration_chunks",
|
|
60
|
+
"get_revision",
|
|
61
|
+
"save_revision",
|
|
62
|
+
"preview_rebuild",
|
|
63
|
+
"start_rebuild",
|
|
64
|
+
"resume_project",
|
|
65
|
+
"retry_stage",
|
|
66
|
+
"soften_stage",
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
const runRow = z.object({
|
|
73
|
+
id: z.string(),
|
|
74
|
+
title: z.string(),
|
|
75
|
+
updated_at: z.string(),
|
|
76
|
+
head: z.string().nullable(),
|
|
77
|
+
});
|
|
78
|
+
// A run nothing moves on but Continue the run (resume_project), which the agent may give once
|
|
79
|
+
// nothing in the project still needs fixing: the runs `which` picks among the live projects.
|
|
80
|
+
function runsToContinue(db, skip, run) {
|
|
81
|
+
const rows = db
|
|
82
|
+
.prepare(`SELECT p.id, p.title, p.updated_at, h.revision_id AS head FROM projects p
|
|
83
|
+
LEFT JOIN project_heads h ON h.project_id=p.id
|
|
84
|
+
WHERE ${liveProject("p")} AND ${run.which}`)
|
|
85
|
+
.all(...run.params);
|
|
86
|
+
return rowsOf(rows, run.source, skip, (value) => {
|
|
87
|
+
const row = runRow.parse(value);
|
|
88
|
+
return [
|
|
89
|
+
{
|
|
90
|
+
kind: run.kind,
|
|
91
|
+
since: row.updated_at,
|
|
92
|
+
projectId: row.id,
|
|
93
|
+
title: row.title,
|
|
94
|
+
stage: null,
|
|
95
|
+
itemKey: null,
|
|
96
|
+
reason: run.reason,
|
|
97
|
+
autoRetryInFlight: false,
|
|
98
|
+
agentCanAct: true,
|
|
99
|
+
ids: { id: row.id, ...(row.head === null ? {} : { baseRevisionId: row.head }) },
|
|
100
|
+
tools: ["get_project", "resume_project"],
|
|
101
|
+
},
|
|
102
|
+
];
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
// No pause reason is stored: any paused run is the agent's to continue, whoever paused it.
|
|
106
|
+
export function pausedRuns(db, skip) {
|
|
107
|
+
return runsToContinue(db, skip, {
|
|
108
|
+
kind: "paused-run",
|
|
109
|
+
source: "paused runs",
|
|
110
|
+
which: "EXISTS (SELECT 1 FROM project_controls c WHERE c.project_id=p.id AND c.paused=1)",
|
|
111
|
+
params: [],
|
|
112
|
+
reason: "Paused: nothing runs until Continue the run (resume_project). Read get_project first: continue it once nothing it shows still needs fixing.",
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
// A run a restart stopped before it finished (`reviews/holds.ts`): like a paused one, nothing
|
|
116
|
+
// goes on until Continue the run, and the agent may give it.
|
|
117
|
+
export function stoppedRuns(db, stopped, skip) {
|
|
118
|
+
return runsToContinue(db, skip, {
|
|
119
|
+
kind: "stopped-run",
|
|
120
|
+
source: "stopped runs",
|
|
121
|
+
which: "p.id IN (SELECT value FROM json_each(?))",
|
|
122
|
+
params: [JSON.stringify([...stopped])],
|
|
123
|
+
reason: "Stopped before it finished, as a restart leaves a run: nothing goes on until Continue the run (resume_project). Read get_project first: continue it once nothing it shows still needs fixing.",
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
const topicsRow = z.object({
|
|
127
|
+
id: z.string(),
|
|
128
|
+
name: z.string(),
|
|
129
|
+
held: z.number(),
|
|
130
|
+
since: z.string(),
|
|
131
|
+
});
|
|
132
|
+
export function heldTopics(db, skip) {
|
|
133
|
+
const rows = db
|
|
134
|
+
.prepare(`SELECT s.id, s.name, count(t.id) AS held, min(t.created_at) AS since FROM schedules s
|
|
135
|
+
JOIN schedule_topics t ON t.schedule_id=s.id AND t.state='held'
|
|
136
|
+
WHERE s.deleted_at IS NULL GROUP BY s.id, s.name`)
|
|
137
|
+
.all();
|
|
138
|
+
return rowsOf(rows, "held topics", skip, (value) => {
|
|
139
|
+
const row = topicsRow.parse(value);
|
|
140
|
+
return [
|
|
141
|
+
{
|
|
142
|
+
kind: "held-topics",
|
|
143
|
+
since: row.since,
|
|
144
|
+
projectId: null,
|
|
145
|
+
title: row.name,
|
|
146
|
+
stage: null,
|
|
147
|
+
itemKey: null,
|
|
148
|
+
reason: `${String(row.held)} suggested ${row.held === 1 ? "topic waits" : "topics wait"} for approval.`,
|
|
149
|
+
autoRetryInFlight: false,
|
|
150
|
+
agentCanAct: true,
|
|
151
|
+
ids: { id: row.id },
|
|
152
|
+
tools: ["list_held_topics", "approve_topics", "reject_topics", "approve_all_topics"],
|
|
153
|
+
},
|
|
154
|
+
];
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
// Calendar → Releases' "Late · upload now": the agent reports it; uploading is the person's.
|
|
158
|
+
export function lateUploads(deps, now) {
|
|
159
|
+
return releaseCalendar(deps, { now, weeks: 2, candidates: [] }).entries.flatMap((entry) => entry.project === null
|
|
160
|
+
? []
|
|
161
|
+
: entry.items
|
|
162
|
+
.filter((item) => item.state === "late")
|
|
163
|
+
.map((item) => {
|
|
164
|
+
const project = entry.project;
|
|
165
|
+
return {
|
|
166
|
+
kind: "late-upload",
|
|
167
|
+
since: item.uploadBy ?? item.at ?? entry.at,
|
|
168
|
+
projectId: project?.id ?? null,
|
|
169
|
+
title: project?.title ?? "",
|
|
170
|
+
stage: "video",
|
|
171
|
+
itemKey: item.short === 0 ? "video" : `short:${String(item.short)}`,
|
|
172
|
+
reason: `Late · upload now: "${item.title}" was due to be uploaded by ${item.uploadBy ?? "now"} for its release at ${item.at ?? entry.at}. Uploading is the person's: report it.`,
|
|
173
|
+
autoRetryInFlight: false,
|
|
174
|
+
agentCanAct: false,
|
|
175
|
+
ids: { id: project?.id ?? "", short: String(item.short) },
|
|
176
|
+
tools: ["get_upload_pack", "get_project_releases"],
|
|
177
|
+
};
|
|
178
|
+
}));
|
|
179
|
+
}
|
|
180
|
+
const videoRow = z.object({
|
|
181
|
+
project_id: z.string(),
|
|
182
|
+
title: z.string(),
|
|
183
|
+
short: z.number(),
|
|
184
|
+
recorded_at: z.string(),
|
|
185
|
+
finish_state: z.string(),
|
|
186
|
+
finish_message: z.string().nullable(),
|
|
187
|
+
comment_state: z.string(),
|
|
188
|
+
comment_message: z.string().nullable(),
|
|
189
|
+
ab_state: z.string(),
|
|
190
|
+
ab_message: z.string().nullable(),
|
|
191
|
+
ab_at: z.string().nullable(),
|
|
192
|
+
});
|
|
193
|
+
// What the Studio extension could not finish on YouTube's side: the agent reports it.
|
|
194
|
+
export function extensionFailures(db, skip) {
|
|
195
|
+
const rows = db
|
|
196
|
+
.prepare(`SELECT v.project_id, p.title, v.short, v.recorded_at, v.finish_state, v.finish_message,
|
|
197
|
+
v.comment_state, v.comment_message, v.ab_state, v.ab_message, v.ab_at
|
|
198
|
+
FROM youtube_videos v JOIN projects p ON p.id=v.project_id
|
|
199
|
+
WHERE (v.finish_state='failed' OR v.comment_state='failed' OR v.ab_state='failed')
|
|
200
|
+
AND ${liveProject("v", "project_id")}`)
|
|
201
|
+
.all();
|
|
202
|
+
return rowsOf(rows, "extension failures", skip, (value) => {
|
|
203
|
+
const row = videoRow.parse(value);
|
|
204
|
+
const touches = [
|
|
205
|
+
row.finish_state === "failed"
|
|
206
|
+
? {
|
|
207
|
+
what: "The finishing touches in YouTube Studio (end screen, captions, related video)",
|
|
208
|
+
message: row.finish_message,
|
|
209
|
+
at: row.recorded_at,
|
|
210
|
+
}
|
|
211
|
+
: undefined,
|
|
212
|
+
row.comment_state === "failed"
|
|
213
|
+
? { what: "The pinned comment", message: row.comment_message, at: row.recorded_at }
|
|
214
|
+
: undefined,
|
|
215
|
+
row.ab_state === "failed"
|
|
216
|
+
? { what: "The A/B test", message: row.ab_message, at: row.ab_at ?? row.recorded_at }
|
|
217
|
+
: undefined,
|
|
218
|
+
].filter((touch) => touch !== undefined);
|
|
219
|
+
return touches.map((touch) => ({
|
|
220
|
+
kind: "extension-failure",
|
|
221
|
+
since: touch.at,
|
|
222
|
+
projectId: row.project_id,
|
|
223
|
+
title: row.title,
|
|
224
|
+
stage: "video",
|
|
225
|
+
itemKey: row.short === 0 ? "video" : `short:${String(row.short)}`,
|
|
226
|
+
reason: `${touch.what} failed: ${touch.message ?? "the extension gave no reason."} Fixing it is the extension's or the person's: report it.`,
|
|
227
|
+
autoRetryInFlight: false,
|
|
228
|
+
agentCanAct: false,
|
|
229
|
+
ids: { id: row.project_id, short: String(row.short) },
|
|
230
|
+
tools: ["get_youtube_videos"],
|
|
231
|
+
}));
|
|
232
|
+
});
|
|
233
|
+
}
|