@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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { stageKinds, stageStates } from "../../kernel/pipeline.js";
|
|
3
3
|
import { optionalDeps } from "../../kernel/runner/graph.js";
|
|
4
|
+
import { admittedStates, parkedKinds } from "../admission/admitted-work.js";
|
|
4
5
|
import { sourceOf } from "../admission/model.js";
|
|
5
6
|
import { usesShorts, usesYoutubeDescription } from "../admission/rules.js";
|
|
6
7
|
import { reviewsNarration } from "../reviews/rules.js";
|
|
@@ -138,7 +139,7 @@ export function executionStandings(deps, projectId, derived) {
|
|
|
138
139
|
if (view === undefined)
|
|
139
140
|
return [];
|
|
140
141
|
const work = derived ?? executionStages(deps, projectId);
|
|
141
|
-
|
|
142
|
+
const standings = stageKinds.map((kind) => {
|
|
142
143
|
const source = sourceOf(view.revision.config.sources, kind);
|
|
143
144
|
const group = work
|
|
144
145
|
.filter((entry) => entry.kind === kind)
|
|
@@ -174,8 +175,17 @@ export function executionStandings(deps, projectId, derived) {
|
|
|
174
175
|
? "provided"
|
|
175
176
|
: "done"
|
|
176
177
|
: "pending";
|
|
177
|
-
return state === "pending" && retryAt !== null
|
|
178
|
+
return state === "pending" && retryAt !== null
|
|
179
|
+
? { kind, state, source, retryAt }
|
|
180
|
+
: { kind, state, source };
|
|
178
181
|
});
|
|
182
|
+
const admitted = admittedStates(deps.db, projectId);
|
|
183
|
+
const parked = parkedKinds(deps.db, projectId);
|
|
184
|
+
return standings.map((stage) => ({
|
|
185
|
+
...stage,
|
|
186
|
+
...(admitted.has(`${stage.kind}:${stage.state}`) ? { admitted: true } : {}),
|
|
187
|
+
...(parked.has(stage.kind) ? { parked: true } : {}),
|
|
188
|
+
}));
|
|
179
189
|
}
|
|
180
190
|
// A finished piece whose request was planned under this fingerprint: a placeholder is planned
|
|
181
191
|
// under the logical fingerprint, the finished piece carries the one its request resolved to.
|
|
@@ -216,7 +226,9 @@ export function projectStandings(deps, projectId) {
|
|
|
216
226
|
.get(entry.work.workId);
|
|
217
227
|
return total + fraction(progress?.progress_current, progress?.progress_total);
|
|
218
228
|
}, 0);
|
|
219
|
-
// A failed step, or else one waiting to run again, says why on the stage row.
|
|
229
|
+
// A failed step, or else one waiting to run again, says why on the stage row. A failed
|
|
230
|
+
// stage keeps the moment it failed: needs_attention tells a narration retry started after
|
|
231
|
+
// the failure (still going) from one started before it (spent) by it.
|
|
220
232
|
const failed = all.find((work) => work.state === "failed") ??
|
|
221
233
|
(stage.retryAt === undefined || stage.retryAt === null
|
|
222
234
|
? undefined
|
|
@@ -227,8 +239,23 @@ export function projectStandings(deps, projectId) {
|
|
|
227
239
|
.prepare("SELECT failure_reason,failure_kind FROM revision_work WHERE id=?")
|
|
228
240
|
.get(failed.work.workId);
|
|
229
241
|
deps.db
|
|
230
|
-
.prepare(
|
|
231
|
-
|
|
242
|
+
.prepare(`UPDATE stages SET state=:state,failure_reason=:reason,failure_kind=:kind,retry_at=:retryAt,
|
|
243
|
+
progress_current=:current,progress_total=:total,
|
|
244
|
+
finished_at=CASE WHEN :state='done' THEN :now
|
|
245
|
+
WHEN :state='failed' THEN CASE WHEN state='failed' AND finished_at IS NOT NULL THEN finished_at ELSE :now END
|
|
246
|
+
ELSE NULL END
|
|
247
|
+
WHERE project_id=:projectId AND kind=:stage`)
|
|
248
|
+
.run({
|
|
249
|
+
state: stage.state,
|
|
250
|
+
reason: textOf(failure?.failure_reason),
|
|
251
|
+
kind: textOf(failure?.failure_kind),
|
|
252
|
+
retryAt: stage.retryAt ?? null,
|
|
253
|
+
current: invocations.length === 0 ? null : current,
|
|
254
|
+
total: invocations.length === 0 ? null : invocations.length,
|
|
255
|
+
now: deps.clock.now().toISOString(),
|
|
256
|
+
projectId,
|
|
257
|
+
stage: stage.kind,
|
|
258
|
+
});
|
|
232
259
|
}
|
|
233
260
|
settleScheduleRunsForProject(deps.db, projectId, deps.clock.now().toISOString());
|
|
234
261
|
return work;
|
|
@@ -2,7 +2,8 @@ import { rmSync } from "node:fs";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { transact } from "../../kernel/db/tx.js";
|
|
4
4
|
import { allPiecesOf, deletePiece, deletePieces, setPiece, } from "../../kernel/runner/piece-repo.js";
|
|
5
|
-
import { projectById,
|
|
5
|
+
import { projectById, stagesOf } from "../admission/repo.js";
|
|
6
|
+
import { resetStage } from "../admission/stage-writes.js";
|
|
6
7
|
import { storeArticleText } from "../article/store.js";
|
|
7
8
|
import { currentRevisionId } from "../revisions/repo.js";
|
|
8
9
|
import { outputPath } from "../storage/layout.js";
|
|
@@ -2,7 +2,8 @@ import { z } from "zod";
|
|
|
2
2
|
import { transact } from "../../kernel/db/tx.js";
|
|
3
3
|
import { isProviderError } from "../../kernel/ports/model.js";
|
|
4
4
|
import { insertPiece, piecesOf, setPiece } from "../../kernel/runner/piece-repo.js";
|
|
5
|
-
import { projectById
|
|
5
|
+
import { projectById } from "../admission/repo.js";
|
|
6
|
+
import { setStageProgress } from "../admission/stage-writes.js";
|
|
6
7
|
import { storeText } from "../storage/staging.js";
|
|
7
8
|
import { noTokens, plusUsage } from "../telemetry/model.js";
|
|
8
9
|
import { researchDocuments } from "./documents.js";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// ceiling: the owner's number. An hourly agent routine answers well inside two hours; a run
|
|
2
|
+
// with no agent at all still finishes the same evening.
|
|
3
|
+
export const externalReviewWaitMs = 2 * 60 * 60 * 1000;
|
|
4
|
+
// `canceled`: Cancel closed it while it waited; taken up again, its step asks it once more.
|
|
5
|
+
export const externalReviewStates = [
|
|
6
|
+
"waiting",
|
|
7
|
+
"answered",
|
|
8
|
+
"auto-passed",
|
|
9
|
+
"superseded",
|
|
10
|
+
"canceled",
|
|
11
|
+
];
|
|
12
|
+
// Why a review takes no verdict, one sentence per standing: what get_external_review explains,
|
|
13
|
+
// and what a refused verdict says after "The verdict wasn't saved."
|
|
14
|
+
export const closedReviewSentences = {
|
|
15
|
+
answered: "This review was already answered, so it takes no other verdict. Call needs_attention to see what is waiting now.",
|
|
16
|
+
"auto-passed": "This review passed at its 2-hour deadline with no verdict, so it takes none now. Call needs_attention to see what is waiting now.",
|
|
17
|
+
superseded: "This review was replaced: its item was made again or edited, and the new one waits for its own verdict. Call needs_attention to see it.",
|
|
18
|
+
"replaced-pending": "This review was replaced: its item was made again or edited, so it takes no verdict, and no new review waits for it yet. One opens once the new item is made; an edit saved but not continued makes none until the run is continued. There is nothing to answer here: call needs_attention to see what is waiting now.",
|
|
19
|
+
canceled: "This review's run was canceled, so nothing waits on it and it takes no verdict: a canceled run's reviews never pass. Call needs_attention to see what is waiting now.",
|
|
20
|
+
trashed: "This review's project is in the Trash, so nothing waits on it and it takes no verdict. Restore the project in Settings → Backup & storage → Trash; the review then waits for its verdict again.",
|
|
21
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { causedBy } from "../../kernel/errors.js";
|
|
2
|
+
import { reviewItemName } from "../notifications/rules.js";
|
|
3
|
+
import { revisionById } from "../revisions/repo.js";
|
|
4
|
+
import { settleExternalReviews } from "./external-repo.js";
|
|
5
|
+
function itemName(db, review) {
|
|
6
|
+
const revision = revisionById(db, review.projectId, review.revisionId);
|
|
7
|
+
return reviewItemName(review.itemKey, revision?.content.imageOrder ?? []);
|
|
8
|
+
}
|
|
9
|
+
// At boot and every few seconds: each review past its deadline passes, says so, and the run
|
|
10
|
+
// goes on. The deadline is a row, so a restart in between loses nothing. Each passed review is
|
|
11
|
+
// announced and woken on its own: one that throws is logged, and the others still go on (the
|
|
12
|
+
// passing itself is already saved).
|
|
13
|
+
export function releaseDueReviews(deps) {
|
|
14
|
+
const passed = settleExternalReviews(deps.db, deps.now());
|
|
15
|
+
for (const review of passed) {
|
|
16
|
+
try {
|
|
17
|
+
deps.emit(review.projectId, {
|
|
18
|
+
type: "review.auto-passed",
|
|
19
|
+
projectId: review.projectId,
|
|
20
|
+
reviewId: review.id,
|
|
21
|
+
stage: review.stage,
|
|
22
|
+
itemKey: review.itemKey,
|
|
23
|
+
item: itemName(deps.db, review),
|
|
24
|
+
revisionId: review.revisionId,
|
|
25
|
+
workId: review.workId,
|
|
26
|
+
});
|
|
27
|
+
deps.emit(review.projectId, { type: "project.updated", projectId: review.projectId });
|
|
28
|
+
deps.tick(review.projectId);
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
deps.log.write("error", "review.deadline", {
|
|
32
|
+
projectId: review.projectId,
|
|
33
|
+
detail: `Review ${review.id} passed at its deadline, but announcing it or waking the run failed: ${causedBy(error)}`,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return passed.length;
|
|
38
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { transact } from "../../kernel/db/tx.js";
|
|
3
|
+
import { liveProject } from "../admission/repo.js";
|
|
4
|
+
import { externalReviewByFingerprint, externalReviewById } from "./external-rows.js";
|
|
5
|
+
import { liveWork, reviewStanding, wantedBy } from "./external-waiting.js";
|
|
6
|
+
import { reviewStages } from "./model.js";
|
|
7
|
+
// A step asked again for the same review keeps its row; one whose earlier wait was superseded
|
|
8
|
+
// or canceled waits again from now.
|
|
9
|
+
export function openExternalReview(db, input) {
|
|
10
|
+
db.prepare(`INSERT INTO external_reviews(id,project_id,revision_id,work_id,piece_id,work_key,item_key,stage,item_fingerprint,review_fingerprint,material,state,waiting_since,deadline_at)
|
|
11
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,'waiting',?,?)
|
|
12
|
+
ON CONFLICT(project_id,review_fingerprint) DO UPDATE SET revision_id=excluded.revision_id,
|
|
13
|
+
work_id=excluded.work_id,piece_id=excluded.piece_id,work_key=excluded.work_key,
|
|
14
|
+
item_fingerprint=excluded.item_fingerprint,material=excluded.material,state='waiting',
|
|
15
|
+
waiting_since=excluded.waiting_since,deadline_at=excluded.deadline_at,verdict=NULL,
|
|
16
|
+
answered_by=NULL,answered_at=NULL
|
|
17
|
+
WHERE external_reviews.state IN ('superseded','canceled')`).run(input.id, input.projectId, input.revisionId, input.workId, input.pieceId, input.workKey, input.itemKey, input.stage, input.itemFingerprint, input.reviewFingerprint, JSON.stringify(input.material), input.waitingSince, input.deadlineAt);
|
|
18
|
+
const saved = externalReviewByFingerprint(db, input.projectId, input.reviewFingerprint);
|
|
19
|
+
if (saved === undefined)
|
|
20
|
+
throw new Error("Slopify hit an internal error (an external review could not be saved). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
|
|
21
|
+
return saved;
|
|
22
|
+
}
|
|
23
|
+
// Cancel stops a run's reviews waiting. Retried, its step asks again (`openExternalReview`) and
|
|
24
|
+
// each review waits its whole two hours from then, not from before the cancel.
|
|
25
|
+
export function stopWaitingReviews(db, projectId) {
|
|
26
|
+
db.prepare("UPDATE external_reviews SET state='canceled' WHERE project_id=? AND state='waiting'").run(projectId);
|
|
27
|
+
}
|
|
28
|
+
// The step that waited ended its stage as held, which parks the work (`finishWork`); a settled
|
|
29
|
+
// review lets it be claimed again while the current version still wants it. A restart also
|
|
30
|
+
// held the work's unfinished pieces (`recoverWork`), so the ones never submitted are allowed
|
|
31
|
+
// again, as boot does for work it keeps (`recoverCheckpointWork`). A paused project stays put:
|
|
32
|
+
// the runner claims nothing for it until Resume.
|
|
33
|
+
function releaseParkedWork(db, projectId, id) {
|
|
34
|
+
const parked = `(SELECT e.work_id FROM external_reviews e WHERE e.project_id=? AND e.id=? AND ${wantedBy("e")})`;
|
|
35
|
+
const released = db
|
|
36
|
+
.prepare(`UPDATE revision_work SET dispatch_state='allowed' WHERE state='pending' AND dispatch_state='held' AND id=${parked}`)
|
|
37
|
+
.run(projectId, id);
|
|
38
|
+
if (Number(released.changes) === 0)
|
|
39
|
+
return;
|
|
40
|
+
db.prepare(`UPDATE revision_work_pieces SET state='pending',dispatch_state='allowed'
|
|
41
|
+
WHERE work_id=${parked} AND state='held' AND submitted_at IS NULL AND continuation IS NULL`).run(projectId, id);
|
|
42
|
+
}
|
|
43
|
+
// The first verdict saved wins: read and answered in one transaction, so two agents answering
|
|
44
|
+
// at once cannot both succeed. A review whose item was just replaced is superseded here rather
|
|
45
|
+
// than at the next settle, a project in the Trash takes no verdict until it is restored, and a
|
|
46
|
+
// canceled run's review takes none at all.
|
|
47
|
+
export function answerExternalReview(db, input) {
|
|
48
|
+
return transact(db, () => {
|
|
49
|
+
const review = externalReviewById(db, input.projectId, input.id);
|
|
50
|
+
if (review === undefined)
|
|
51
|
+
return { ok: false, reason: "not-found" };
|
|
52
|
+
const standing = reviewStanding(db, review);
|
|
53
|
+
if (standing === "trashed")
|
|
54
|
+
return { ok: false, reason: "trashed" };
|
|
55
|
+
if (standing === "superseded" || standing === "replaced-pending")
|
|
56
|
+
db.prepare("UPDATE external_reviews SET state='superseded' WHERE project_id=? AND id=? AND state='waiting'").run(input.projectId, input.id);
|
|
57
|
+
if (standing !== "waiting")
|
|
58
|
+
return { ok: false, reason: standing, value: review };
|
|
59
|
+
db.prepare("UPDATE external_reviews SET state='answered', verdict=?, answered_by=?, answered_at=? WHERE project_id=? AND id=?").run(JSON.stringify(input.verdict), input.by, input.at, input.projectId, input.id);
|
|
60
|
+
releaseParkedWork(db, input.projectId, input.id);
|
|
61
|
+
return {
|
|
62
|
+
ok: true,
|
|
63
|
+
value: {
|
|
64
|
+
...review,
|
|
65
|
+
state: "answered",
|
|
66
|
+
verdict: input.verdict,
|
|
67
|
+
answeredBy: input.by,
|
|
68
|
+
answeredAt: input.at,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
const dueRow = z.object({
|
|
74
|
+
id: z.string(),
|
|
75
|
+
project_id: z.string(),
|
|
76
|
+
revision_id: z.string(),
|
|
77
|
+
work_id: z.string(),
|
|
78
|
+
stage: z.enum(reviewStages),
|
|
79
|
+
item_key: z.string(),
|
|
80
|
+
});
|
|
81
|
+
// Replaced reviews stop waiting; waiting ones past their deadline pass without review. A
|
|
82
|
+
// paused project's review passes too (nothing runs until Resume); a canceled run's never does,
|
|
83
|
+
// nor one of a project in the Trash (it waits on, and passes once restored).
|
|
84
|
+
// Read by plain columns only: a review whose stored material can't be read back still passes,
|
|
85
|
+
// and never holds up the others.
|
|
86
|
+
export function settleExternalReviews(db, now) {
|
|
87
|
+
const at = now.toISOString();
|
|
88
|
+
return transact(db, () => {
|
|
89
|
+
db.prepare(`UPDATE external_reviews SET state='superseded' WHERE state='waiting' AND NOT ${wantedBy("external_reviews")}`).run();
|
|
90
|
+
const due = db
|
|
91
|
+
.prepare(`SELECT e.id, e.project_id, e.revision_id, e.work_id, e.stage, e.item_key FROM external_reviews e
|
|
92
|
+
WHERE e.state='waiting' AND e.deadline_at<=? AND ${liveWork("e")} AND ${liveProject("e", "project_id")}
|
|
93
|
+
ORDER BY e.waiting_since, e.rowid`)
|
|
94
|
+
.all(at)
|
|
95
|
+
.map((value) => dueRow.parse(value));
|
|
96
|
+
return due.map((row) => {
|
|
97
|
+
db.prepare("UPDATE external_reviews SET state='auto-passed', answered_at=? WHERE id=? AND state='waiting'").run(at, row.id);
|
|
98
|
+
releaseParkedWork(db, row.project_id, row.id);
|
|
99
|
+
return {
|
|
100
|
+
id: row.id,
|
|
101
|
+
projectId: row.project_id,
|
|
102
|
+
revisionId: row.revision_id,
|
|
103
|
+
workId: row.work_id,
|
|
104
|
+
stage: row.stage,
|
|
105
|
+
itemKey: row.item_key,
|
|
106
|
+
};
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { actors } from "../agents/model.js";
|
|
3
|
+
import { externalReviewStates } from "./external-model.js";
|
|
4
|
+
import { reviewStages } from "./model.js";
|
|
5
|
+
const materialSchema = z.object({
|
|
6
|
+
request: z.object({
|
|
7
|
+
stage: z.enum(reviewStages),
|
|
8
|
+
instruction: z.string(),
|
|
9
|
+
sections: z.array(z.object({ label: z.string(), text: z.string() })),
|
|
10
|
+
images: z.array(z.string()),
|
|
11
|
+
}),
|
|
12
|
+
images: z.array(z.object({ stored: z.string(), name: z.string() })),
|
|
13
|
+
});
|
|
14
|
+
const verdictSchema = z.object({ passed: z.boolean(), reasons: z.array(z.string()) });
|
|
15
|
+
const rowSchema = z.object({
|
|
16
|
+
id: z.string(),
|
|
17
|
+
project_id: z.string(),
|
|
18
|
+
revision_id: z.string(),
|
|
19
|
+
work_id: z.string(),
|
|
20
|
+
piece_id: z.string(),
|
|
21
|
+
work_key: z.string(),
|
|
22
|
+
item_key: z.string(),
|
|
23
|
+
stage: z.enum(reviewStages),
|
|
24
|
+
item_fingerprint: z.string(),
|
|
25
|
+
review_fingerprint: z.string(),
|
|
26
|
+
material: z.string(),
|
|
27
|
+
state: z.enum(externalReviewStates),
|
|
28
|
+
waiting_since: z.string(),
|
|
29
|
+
deadline_at: z.string(),
|
|
30
|
+
verdict: z.string().nullable(),
|
|
31
|
+
answered_by: z.enum(actors).nullable(),
|
|
32
|
+
answered_at: z.string().nullable(),
|
|
33
|
+
});
|
|
34
|
+
// One stored review as the slice speaks of it; throws for a row that can't be read back.
|
|
35
|
+
export function externalReviewOf(value) {
|
|
36
|
+
const row = rowSchema.parse(value);
|
|
37
|
+
return {
|
|
38
|
+
id: row.id,
|
|
39
|
+
projectId: row.project_id,
|
|
40
|
+
revisionId: row.revision_id,
|
|
41
|
+
workId: row.work_id,
|
|
42
|
+
pieceId: row.piece_id,
|
|
43
|
+
workKey: row.work_key,
|
|
44
|
+
itemKey: row.item_key,
|
|
45
|
+
stage: row.stage,
|
|
46
|
+
itemFingerprint: row.item_fingerprint,
|
|
47
|
+
reviewFingerprint: row.review_fingerprint,
|
|
48
|
+
material: materialSchema.parse(JSON.parse(row.material)),
|
|
49
|
+
state: row.state,
|
|
50
|
+
waitingSince: row.waiting_since,
|
|
51
|
+
deadlineAt: row.deadline_at,
|
|
52
|
+
verdict: row.verdict === null ? null : verdictSchema.parse(JSON.parse(row.verdict)),
|
|
53
|
+
answeredBy: row.answered_by,
|
|
54
|
+
answeredAt: row.answered_at,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export function externalReviewByFingerprint(db, projectId, reviewFingerprint) {
|
|
58
|
+
const row = db
|
|
59
|
+
.prepare("SELECT * FROM external_reviews WHERE project_id=? AND review_fingerprint=?")
|
|
60
|
+
.get(projectId, reviewFingerprint);
|
|
61
|
+
return row === undefined ? undefined : externalReviewOf(row);
|
|
62
|
+
}
|
|
63
|
+
export function externalReviewById(db, projectId, id) {
|
|
64
|
+
const row = db
|
|
65
|
+
.prepare("SELECT * FROM external_reviews WHERE project_id=? AND id=?")
|
|
66
|
+
.get(projectId, id);
|
|
67
|
+
return row === undefined ? undefined : externalReviewOf(row);
|
|
68
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { liveProject, oneProject, projectTrashed } from "../admission/repo.js";
|
|
3
|
+
import { externalReviewOf } from "./external-rows.js";
|
|
4
|
+
// A review the project still wants: its step is reserved by the project's current version with
|
|
5
|
+
// the same fingerprint. A rerun or an edit that replaced the item leaves it unwanted.
|
|
6
|
+
export function wantedBy(table) {
|
|
7
|
+
return `EXISTS (SELECT 1 FROM revision_work_reservations r
|
|
8
|
+
JOIN project_heads h ON h.project_id=r.project_id AND h.revision_id=r.revision_id
|
|
9
|
+
WHERE r.project_id=${table}.project_id AND r.work_id=${table}.work_id
|
|
10
|
+
AND r.work_key=${table}.work_key AND r.fingerprint=${table}.review_fingerprint)`;
|
|
11
|
+
}
|
|
12
|
+
// Nothing of a canceled run goes ahead on its own: the review's work, or its own step, is
|
|
13
|
+
// canceled. Another step canceled is not enough: retrying one step (`recoverProject`) admits
|
|
14
|
+
// it again while the rest stay canceled, and its reviews wait for their verdict.
|
|
15
|
+
export function liveWork(table) {
|
|
16
|
+
return `EXISTS (SELECT 1 FROM revision_work w WHERE w.id=${table}.work_id AND NOT ${canceledStep("w")})`;
|
|
17
|
+
}
|
|
18
|
+
// The step reads canceled by its stage too, whatever the work row says: a restart before
|
|
19
|
+
// canceled work was kept canceled (`recoverWork`) held it pending.
|
|
20
|
+
function canceledStep(work) {
|
|
21
|
+
return `(${work}.state='canceled' OR EXISTS (SELECT 1 FROM stages s WHERE s.id=${work}.stage_id AND s.state='canceled'))`;
|
|
22
|
+
}
|
|
23
|
+
// Where a review stands, in the one order get_external_review and a refused verdict both say
|
|
24
|
+
// it: a verdict already kept, its run canceled (Cancel closed it, or its step reads canceled;
|
|
25
|
+
// no new one waits until the step is taken up and asks again, unless its item was made again
|
|
26
|
+
// and the new review waits: superseded), its item replaced or its step gone (`superseded`, or
|
|
27
|
+
// `replaced-pending` while no new one waits yet), then its project in the Trash - said last,
|
|
28
|
+
// as only then would Restore bring it back. It waits only when none holds, exactly when
|
|
29
|
+
// `stillWaiting` lists it.
|
|
30
|
+
export function reviewStanding(db, review) {
|
|
31
|
+
if (review.state === "answered" || review.state === "auto-passed")
|
|
32
|
+
return review.state;
|
|
33
|
+
if (review.state === "canceled" ||
|
|
34
|
+
db
|
|
35
|
+
.prepare(`SELECT 1 FROM revision_work w WHERE w.id=? AND ${canceledStep("w")}`)
|
|
36
|
+
.get(review.workId) !== undefined)
|
|
37
|
+
return replacementWaits(db, review) ? "superseded" : "canceled";
|
|
38
|
+
const wanted = review.state === "waiting" &&
|
|
39
|
+
db
|
|
40
|
+
.prepare(`SELECT 1 FROM external_reviews e WHERE e.project_id=? AND e.id=? AND ${wantedBy("e")} AND ${liveWork("e")}`)
|
|
41
|
+
.get(review.projectId, review.id) !== undefined;
|
|
42
|
+
if (!wanted)
|
|
43
|
+
return replacementWaits(db, review) ? "superseded" : "replaced-pending";
|
|
44
|
+
return projectTrashed(db, review.projectId) ? "trashed" : "waiting";
|
|
45
|
+
}
|
|
46
|
+
// Whether another review of the same item waits in its place, the project in the Trash or not:
|
|
47
|
+
// either way the old one was replaced.
|
|
48
|
+
function replacementWaits(db, review) {
|
|
49
|
+
return (db
|
|
50
|
+
.prepare(`SELECT 1 FROM external_reviews e WHERE ${wantedAndLive} AND e.project_id=? AND e.item_key=? AND e.id!=?`)
|
|
51
|
+
.get(review.projectId, review.itemKey, review.id) !== undefined);
|
|
52
|
+
}
|
|
53
|
+
// A review `e` marked waiting, still wanted and of a run that isn't canceled.
|
|
54
|
+
const wantedAndLive = `e.state='waiting' AND ${wantedBy("e")} AND ${liveWork("e")}`;
|
|
55
|
+
// A review `e` that waits: the same, of a project not in the Trash.
|
|
56
|
+
const stillWaiting = `${wantedAndLive} AND ${liveProject("e", "project_id")}`;
|
|
57
|
+
// Waiting, still wanted by the current version, of a run that isn't canceled, oldest first.
|
|
58
|
+
export function waitingExternalReviews(db, projectId) {
|
|
59
|
+
return waitingExternalReviewRows(db, projectId).map(externalReviewOf);
|
|
60
|
+
}
|
|
61
|
+
// The same rows unread, for a caller that skips one it can't read instead of failing them all.
|
|
62
|
+
export function waitingExternalReviewRows(db, projectId) {
|
|
63
|
+
const one = oneProject("e", projectId);
|
|
64
|
+
return db
|
|
65
|
+
.prepare(`SELECT e.* FROM external_reviews e WHERE ${stillWaiting}${one.sql} ORDER BY e.waiting_since, e.rowid`)
|
|
66
|
+
.all(...one.params);
|
|
67
|
+
}
|
|
68
|
+
// The projects with a review waiting for an agent, as `waitingExternalReviewRows` finds them.
|
|
69
|
+
export function projectsWaitingOnAgents(db, projectId) {
|
|
70
|
+
const one = oneProject("e", projectId);
|
|
71
|
+
const rows = db
|
|
72
|
+
.prepare(`SELECT DISTINCT e.project_id FROM external_reviews e WHERE ${stillWaiting}${one.sql}`)
|
|
73
|
+
.all(...one.params);
|
|
74
|
+
return new Set(rows.map((row) => z.string().parse(row.project_id)));
|
|
75
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { derive, satisfied, deps as stageDeps, } from "../../kernel/runner/graph.js";
|
|
3
|
+
import { pendingAdmitted } from "../admission/admitted-work.js";
|
|
4
|
+
import { liveProject, oneProject, stageStandingsByProject } from "../admission/repo.js";
|
|
5
|
+
import { checkpointStageSchema } from "../checkpoints/schema.js";
|
|
6
|
+
import { heldForItsDay } from "../schedules/prepare.js";
|
|
7
|
+
import { uploadedAndSettled } from "../uploads/repo.js";
|
|
8
|
+
import { projectsWaitingOnAgents } from "./external-waiting.js";
|
|
9
|
+
const gateRow = z.object({
|
|
10
|
+
project_id: z.string(),
|
|
11
|
+
checkpoint_id: z.string(),
|
|
12
|
+
stage: checkpointStageSchema,
|
|
13
|
+
revision_id: z.string(),
|
|
14
|
+
fingerprint: z.string(),
|
|
15
|
+
created_at: z.string(),
|
|
16
|
+
own: z.number(),
|
|
17
|
+
});
|
|
18
|
+
const projectRow = z.object({ project_id: z.string() });
|
|
19
|
+
// Every project's holds, or one project's alone when `projectId` is given. A caller that has
|
|
20
|
+
// already read the stage standings (`stageStandingsByProject`) or the uploaded projects that
|
|
21
|
+
// read Done (`uploadedAndSettled`) passes them; one that lists what it can read passes `skip`,
|
|
22
|
+
// given each checkpoint row that can't be read instead of failing them all.
|
|
23
|
+
export function projectHolds(db, scope = {}) {
|
|
24
|
+
const { projectId, skip } = scope;
|
|
25
|
+
const gatesOf = oneProject("c", projectId);
|
|
26
|
+
const gateSql = `SELECT c.project_id, c.checkpoint_id, c.stage, c.revision_id, c.fingerprint, c.created_at, (c.stage='video' AND ${heldForItsDay("c")}) AS own
|
|
27
|
+
FROM review_checkpoints c
|
|
28
|
+
JOIN project_heads h ON h.project_id=c.project_id AND h.revision_id=c.revision_id
|
|
29
|
+
WHERE c.state IN ('held','pending-review') AND c.approved_at IS NULL AND ${liveProject("c", "project_id")}${gatesOf.sql}`;
|
|
30
|
+
const gates = db
|
|
31
|
+
.prepare(gateSql)
|
|
32
|
+
.all(...gatesOf.params)
|
|
33
|
+
.flatMap((row) => {
|
|
34
|
+
if (skip === undefined)
|
|
35
|
+
return [gateRow.parse(row)];
|
|
36
|
+
const gate = gateRow.safeParse(row);
|
|
37
|
+
if (gate.success)
|
|
38
|
+
return [gate.data];
|
|
39
|
+
skip(row, gate.error);
|
|
40
|
+
return [];
|
|
41
|
+
});
|
|
42
|
+
const standings = scope.standings ?? stageStandingsByProject(db, projectId);
|
|
43
|
+
// A checkpoint holds the run once the stages its step needs are made; before that the run
|
|
44
|
+
// is held by whatever those stages wait on.
|
|
45
|
+
const reached = (project, stage) => stageDeps[stage].every((dependency) => {
|
|
46
|
+
const standing = standings.get(project)?.find((made) => made.kind === dependency);
|
|
47
|
+
return standing === undefined || satisfied(standing.state);
|
|
48
|
+
});
|
|
49
|
+
const theirs = new Set();
|
|
50
|
+
const reachedTheirs = new Set();
|
|
51
|
+
const reachedOwn = new Set();
|
|
52
|
+
const holding = new Map();
|
|
53
|
+
for (const gate of gates) {
|
|
54
|
+
const own = gate.own === 1;
|
|
55
|
+
if (!own)
|
|
56
|
+
theirs.add(gate.project_id);
|
|
57
|
+
if (!reached(gate.project_id, gate.stage))
|
|
58
|
+
continue;
|
|
59
|
+
(own ? reachedOwn : reachedTheirs).add(gate.project_id);
|
|
60
|
+
const hold = {
|
|
61
|
+
checkpointId: gate.checkpoint_id,
|
|
62
|
+
stage: gate.stage,
|
|
63
|
+
waitsFor: own ? "day" : "person",
|
|
64
|
+
revisionId: gate.revision_id,
|
|
65
|
+
fingerprint: gate.fingerprint,
|
|
66
|
+
since: gate.created_at,
|
|
67
|
+
};
|
|
68
|
+
holding.set(gate.project_id, [...(holding.get(gate.project_id) ?? []), hold]);
|
|
69
|
+
}
|
|
70
|
+
const headsOf = oneProject("h", projectId);
|
|
71
|
+
const scheduled = db
|
|
72
|
+
.prepare(`SELECT h.project_id FROM project_heads h WHERE ${heldForItsDay("h")}${headsOf.sql}`)
|
|
73
|
+
.all(...headsOf.params)
|
|
74
|
+
.map((row) => projectRow.parse(row).project_id);
|
|
75
|
+
const agents = projectsWaitingOnAgents(db, projectId);
|
|
76
|
+
const runs = heldRuns(db, projectId);
|
|
77
|
+
// An uploaded project with nothing admitted reads Done (`edge/http/projects.ts`).
|
|
78
|
+
const settled = scope.settled ?? uploadedAndSettled(db, projectId);
|
|
79
|
+
const waitingForAgent = new Set([...agents].filter((id) => !reachedTheirs.has(id) && runs.get(id)?.held !== true));
|
|
80
|
+
const stopped = new Set([...runs]
|
|
81
|
+
.filter(([, run]) => run.stuck)
|
|
82
|
+
.map(([id]) => id)
|
|
83
|
+
.filter((id) => !reachedTheirs.has(id) &&
|
|
84
|
+
!waitingForAgent.has(id) &&
|
|
85
|
+
// Held at its day's hold, it goes on by itself on its day, though a failed step's
|
|
86
|
+
// admission holds its video's step back until then; a step a restart held still
|
|
87
|
+
// waits for Continue the run.
|
|
88
|
+
(!reachedOwn.has(id) || runs.get(id)?.held === true) &&
|
|
89
|
+
!settled.has(id) &&
|
|
90
|
+
derive(standings.get(id) ?? []) === "pending"));
|
|
91
|
+
return {
|
|
92
|
+
prepared: new Set(scheduled.filter((id) => !theirs.has(id) && !stopped.has(id))),
|
|
93
|
+
waitingForAgent,
|
|
94
|
+
stopped,
|
|
95
|
+
// Only Continue the run moves a stopped run on: the one hold it can have reached, its
|
|
96
|
+
// day's, holds it again once it has.
|
|
97
|
+
holding: new Map([...holding].filter(([id]) => !stopped.has(id))),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
// An admitted step of `h`'s project still to run (`pendingAdmitted`), held other than one
|
|
101
|
+
// parked on an agent's review: a restart (`recoverWork`) or a pause held it, and only Continue
|
|
102
|
+
// the run starts it.
|
|
103
|
+
function heldBesideReviews(h) {
|
|
104
|
+
return `EXISTS (SELECT 1 FROM revision_work w
|
|
105
|
+
WHERE w.project_id=${h}.project_id AND w.dispatch_state='held' AND ${pendingAdmitted("w")}
|
|
106
|
+
AND NOT EXISTS (SELECT 1 FROM external_reviews e WHERE e.work_id=w.id AND e.state='waiting'))`;
|
|
107
|
+
}
|
|
108
|
+
const runRow = z.object({ project_id: z.string(), held: z.number(), stuck: z.number() });
|
|
109
|
+
// Whether `h` is still: no step's call in flight, not paused and not waiting its turn in a
|
|
110
|
+
// batch. A running stage needs no clause here: it makes the run's state running (`derive`).
|
|
111
|
+
function still(h) {
|
|
112
|
+
return `NOT EXISTS(SELECT 1 FROM revision_work w WHERE w.project_id=${h}.project_id AND w.state='running')
|
|
113
|
+
AND NOT EXISTS(SELECT 1 FROM project_controls c WHERE c.project_id=${h}.project_id AND c.paused=1)
|
|
114
|
+
AND NOT EXISTS(SELECT 1 FROM project_queue q WHERE q.project_id=${h}.project_id AND q.state='queued')`;
|
|
115
|
+
}
|
|
116
|
+
// No admitted work of `h`'s current version that can start: a saved rerun or edit is admitted
|
|
117
|
+
// by no one until Resume, and work whose admission has a failed sibling never starts.
|
|
118
|
+
function nothingAdmitted(h) {
|
|
119
|
+
return `NOT EXISTS(SELECT 1 FROM revision_work w
|
|
120
|
+
JOIN revision_work_reservations r ON r.work_id=w.id AND r.revision_id=${h}.revision_id
|
|
121
|
+
WHERE w.state='pending' AND w.dispatch_state='allowed' AND NOT EXISTS(
|
|
122
|
+
SELECT 1 FROM revision_work f WHERE f.admission_id=w.admission_id AND f.state IN ('failed','canceled')))`;
|
|
123
|
+
}
|
|
124
|
+
// Each project out of the Trash with a step held beside the reviews (`held`), and whether its
|
|
125
|
+
// run is stuck: it has unfinished stages, is still, and nothing will move it on but Continue
|
|
126
|
+
// the run, because nothing admitted can start or because a step is held (a step still allowed
|
|
127
|
+
// may wait on it). Each reading is made once per project (MATERIALIZED), not once per use.
|
|
128
|
+
function heldRuns(db, projectId) {
|
|
129
|
+
const one = oneProject("h", projectId);
|
|
130
|
+
const rows = db
|
|
131
|
+
.prepare(`WITH runs AS MATERIALIZED (
|
|
132
|
+
SELECT h.project_id, ${heldBesideReviews("h")} AS held,
|
|
133
|
+
EXISTS(SELECT 1 FROM stages s WHERE s.project_id=h.project_id AND s.state NOT IN ('done','provided','skipped'))
|
|
134
|
+
AND ${still("h")} AS idle,
|
|
135
|
+
${nothingAdmitted("h")} AS unadmitted
|
|
136
|
+
FROM project_heads h WHERE ${liveProject("h", "project_id")}${one.sql})
|
|
137
|
+
SELECT project_id, held, idle AND (held OR unadmitted) AS stuck FROM runs
|
|
138
|
+
WHERE held OR (idle AND unadmitted)`)
|
|
139
|
+
.all(...one.params);
|
|
140
|
+
return new Map(rows.map((row) => {
|
|
141
|
+
const run = runRow.parse(row);
|
|
142
|
+
return [run.project_id, { held: run.held === 1, stuck: run.stuck === 1 }];
|
|
143
|
+
}));
|
|
144
|
+
}
|
|
@@ -6,6 +6,15 @@ export const reviewStages = ["article", "images", "narration", "thumbnail", "sho
|
|
|
6
6
|
// Off is the same as the stage having no entry, which is what every project saved before
|
|
7
7
|
// reviews existed has.
|
|
8
8
|
export const reviewModes = ["off", "flag", "redo"];
|
|
9
|
+
// Who answers the reviews: a text model Slopify calls, or an external agent through MCP.
|
|
10
|
+
export const reviewers = ["model", "agent"];
|
|
11
|
+
// Who gave a verdict: the reviewer model, an external agent, the person (through the verdict
|
|
12
|
+
// route), or nobody when the review passed at its deadline.
|
|
13
|
+
export const reviewAnswerers = ["model", "agent", "person", "nobody"];
|
|
14
|
+
export const reviewerLabels = {
|
|
15
|
+
model: "Model",
|
|
16
|
+
agent: "External agent (MCP)",
|
|
17
|
+
};
|
|
9
18
|
// ceiling: the owner's numbers. Two redos is what a person would try before looking at it
|
|
10
19
|
// themselves; five is already six paid images for one slot.
|
|
11
20
|
export const defaultReviewRetries = 2;
|
|
@@ -35,6 +44,9 @@ export function reviewModeOf(settings, stage) {
|
|
|
35
44
|
export function reviewRetriesOf(settings) {
|
|
36
45
|
return settings?.retries ?? defaultReviewRetries;
|
|
37
46
|
}
|
|
47
|
+
export function reviewedByAgent(settings) {
|
|
48
|
+
return settings?.reviewer === "agent";
|
|
49
|
+
}
|
|
38
50
|
export function reviewedStages(settings) {
|
|
39
51
|
return reviewStages.filter((stage) => reviewModeOf(settings, stage) !== "off");
|
|
40
52
|
}
|
|
@@ -56,6 +68,7 @@ export function reviewSettingsForm(settings) {
|
|
|
56
68
|
provider: settings.provider,
|
|
57
69
|
model: settings.model,
|
|
58
70
|
...(settings.thinking === undefined ? {} : { thinking: settings.thinking }),
|
|
71
|
+
...(settings.reviewer === "agent" ? { reviewer: "agent" } : {}),
|
|
59
72
|
retries: settings.retries === undefined ? "" : String(settings.retries),
|
|
60
73
|
stages: Object.fromEntries(reviewStages.flatMap((stage) => {
|
|
61
74
|
const picked = settings.stages[stage];
|
|
@@ -86,6 +99,7 @@ export function reviewSettingsFromForm(form, makes) {
|
|
|
86
99
|
provider: form.provider,
|
|
87
100
|
model: form.model,
|
|
88
101
|
...(form.thinking === undefined ? {} : { thinking: form.thinking }),
|
|
102
|
+
...(form.reviewer === "agent" ? { reviewer: "agent" } : {}),
|
|
89
103
|
...(retries === undefined || !valid ? {} : { retries }),
|
|
90
104
|
stages,
|
|
91
105
|
},
|