@gentbajko/slopify 3.17.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 -10
- package/dist/edge/http/reviews.js +53 -18
- package/dist/edge/http/revisions.js +5 -1
- package/dist/edge/http/settings.js +9 -1
- package/dist/edge/http/starts.js +41 -0
- package/dist/edge/mcp/attribution.js +38 -0
- package/dist/edge/mcp/call-route.js +203 -0
- package/dist/edge/mcp/excluded-routes.js +95 -0
- package/dist/edge/mcp/file-tools.js +141 -0
- package/dist/edge/mcp/http.js +79 -0
- package/dist/edge/mcp/instructions.js +17 -0
- package/dist/edge/mcp/library-tools.js +143 -0
- package/dist/edge/mcp/remake-tools.js +90 -0
- package/dist/edge/mcp/review-tools.js +64 -0
- package/dist/edge/mcp/route-tool.js +61 -0
- package/dist/edge/mcp/server.js +48 -0
- package/dist/edge/mcp/state-shapes.js +60 -0
- package/dist/edge/mcp/tools-channels.js +153 -0
- package/dist/edge/mcp/tools-library.js +151 -0
- package/dist/edge/mcp/tools-planning.js +137 -0
- package/dist/edge/mcp/tools-projects.js +167 -0
- package/dist/edge/mcp/tools-publishing.js +148 -0
- package/dist/edge/mcp/tools-runs.js +94 -0
- package/dist/edge/mcp/tools-schedules.js +143 -0
- package/dist/edge/mcp/tools-setup.js +234 -0
- package/dist/edge/mcp/tools-storage.js +99 -0
- package/dist/edge/mcp/tools.js +21 -0
- package/dist/extension/slopify-studio-chrome.zip +0 -0
- package/dist/extension/slopify-studio-firefox.zip +0 -0
- package/dist/kernel/db/migrations/0055-external-reviews.sql +56 -0
- package/dist/kernel/db/migrations/0056-agent-actions.sql +20 -0
- package/dist/kernel/db/migrations/0057-agent-action-once.sql +5 -0
- package/dist/kernel/db/migrations/0058-revision-work-admission.sql +3 -0
- package/dist/kernel/db/migrations/0059-external-review-canceled.sql +29 -0
- package/dist/kernel/db/migrations/0060-reservation-work.sql +4 -0
- package/dist/kernel/db/migrations/0061-project-control-counts.sql +4 -0
- package/dist/kernel/db/migrations/0062-told-failures.sql +8 -0
- package/dist/kernel/pipeline.js +6 -0
- package/dist/kernel/runner/graph.js +37 -2
- package/dist/kernel/runner/index.js +6 -3
- package/dist/main.js +29 -13
- package/dist/patch-notes/3.18.0.md +13 -0
- package/dist/patch-notes/index.json +6 -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/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-DpdXRssU.css → index-BKbZIxAq.css} +1 -1
- package/dist/web/assets/index-Drth_MfQ.js +325 -0
- package/dist/web/assets/{pdf-B42WW32R.js → pdf-CQFUyijy.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-CbmBh8V-.js +0 -325
|
@@ -10,8 +10,8 @@ import { pauseProject } from "../../slices/control/index.js";
|
|
|
10
10
|
import { revisionControlSchema, } from "../../slices/control/revision-control-schema.js";
|
|
11
11
|
import { recoverProject } from "../../slices/rebuild/recovery.js";
|
|
12
12
|
import { recoveryResultSchema } from "../../slices/rebuild/recovery-model.js";
|
|
13
|
-
import { resumable } from "../../slices/rebuild/recovery-repo.js";
|
|
14
13
|
import { clearSoftening, requestSoftening, softenableKeys } from "../../slices/rebuild/soften.js";
|
|
14
|
+
import { projectHolds } from "../../slices/reviews/holds.js";
|
|
15
15
|
import { adoptBaseline } from "../../slices/revisions/adopt.js";
|
|
16
16
|
import { currentRevisionId } from "../../slices/revisions/repo.js";
|
|
17
17
|
import { allowsCustomModel } from "../../slices/settings/models.js";
|
|
@@ -73,7 +73,7 @@ export function actionRoutes(deps) {
|
|
|
73
73
|
const stages = stagesOf(deps.db, projectId);
|
|
74
74
|
return {
|
|
75
75
|
revisionId: currentRevisionId(deps.db, projectId) ?? null,
|
|
76
|
-
resumable:
|
|
76
|
+
resumable: projectHolds(deps.db, { projectId }).stopped.has(projectId),
|
|
77
77
|
project: {
|
|
78
78
|
...projectById(deps.db, projectId),
|
|
79
79
|
status: derive(stages, projectPaused(deps.db, projectId)),
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// The MCP endpoint calls the API in-process with these as the request's bindings (`c.env`).
|
|
2
|
+
// No network request can set them (the Node server passes its own `{ incoming, outgoing }`),
|
|
3
|
+
// which is how a route knows an agent asked, not the person.
|
|
4
|
+
export const agentBindings = { actor: "agent" };
|
|
5
|
+
export function actorOf(c) {
|
|
6
|
+
const env = c.env;
|
|
7
|
+
return typeof env === "object" && env !== null && "actor" in env && env.actor === "agent"
|
|
8
|
+
? "agent"
|
|
9
|
+
: "person";
|
|
10
|
+
}
|
package/dist/edge/http/app.js
CHANGED
|
@@ -2,8 +2,11 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
3
3
|
import { Hono } from "hono";
|
|
4
4
|
import { streamSSE } from "hono/streaming";
|
|
5
|
+
import { mcpPath } from "../../slices/agents/model.js";
|
|
5
6
|
import { isSampleProject } from "../../slices/onboarding/sample.js";
|
|
7
|
+
import { mcpRoutes } from "../mcp/http.js";
|
|
6
8
|
import { actionRoutes } from "./actions.js";
|
|
9
|
+
import { agentBindings } from "./actor.js";
|
|
7
10
|
import { audioPreviewRoutes } from "./audio-preview.js";
|
|
8
11
|
import { auditionRoutes } from "./auditions.js";
|
|
9
12
|
import { autostartRoutes } from "./autostart.js";
|
|
@@ -15,6 +18,7 @@ import { diagnosticsRoutes } from "./diagnostics.js";
|
|
|
15
18
|
import { documentThemeRoutes } from "./document-themes.js";
|
|
16
19
|
import { draftRoutes } from "./drafts.js";
|
|
17
20
|
import { entryRoutes } from "./entries.js";
|
|
21
|
+
import { externalReviewRoutes } from "./external-reviews.js";
|
|
18
22
|
import { fileRoutes } from "./files.js";
|
|
19
23
|
import { fontsRoutes } from "./fonts.js";
|
|
20
24
|
import { homeRoutes, uploadedRoutes } from "./home.js";
|
|
@@ -37,6 +41,7 @@ import { runCostRoutes } from "./run-cost.js";
|
|
|
37
41
|
import { calendarRoutes, scheduleRoutes } from "./schedules.js";
|
|
38
42
|
import { settingsRoutes } from "./settings.js";
|
|
39
43
|
import { stagingRoutes } from "./staging.js";
|
|
44
|
+
import { startRecorder } from "./starts.js";
|
|
40
45
|
import { storageRoutes } from "./storage.js";
|
|
41
46
|
import { filesRoutes } from "./storage-files.js";
|
|
42
47
|
import { studioRoutes } from "./studio.js";
|
|
@@ -75,6 +80,7 @@ function apiRoutes(deps, startedAt) {
|
|
|
75
80
|
.route("/projects", projectRoutes(deps))
|
|
76
81
|
.route("/projects", checkpointRoutes(deps))
|
|
77
82
|
.route("/projects", reviewRoutes(deps))
|
|
83
|
+
.route("/projects", externalReviewRoutes(deps))
|
|
78
84
|
.route("/projects", revisionRoutes(deps))
|
|
79
85
|
.route("/projects", outputRoutes(deps))
|
|
80
86
|
.route("/projects", revisionFolderRoutes(deps))
|
|
@@ -175,6 +181,7 @@ export function createApp(deps) {
|
|
|
175
181
|
extensions: { reason: "sample-read-only" },
|
|
176
182
|
});
|
|
177
183
|
})
|
|
184
|
+
.use("/api/*", startRecorder(deps))
|
|
178
185
|
.onError((error, c) => problemFromError(c, error, deps))
|
|
179
186
|
.notFound(missing)
|
|
180
187
|
.route("/api", apiRoutes(deps, startedAt))
|
|
@@ -186,6 +193,20 @@ export function createApp(deps) {
|
|
|
186
193
|
// The API answers for its whole prefix, so an unknown endpoint is a problem+json 404
|
|
187
194
|
// rather than the SPA's index.html with a 200.
|
|
188
195
|
.all("/api/*", missing);
|
|
196
|
+
// Agents reach the same API in-process, as the agent (`actor.ts`): every tool validates,
|
|
197
|
+
// refuses and answers exactly like the page's own request.
|
|
198
|
+
app.route(mcpPath, mcpRoutes({
|
|
199
|
+
db: deps.db,
|
|
200
|
+
paths: deps.paths,
|
|
201
|
+
ids: deps.ids,
|
|
202
|
+
clock: deps.clock,
|
|
203
|
+
log: deps.log,
|
|
204
|
+
version: deps.version,
|
|
205
|
+
host: deps.host,
|
|
206
|
+
// An uploaded narration's length, for its word timing (get_project_file).
|
|
207
|
+
...(deps.measureAudio === undefined ? {} : { measureAudio: deps.measureAudio }),
|
|
208
|
+
api: async (request) => app.request(request, undefined, agentBindings),
|
|
209
|
+
}));
|
|
189
210
|
// ceiling: the SPA directory is looked up once, at boot. The build that fills it runs
|
|
190
211
|
// before the server starts; a server started before its own build needs a restart.
|
|
191
212
|
if (existsSync(deps.webDist)) {
|
|
@@ -28,6 +28,14 @@ function refused(c, reason) {
|
|
|
28
28
|
extensions: { reason },
|
|
29
29
|
});
|
|
30
30
|
}
|
|
31
|
+
function alreadyApproved(c) {
|
|
32
|
+
return problem(c, {
|
|
33
|
+
status: 409,
|
|
34
|
+
title: titleOf(409),
|
|
35
|
+
detail: "This checkpoint was already approved, so nothing changed. Reload the page (or call needs_attention) to see what waits now.",
|
|
36
|
+
extensions: { reason: "already-approved" },
|
|
37
|
+
});
|
|
38
|
+
}
|
|
31
39
|
export function checkpointRoutes(deps) {
|
|
32
40
|
return buildCheckpointRoutes(deps);
|
|
33
41
|
}
|
|
@@ -75,12 +83,12 @@ function buildCheckpointRoutes(deps) {
|
|
|
75
83
|
if (!authority)
|
|
76
84
|
return refused(c, "conflict");
|
|
77
85
|
const result = authority.release(projectId, checkpointId, identity);
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
return c.json({ checkpoint, replayed:
|
|
86
|
+
// A known key was answered by the replay above, so a duplicate here is an approval
|
|
87
|
+
// someone else made first.
|
|
88
|
+
if (!result.ok)
|
|
89
|
+
return result.reason === "duplicate" ? alreadyApproved(c) : refused(c, result.reason);
|
|
90
|
+
notify(projectId, identity.revisionId, false);
|
|
91
|
+
return c.json({ checkpoint: checkpointRowSchema.parse(result.value), replayed: false });
|
|
84
92
|
});
|
|
85
93
|
})
|
|
86
94
|
.patch("/:id/checkpoints", zValidator("param", projectParam, onInvalid), zValidator("json", checkpointChangeSchema, onInvalid), async (c) => {
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { zValidator } from "@hono/zod-validator";
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import { HTTPException } from "hono/http-exception";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { causedBy } from "../../kernel/errors.js";
|
|
6
|
+
import { closedReviewSentences } from "../../slices/reviews/external-model.js";
|
|
7
|
+
import { answerExternalReview } from "../../slices/reviews/external-repo.js";
|
|
8
|
+
import { verdictAnswerSchema, verdictIssue } from "../../slices/reviews/verdict.js";
|
|
9
|
+
import { actorOf } from "./actor.js";
|
|
10
|
+
import { onInvalid, problem, problemFromError, titleOf } from "./problem.js";
|
|
11
|
+
const id = z
|
|
12
|
+
.string()
|
|
13
|
+
.min(1)
|
|
14
|
+
.max(64)
|
|
15
|
+
.regex(/^[0-9A-Za-z_-]+$/);
|
|
16
|
+
const reviewParam = z.object({ id, reviewId: id });
|
|
17
|
+
// A verdict that wasn't saved, what was wrong with it, and the tool and body to send instead.
|
|
18
|
+
// Said before the review is looked up, so it claims nothing about the review: a fixed verdict
|
|
19
|
+
// that is too late is told so then.
|
|
20
|
+
function notSaved(c, why) {
|
|
21
|
+
return problem(c, {
|
|
22
|
+
status: 400,
|
|
23
|
+
title: titleOf(400),
|
|
24
|
+
detail: `The verdict wasn't saved${why}. Call submit_review_verdict again with body {verdict: "pass" | "fail", reasons: ["one short sentence per problem"]} and that fixed; a pass may send reasons: [].`,
|
|
25
|
+
extensions: { reason: "invalid-verdict" },
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
// What an agent that left out `body` sent: nothing, or nothing Hono reads as JSON (it reads a
|
|
29
|
+
// body without a JSON content type as {}). It is told the tool and the body to send.
|
|
30
|
+
function noVerdict(c) {
|
|
31
|
+
return problem(c, {
|
|
32
|
+
status: 400,
|
|
33
|
+
title: titleOf(400),
|
|
34
|
+
detail: 'The verdict wasn\'t saved: no verdict came with it. Call submit_review_verdict again with body {verdict: "pass" | "fail", reasons: ["one short sentence per problem"]}; a pass may send reasons: [].',
|
|
35
|
+
extensions: { reason: "invalid-verdict" },
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
const sentNothing = (value) => typeof value !== "object" ||
|
|
39
|
+
value === null ||
|
|
40
|
+
Array.isArray(value) ||
|
|
41
|
+
Object.keys(value).length === 0;
|
|
42
|
+
// A verdict that isn't {verdict, reasons} in the reviewer's rules.
|
|
43
|
+
function verdictRefused(result, c) {
|
|
44
|
+
if (result.success)
|
|
45
|
+
return undefined;
|
|
46
|
+
if (sentNothing(result.data))
|
|
47
|
+
return noVerdict(c);
|
|
48
|
+
return notSaved(c, verdictIssue(result.error.issues, "it is not {verdict, reasons}"));
|
|
49
|
+
}
|
|
50
|
+
// An external agent's verdict on a review waiting for it (`slices/reviews/external-repo.ts`).
|
|
51
|
+
// The rules are the reviewer model's own; the outcome is decided when the review step runs
|
|
52
|
+
// again. The one 400 the route throws is the json validator's, for a body it can't parse as
|
|
53
|
+
// JSON.
|
|
54
|
+
export function externalReviewRoutes(deps) {
|
|
55
|
+
return new Hono()
|
|
56
|
+
.onError((error, c) => error instanceof HTTPException && error.status === 400
|
|
57
|
+
? notSaved(c, ": the body wasn't JSON")
|
|
58
|
+
: problemFromError(c, error, deps))
|
|
59
|
+
.post("/:id/external-reviews/:reviewId/verdict", zValidator("param", reviewParam, onInvalid), zValidator("json", verdictAnswerSchema, verdictRefused), async (c) => {
|
|
60
|
+
const { id: projectId, reviewId } = c.req.valid("param");
|
|
61
|
+
const answer = c.req.valid("json");
|
|
62
|
+
const result = answerExternalReview(deps.db, {
|
|
63
|
+
projectId,
|
|
64
|
+
id: reviewId,
|
|
65
|
+
verdict: { passed: answer.verdict === "pass", reasons: answer.reasons },
|
|
66
|
+
by: actorOf(c),
|
|
67
|
+
at: deps.clock.now().toISOString(),
|
|
68
|
+
});
|
|
69
|
+
if (!result.ok)
|
|
70
|
+
return result.reason === "not-found"
|
|
71
|
+
? problem(c, {
|
|
72
|
+
status: 404,
|
|
73
|
+
title: titleOf(404),
|
|
74
|
+
detail: "This review doesn't exist: the project may have been deleted. Call needs_attention to see what is waiting now.",
|
|
75
|
+
extensions: { reason: "not-found" },
|
|
76
|
+
})
|
|
77
|
+
: problem(c, {
|
|
78
|
+
status: 409,
|
|
79
|
+
title: titleOf(409),
|
|
80
|
+
detail: `The verdict wasn't saved. ${closedReviewSentences[result.reason]}`,
|
|
81
|
+
extensions: { reason: result.reason },
|
|
82
|
+
});
|
|
83
|
+
try {
|
|
84
|
+
deps.runner.tick(projectId);
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
deps.log.write("error", "review.wake", {
|
|
88
|
+
projectId,
|
|
89
|
+
detail: causedBy(error),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
deps.hub.emit(projectId, { type: "project.updated", projectId });
|
|
93
|
+
return c.json({ review: { id: result.value.id, state: result.value.state } });
|
|
94
|
+
});
|
|
95
|
+
}
|
|
@@ -4,19 +4,22 @@ import { z } from "zod";
|
|
|
4
4
|
import { transact } from "../../kernel/db/tx.js";
|
|
5
5
|
import { derive, progressOf } from "../../kernel/runner/graph.js";
|
|
6
6
|
import { listProjectHeads } from "../../slices/admission/heads.js";
|
|
7
|
-
import { projectById, projectTrashed, runDraftSchema, stageStandingsByProject, stagesOf, } from "../../slices/admission/repo.js";
|
|
7
|
+
import { projectById, projectTrashed, runDraftSchema, stageStandingsByProject, stagesOf, stepFailuresByProject, } from "../../slices/admission/repo.js";
|
|
8
|
+
import { readsByWaitBesideFailure, stepFailedWhileWaiting, } from "../../slices/admission/state-words.js";
|
|
9
|
+
import { lastStarter } from "../../slices/agents/repo.js";
|
|
8
10
|
import { defaultChannelId } from "../../slices/channels/model.js";
|
|
9
11
|
import { channelById, projectChannels, setProjectChannel } from "../../slices/channels/repo.js";
|
|
10
12
|
import { withProjectControl } from "../../slices/control/lock.js";
|
|
11
13
|
import { stagesWithEta } from "../../slices/eta/view.js";
|
|
12
14
|
import { videoActivity } from "../../slices/rebuild/activity.js";
|
|
13
|
-
import {
|
|
15
|
+
import { projectHolds } from "../../slices/reviews/holds.js";
|
|
14
16
|
import { adoptBaseline } from "../../slices/revisions/adopt.js";
|
|
15
17
|
import { currentRevisionId } from "../../slices/revisions/repo.js";
|
|
16
18
|
import { limitWaitsByProject, listingWait } from "../../slices/run-cost/panel.js";
|
|
17
19
|
import { outputsOf } from "../../slices/storage/repo.js";
|
|
18
20
|
import { trashProject } from "../../slices/trash/service.js";
|
|
19
21
|
import { setAsideProjects, uploadedAndSettled, uploadedProjects, } from "../../slices/uploads/repo.js";
|
|
22
|
+
import { settledStatus } from "../../slices/uploads/rules.js";
|
|
20
23
|
import { onInvalid, problem, titleOf } from "./problem.js";
|
|
21
24
|
import { createProject } from "./project-create.js";
|
|
22
25
|
const idParam = z.object({
|
|
@@ -36,12 +39,15 @@ export function projectRoutes(deps) {
|
|
|
36
39
|
log: deps.log,
|
|
37
40
|
hasInflight: deps.runner.hasInflight,
|
|
38
41
|
};
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
const summarise = (project) => {
|
|
43
|
+
const stages = stagesOf(deps.db, project.id);
|
|
44
|
+
const status = settledStatus(derive(stages, project.paused), uploadedAndSettled(deps.db, project.id).has(project.id));
|
|
45
|
+
return {
|
|
46
|
+
...project,
|
|
47
|
+
status,
|
|
48
|
+
...(readsByWaitBesideFailure(status, stages) ? { stepFailed: true } : {}),
|
|
49
|
+
};
|
|
50
|
+
};
|
|
45
51
|
return (new Hono()
|
|
46
52
|
.post("/", zValidator("json", runDraftSchema, onInvalid), async (c) => {
|
|
47
53
|
const created = await createProject(deps, c.req.valid("json"));
|
|
@@ -67,17 +73,29 @@ export function projectRoutes(deps) {
|
|
|
67
73
|
const uploads = uploadedProjects(deps.db);
|
|
68
74
|
const aside = setAsideProjects(deps.db);
|
|
69
75
|
const settled = uploadedAndSettled(deps.db);
|
|
76
|
+
const holds = projectHolds(deps.db, { standings, settled });
|
|
70
77
|
const waits = limitWaitsByProject(deps.db);
|
|
78
|
+
const failures = stepFailuresByProject(deps.db);
|
|
71
79
|
const projects = listProjectHeads(deps.db).map((project) => {
|
|
72
80
|
const stages = standings.get(project.id) ?? [];
|
|
73
81
|
const waiting = waits.get(project.id);
|
|
82
|
+
const holding = holds.holding.get(project.id);
|
|
83
|
+
const status = settledStatus(derive(stages, project.paused), settled.has(project.id));
|
|
74
84
|
return {
|
|
75
85
|
...project,
|
|
76
|
-
status
|
|
86
|
+
status,
|
|
77
87
|
progress: progressOf(stages),
|
|
78
88
|
channelId: channels.get(project.id) ?? defaultChannelId,
|
|
79
89
|
uploadedAt: uploads.get(project.id) ?? null,
|
|
80
90
|
...(aside.has(project.id) ? { setAside: true } : {}),
|
|
91
|
+
...(holds.prepared.has(project.id) ? { prepared: true } : {}),
|
|
92
|
+
...(holds.waitingForAgent.has(project.id) ? { waitingForAgent: true } : {}),
|
|
93
|
+
...(holds.stopped.has(project.id) ? { stopped: true } : {}),
|
|
94
|
+
...(readsByWaitBesideFailure(status, stages) ? { stepFailed: true } : {}),
|
|
95
|
+
...(stepFailedWhileWaiting(status, stages)
|
|
96
|
+
? { stepFailures: failures.get(project.id) ?? [] }
|
|
97
|
+
: {}),
|
|
98
|
+
...(holding === undefined ? {} : { holding }),
|
|
81
99
|
...(waiting === undefined ? {} : { limitWaits: waiting.map(listingWait) }),
|
|
82
100
|
};
|
|
83
101
|
});
|
|
@@ -96,12 +114,20 @@ export function projectRoutes(deps) {
|
|
|
96
114
|
}
|
|
97
115
|
if (deps.catalogue !== undefined)
|
|
98
116
|
adoptBaseline(deps, project.id);
|
|
117
|
+
const holds = projectHolds(deps.db, { projectId: project.id });
|
|
118
|
+
const stopped = holds.stopped.has(project.id);
|
|
99
119
|
return c.json({
|
|
100
120
|
revisionId: currentRevisionId(deps.db, project.id) ?? null,
|
|
101
|
-
resumable:
|
|
121
|
+
resumable: stopped,
|
|
122
|
+
// The checkpoints holding the run now, and whom each waits for (`reviews/holds.ts`).
|
|
123
|
+
holding: holds.holding.get(project.id) ?? [],
|
|
124
|
+
startedBy: lastStarter(deps.db, project.id),
|
|
102
125
|
project: {
|
|
103
126
|
...summarise(project),
|
|
104
127
|
...(setAsideProjects(deps.db).has(project.id) ? { setAside: true } : {}),
|
|
128
|
+
...(holds.prepared.has(project.id) ? { prepared: true } : {}),
|
|
129
|
+
...(holds.waitingForAgent.has(project.id) ? { waitingForAgent: true } : {}),
|
|
130
|
+
...(stopped ? { stopped: true } : {}),
|
|
105
131
|
},
|
|
106
132
|
stages: stagesWithEta(deps.db, stagesOf(deps.db, project.id), project.config, deps.clock.now()).map((stage) => {
|
|
107
133
|
if (stage.kind !== "video" || stage.state !== "running")
|
|
@@ -3,11 +3,13 @@ import { Hono } from "hono";
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { revisionControlSchema } from "../../slices/control/revision-control-schema.js";
|
|
5
5
|
import { recoverProject } from "../../slices/rebuild/recovery.js";
|
|
6
|
-
import {
|
|
6
|
+
import { readRecovery } from "../../slices/rebuild/recovery-repo.js";
|
|
7
|
+
import { claimRedoStart, redoStarting, releaseRedoStart, } from "../../slices/rebuild/review-redo.js";
|
|
7
8
|
import { actOnVerdict, listVerdicts, verdictById } from "../../slices/reviews/repo.js";
|
|
8
9
|
import { latestReviews } from "../../slices/reviews/view.js";
|
|
9
10
|
import { currentRevisionId } from "../../slices/revisions/repo.js";
|
|
10
11
|
import { getRevisionView } from "../../slices/revisions/view.js";
|
|
12
|
+
import { actorOf } from "./actor.js";
|
|
11
13
|
import { onInvalid, problem, titleOf } from "./problem.js";
|
|
12
14
|
const id = z
|
|
13
15
|
.string()
|
|
@@ -25,6 +27,12 @@ export function reviewRoutes(deps) {
|
|
|
25
27
|
title: titleOf(404),
|
|
26
28
|
detail: "This review no longer exists; the project may have been deleted or restored from a backup. Reload the project page.",
|
|
27
29
|
});
|
|
30
|
+
const decided = (c, verdict) => problem(c, {
|
|
31
|
+
status: 409,
|
|
32
|
+
title: titleOf(409),
|
|
33
|
+
detail: `This verdict was already decided: the item was ${verdict.action === "overruled" ? "kept as it is" : "sent to be made again"}${verdict.actionBy === "agent" ? " by an agent" : ""}, so nothing changed. Reload the project page (or call needs_attention) to see what waits now.`,
|
|
34
|
+
extensions: { reason: "decided" },
|
|
35
|
+
});
|
|
28
36
|
const notify = (projectId) => {
|
|
29
37
|
try {
|
|
30
38
|
deps.hub.emit(projectId, { type: "project.updated", projectId });
|
|
@@ -61,9 +69,13 @@ export function reviewRoutes(deps) {
|
|
|
61
69
|
// Checked and changed in one synchronous step, so a redo cannot start in between.
|
|
62
70
|
if (redoStarting(verdictId))
|
|
63
71
|
return busy();
|
|
64
|
-
const result = actOnVerdict(deps.db, projectId, verdictId, "overruled", deps.clock.now().toISOString());
|
|
72
|
+
const result = actOnVerdict(deps.db, projectId, verdictId, "overruled", deps.clock.now().toISOString(), actorOf(c));
|
|
65
73
|
if (!result.ok)
|
|
66
|
-
return result.reason === "not-found"
|
|
74
|
+
return result.reason === "not-found"
|
|
75
|
+
? gone(c)
|
|
76
|
+
: result.reason === "decided"
|
|
77
|
+
? decided(c, result.value)
|
|
78
|
+
: busy();
|
|
67
79
|
// A called-off redo released the work that was waiting on the item.
|
|
68
80
|
try {
|
|
69
81
|
deps.runner.tick(projectId);
|
|
@@ -79,28 +91,51 @@ export function reviewRoutes(deps) {
|
|
|
79
91
|
const verdict = verdictById(deps.db, projectId, verdictId);
|
|
80
92
|
if (verdict === undefined)
|
|
81
93
|
return gone(c);
|
|
94
|
+
const request = {
|
|
95
|
+
...c.req.valid("json"),
|
|
96
|
+
action: { kind: "redo", item: verdict.itemKey },
|
|
97
|
+
};
|
|
98
|
+
if (verdict.action !== null) {
|
|
99
|
+
// The same Redo sent again (a retry after a lost answer) gets its first answer.
|
|
100
|
+
const first = deps.rebuild === undefined ? undefined : readRecovery(deps.rebuild, projectId, request);
|
|
101
|
+
return first !== undefined && "ok" in first && first.ok
|
|
102
|
+
? c.json(first, 202)
|
|
103
|
+
: decided(c, verdict);
|
|
104
|
+
}
|
|
82
105
|
if (deps.rebuild === undefined)
|
|
83
106
|
return problem(c, {
|
|
84
107
|
status: 503,
|
|
85
108
|
title: titleOf(503),
|
|
86
109
|
detail: "Redo is not ready yet because Slopify is still starting. Wait a moment, reload the page and try again.",
|
|
87
110
|
});
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
extensions: { reason: "conflict" },
|
|
94
|
-
});
|
|
95
|
-
const result = await recoverProject(deps.rebuild, projectId, {
|
|
96
|
-
...c.req.valid("json"),
|
|
97
|
-
action: { kind: "redo", item: verdict.itemKey },
|
|
111
|
+
const already = () => problem(c, {
|
|
112
|
+
status: 409,
|
|
113
|
+
title: titleOf(409),
|
|
114
|
+
detail: "Slopify is already making this item again. Wait for it to land, then reload the project page.",
|
|
115
|
+
extensions: { reason: "conflict" },
|
|
98
116
|
});
|
|
99
|
-
if (
|
|
100
|
-
return
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
117
|
+
if (verdict.redoState === "pending" || verdict.redoState === "started")
|
|
118
|
+
return already();
|
|
119
|
+
// Claimed before the rebuild is awaited, so Overrule cannot decide the verdict meanwhile.
|
|
120
|
+
if (!claimRedoStart(verdictId))
|
|
121
|
+
return already();
|
|
122
|
+
try {
|
|
123
|
+
const result = await recoverProject(deps.rebuild, projectId, request);
|
|
124
|
+
if (!result.ok)
|
|
125
|
+
return refused(c, result, projectId);
|
|
126
|
+
const acted = actOnVerdict(deps.db, projectId, verdictId, "redone", deps.clock.now().toISOString(), actorOf(c));
|
|
127
|
+
if (!acted.ok)
|
|
128
|
+
return acted.reason === "not-found"
|
|
129
|
+
? gone(c)
|
|
130
|
+
: acted.reason === "decided"
|
|
131
|
+
? decided(c, acted.value)
|
|
132
|
+
: already();
|
|
133
|
+
notify(projectId);
|
|
134
|
+
return c.json(result, 202);
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
releaseRedoStart(verdictId);
|
|
138
|
+
}
|
|
104
139
|
});
|
|
105
140
|
function refused(c, result, projectId) {
|
|
106
141
|
const status = result.reason === "no-project"
|
|
@@ -2,6 +2,7 @@ import { zValidator } from "@hono/zod-validator";
|
|
|
2
2
|
import { Hono } from "hono";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { projectExists } from "../../slices/admission/repo.js";
|
|
5
|
+
import { agentActionsOf } from "../../slices/agents/repo.js";
|
|
5
6
|
import { previewRebuildSchema, startRebuildSchema } from "../../slices/rebuild/model.js";
|
|
6
7
|
import { narrationChunkOrder } from "../../slices/rebuild/runtime-narration-text.js";
|
|
7
8
|
import { previewRebuild, startRebuild } from "../../slices/rebuild/service.js";
|
|
@@ -73,7 +74,10 @@ export function revisionRoutes(deps) {
|
|
|
73
74
|
.get("/:id/revisions", zValidator("param", projectParam, onInvalid), (c) => {
|
|
74
75
|
const projectId = c.req.valid("param").id;
|
|
75
76
|
return projectExists(deps.db, projectId)
|
|
76
|
-
? c.json({
|
|
77
|
+
? c.json({
|
|
78
|
+
revisions: listRevisionHistory(deps.db, projectId),
|
|
79
|
+
agentActions: agentActionsOf(deps.db, projectId),
|
|
80
|
+
})
|
|
77
81
|
: refused(c, deps, projectId, { reason: "no-project" });
|
|
78
82
|
})
|
|
79
83
|
.get("/:id/revisions/:revisionId", zValidator("param", revisionParam, onInvalid), (c) => {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { zValidator } from "@hono/zod-validator";
|
|
2
2
|
import { Hono } from "hono";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
-
import {
|
|
4
|
+
import { lastAgentConnection } from "../../slices/agents/connection.js";
|
|
5
|
+
import { recentAgentActions } from "../../slices/agents/repo.js";
|
|
6
|
+
import { notificationUrlMax, notificationUrlProblem, testNotice, webhookBody, } from "../../slices/notifications/address.js";
|
|
5
7
|
import { createNotificationSender, sendFailureText } from "../../slices/notifications/send.js";
|
|
6
8
|
import { readNotificationUrl, saveNotificationUrl } from "../../slices/notifications/settings.js";
|
|
7
9
|
import { appearances, providerIds } from "../../slices/settings/model.js";
|
|
@@ -117,6 +119,12 @@ export function settingsRoutes(deps) {
|
|
|
117
119
|
}
|
|
118
120
|
return c.json({ sent: true });
|
|
119
121
|
})
|
|
122
|
+
// Settings → Connections → Agents (MCP): when an agent last opened an MCP session, and
|
|
123
|
+
// the latest changes agents made.
|
|
124
|
+
.get("/agents", (c) => c.json({
|
|
125
|
+
lastConnectedAt: lastAgentConnection(deps.db),
|
|
126
|
+
recent: recentAgentActions(deps.db),
|
|
127
|
+
}))
|
|
120
128
|
.get("/voices", (c) => c.json({ voices: voices(voiceDeps) }))
|
|
121
129
|
.post("/voices", zValidator("json", voiceBody, onInvalid), async (c) => {
|
|
122
130
|
const body = c.req.valid("json");
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { causedBy } from "../../kernel/errors.js";
|
|
2
|
+
import { projectsMadeBy } from "../../slices/agents/model.js";
|
|
3
|
+
import { recordStart } from "../../slices/agents/repo.js";
|
|
4
|
+
import { actorOf } from "./actor.js";
|
|
5
|
+
const projectStart = /^\/api\/projects\/([0-9A-Za-z_-]{1,64})\/(?:resume|rebuild|stages\/[a-z]+\/(?:retry|soften|rerun)|reviews\/[0-9A-Za-z_-]+\/redo)\/?$/;
|
|
6
|
+
const createdStart = /^\/api\/(?:drafts\/[^/]+\/start|onboarding\/short|schedules\/[^/]+\/topics\/prepare)\/?$/;
|
|
7
|
+
// The projects whose work a successful POST started: the one its path names, or those it made.
|
|
8
|
+
async function startedProjects(path, response, log) {
|
|
9
|
+
const named = projectStart.exec(path)?.[1];
|
|
10
|
+
if (named !== undefined)
|
|
11
|
+
return [named];
|
|
12
|
+
if (!createdStart.test(path))
|
|
13
|
+
return [];
|
|
14
|
+
try {
|
|
15
|
+
return projectsMadeBy(await response.clone().json());
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
log.write("error", "project.start", {
|
|
19
|
+
detail: `Who started the projects ${path} made wasn't recorded: its answer couldn't be read: ${causedBy(error)}`,
|
|
20
|
+
});
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
// Who last started each project's work, for the notices its run sends.
|
|
25
|
+
export function startRecorder(deps) {
|
|
26
|
+
return async (c, next) => {
|
|
27
|
+
await next();
|
|
28
|
+
if (c.req.method !== "POST" || !c.res.ok)
|
|
29
|
+
return;
|
|
30
|
+
for (const projectId of await startedProjects(c.req.path, c.res, deps.log))
|
|
31
|
+
try {
|
|
32
|
+
recordStart(deps.db, { projectId, actor: actorOf(c), at: deps.clock.now().toISOString() });
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
deps.log.write("error", "project.start", {
|
|
36
|
+
projectId,
|
|
37
|
+
detail: causedBy(error),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { projectsMadeBy } from "../../slices/agents/model.js";
|
|
3
|
+
import { paramsOf, routeOf } from "./route-tool.js";
|
|
4
|
+
const movedBody = z.object({ projectIds: z.array(z.string()) }).loose();
|
|
5
|
+
const trashBody = z
|
|
6
|
+
.object({ items: z.array(z.object({ kind: z.string(), id: z.string() })) })
|
|
7
|
+
.loose();
|
|
8
|
+
const savedVersion = z
|
|
9
|
+
.object({ view: z.object({ revision: z.object({ id: z.string() }) }) })
|
|
10
|
+
.loose();
|
|
11
|
+
// The projects a tool changed (the one its path names, those its body lists, or those its
|
|
12
|
+
// answer made) and the schedule it changed.
|
|
13
|
+
export function subjectsOf(tool, args, value) {
|
|
14
|
+
const { path } = routeOf(tool);
|
|
15
|
+
return {
|
|
16
|
+
projectIds: projectIdsOf(path, args, value),
|
|
17
|
+
scheduleId: path.startsWith("/api/schedules/:id") && typeof args.id === "string" ? args.id : null,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function projectIdsOf(path, args, value) {
|
|
21
|
+
if (path.includes("/projects/:id") && typeof args.id === "string")
|
|
22
|
+
return [args.id];
|
|
23
|
+
if (paramsOf(path).includes("projectId") && typeof args.projectId === "string")
|
|
24
|
+
return [args.projectId];
|
|
25
|
+
if (path.startsWith("/api/trash/:kind/:id"))
|
|
26
|
+
return args.kind === "project" && typeof args.id === "string" ? [args.id] : [];
|
|
27
|
+
const moved = movedBody.safeParse(args.body);
|
|
28
|
+
if (moved.success)
|
|
29
|
+
return moved.data.projectIds;
|
|
30
|
+
const trashed = trashBody.safeParse(args.body);
|
|
31
|
+
if (trashed.success)
|
|
32
|
+
return trashed.data.items.filter((item) => item.kind === "project").map((item) => item.id);
|
|
33
|
+
return projectsMadeBy(value);
|
|
34
|
+
}
|
|
35
|
+
export function revisionIdOf(value) {
|
|
36
|
+
const saved = savedVersion.safeParse(value);
|
|
37
|
+
return saved.success ? saved.data.view.revision.id : null;
|
|
38
|
+
}
|