@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
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { causedBy } from "../../kernel/errors.js";
|
|
3
|
+
import { liveProject } from "../admission/repo.js";
|
|
4
|
+
import { externalReviewOf } from "../reviews/external-rows.js";
|
|
5
|
+
import { waitingExternalReviewRows } from "../reviews/external-waiting.js";
|
|
6
|
+
import { projectHolds } from "../reviews/holds.js";
|
|
7
|
+
import { setAsideProjects } from "../uploads/repo.js";
|
|
8
|
+
import { extensionFailures, failedSteps, heldTopics, lateUploads, pausedRuns, rowsOf, stoppedRuns, } from "./attention-work.js";
|
|
9
|
+
export const attentionKinds = [
|
|
10
|
+
"external-review",
|
|
11
|
+
"flagged-verdict",
|
|
12
|
+
"checkpoint",
|
|
13
|
+
"failed-step",
|
|
14
|
+
"paused-run",
|
|
15
|
+
"stopped-run",
|
|
16
|
+
"held-topics",
|
|
17
|
+
"late-upload",
|
|
18
|
+
"extension-failure",
|
|
19
|
+
];
|
|
20
|
+
const projectOf = z.object({ project_id: z.string() });
|
|
21
|
+
// Oldest first. A project Kept as is waits for nobody, so none of its entries is listed. Each
|
|
22
|
+
// source is read on its own: one that fails is logged and left out, and the others still list.
|
|
23
|
+
export function needsAttention(deps, now) {
|
|
24
|
+
const { db, log } = deps;
|
|
25
|
+
const skip = (source, value, error) => {
|
|
26
|
+
const project = projectOf.safeParse(value);
|
|
27
|
+
log.write("error", "attention.row", {
|
|
28
|
+
...(project.success ? { projectId: project.data.project_id } : {}),
|
|
29
|
+
detail: `needs_attention left out one of the ${source} it could not read: ${causedBy(error)}`,
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
// Read once for the checkpoints and the stopped runs, when the first of them asks.
|
|
33
|
+
let heldOnce;
|
|
34
|
+
const holds = () => {
|
|
35
|
+
heldOnce ??= projectHolds(db, {
|
|
36
|
+
skip: (value, error) => skip("review checkpoints", value, error),
|
|
37
|
+
});
|
|
38
|
+
return heldOnce;
|
|
39
|
+
};
|
|
40
|
+
const sources = [
|
|
41
|
+
["external reviews", () => externalReviews(db, skip)],
|
|
42
|
+
["flagged verdicts", () => flaggedVerdicts(db, skip)],
|
|
43
|
+
["review checkpoints", () => heldCheckpoints(db, holds())],
|
|
44
|
+
["failed steps", () => failedSteps(db, skip)],
|
|
45
|
+
["paused runs", () => pausedRuns(db, skip)],
|
|
46
|
+
["stopped runs", () => stoppedRuns(db, holds().stopped, skip)],
|
|
47
|
+
["held topics", () => heldTopics(db, skip)],
|
|
48
|
+
["late uploads", () => lateUploads(deps, now)],
|
|
49
|
+
["extension failures", () => extensionFailures(db, skip)],
|
|
50
|
+
];
|
|
51
|
+
const aside = setAsideProjects(db);
|
|
52
|
+
return sources
|
|
53
|
+
.flatMap(([source, entries]) => {
|
|
54
|
+
try {
|
|
55
|
+
return entries();
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
log.write("error", "attention.source", {
|
|
59
|
+
detail: `needs_attention left out all ${source} because reading them failed: ${causedBy(error)}`,
|
|
60
|
+
});
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
.filter((entry) => entry.projectId === null || !aside.has(entry.projectId))
|
|
65
|
+
.toSorted((a, b) => a.since.localeCompare(b.since));
|
|
66
|
+
}
|
|
67
|
+
function titles(db) {
|
|
68
|
+
return new Map(db
|
|
69
|
+
.prepare("SELECT id, title FROM projects")
|
|
70
|
+
.all()
|
|
71
|
+
.map((row) => {
|
|
72
|
+
const parsed = z.object({ id: z.string(), title: z.string() }).parse(row);
|
|
73
|
+
return [parsed.id, parsed.title];
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
function externalReviews(db, skip) {
|
|
77
|
+
const named = titles(db);
|
|
78
|
+
return rowsOf(waitingExternalReviewRows(db), "external reviews", skip, (value) => {
|
|
79
|
+
const review = externalReviewOf(value);
|
|
80
|
+
return [
|
|
81
|
+
{
|
|
82
|
+
kind: "external-review",
|
|
83
|
+
since: review.waitingSince,
|
|
84
|
+
projectId: review.projectId,
|
|
85
|
+
title: named.get(review.projectId) ?? "",
|
|
86
|
+
stage: review.stage,
|
|
87
|
+
itemKey: review.itemKey,
|
|
88
|
+
reason: `Waiting for an agent's verdict; it passes without review at ${review.deadlineAt}.`,
|
|
89
|
+
autoRetryInFlight: false,
|
|
90
|
+
agentCanAct: true,
|
|
91
|
+
ids: { id: review.projectId, reviewId: review.id },
|
|
92
|
+
tools: ["get_external_review", "submit_review_verdict"],
|
|
93
|
+
},
|
|
94
|
+
];
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
const verdictRow = z.object({
|
|
98
|
+
id: z.string(),
|
|
99
|
+
project_id: z.string(),
|
|
100
|
+
title: z.string(),
|
|
101
|
+
stage: z.string(),
|
|
102
|
+
item_key: z.string(),
|
|
103
|
+
reasons: z.string(),
|
|
104
|
+
created_at: z.string(),
|
|
105
|
+
redo_state: z.enum(["pending", "started", "failed"]).nullable(),
|
|
106
|
+
redo_error: z.string().nullable(),
|
|
107
|
+
head: z.string(),
|
|
108
|
+
});
|
|
109
|
+
// Each item's latest failed verdict nobody has acted on, in the current version.
|
|
110
|
+
function flaggedVerdicts(db, skip) {
|
|
111
|
+
const rows = db
|
|
112
|
+
.prepare(`SELECT v.id, v.project_id, p.title, v.stage, v.item_key, v.reasons, v.created_at, v.redo_state, v.redo_error, h.revision_id AS head
|
|
113
|
+
FROM review_verdicts v
|
|
114
|
+
JOIN projects p ON p.id=v.project_id
|
|
115
|
+
JOIN project_heads h ON h.project_id=v.project_id AND h.revision_id=v.revision_id
|
|
116
|
+
WHERE v.passed=0 AND v.action IS NULL AND ${liveProject("v", "project_id")}
|
|
117
|
+
AND NOT EXISTS (SELECT 1 FROM review_verdicts n WHERE n.project_id=v.project_id
|
|
118
|
+
AND n.item_key=v.item_key AND (n.created_at>v.created_at OR (n.created_at=v.created_at AND n.rowid>v.rowid)))`)
|
|
119
|
+
.all();
|
|
120
|
+
return rowsOf(rows, "flagged verdicts", skip, (value) => {
|
|
121
|
+
const row = verdictRow.parse(value);
|
|
122
|
+
const reasons = z.array(z.string()).parse(JSON.parse(row.reasons));
|
|
123
|
+
const redoing = row.redo_state === "pending" || row.redo_state === "started";
|
|
124
|
+
return [
|
|
125
|
+
{
|
|
126
|
+
kind: "flagged-verdict",
|
|
127
|
+
since: row.created_at,
|
|
128
|
+
projectId: row.project_id,
|
|
129
|
+
title: row.title,
|
|
130
|
+
stage: row.stage,
|
|
131
|
+
itemKey: row.item_key,
|
|
132
|
+
reason: row.redo_state === "failed" && row.redo_error !== null
|
|
133
|
+
? row.redo_error
|
|
134
|
+
: reasons.join(" "),
|
|
135
|
+
autoRetryInFlight: redoing,
|
|
136
|
+
agentCanAct: !redoing,
|
|
137
|
+
ids: { id: row.project_id, verdictId: row.id, baseRevisionId: row.head },
|
|
138
|
+
tools: ["list_reviews", "overrule_review", "redo_review"],
|
|
139
|
+
},
|
|
140
|
+
];
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
// Only a checkpoint that holds the run for the person (`reviews/holds.ts`): one whose stage's
|
|
144
|
+
// inputs aren't made yet holds nothing, and a Before Video hold a schedule added waits for its
|
|
145
|
+
// scheduled day, not for anyone.
|
|
146
|
+
function heldCheckpoints(db, { holding }) {
|
|
147
|
+
const named = titles(db);
|
|
148
|
+
return [...holding].flatMap(([projectId, holds]) => holds
|
|
149
|
+
.filter((hold) => hold.waitsFor === "person")
|
|
150
|
+
.map((hold) => ({
|
|
151
|
+
kind: "checkpoint",
|
|
152
|
+
since: hold.since,
|
|
153
|
+
projectId,
|
|
154
|
+
title: named.get(projectId) ?? "",
|
|
155
|
+
stage: hold.stage,
|
|
156
|
+
itemKey: null,
|
|
157
|
+
reason: `Waiting for your review before ${hold.stage}: held until it is approved.`,
|
|
158
|
+
autoRetryInFlight: false,
|
|
159
|
+
agentCanAct: true,
|
|
160
|
+
ids: {
|
|
161
|
+
id: projectId,
|
|
162
|
+
checkpointId: hold.checkpointId,
|
|
163
|
+
revisionId: hold.revisionId,
|
|
164
|
+
fingerprint: hold.fingerprint,
|
|
165
|
+
},
|
|
166
|
+
tools: ["get_checkpoints", "approve_checkpoint"],
|
|
167
|
+
})));
|
|
168
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { readSetting, writeSetting } from "../settings/repo.js";
|
|
3
|
+
// When an agent last opened an MCP session. This install's own fact, so backups leave it out
|
|
4
|
+
// (`storage/portable.ts`).
|
|
5
|
+
export const agentConnectionKey = "agents.lastConnectedAt";
|
|
6
|
+
export function recordAgentConnection(db, at) {
|
|
7
|
+
writeSetting(db, agentConnectionKey, JSON.stringify(at));
|
|
8
|
+
}
|
|
9
|
+
export function lastAgentConnection(db) {
|
|
10
|
+
const raw = readSetting(db, agentConnectionKey);
|
|
11
|
+
if (raw === undefined)
|
|
12
|
+
return null;
|
|
13
|
+
const parsed = z.iso.datetime().safeParse(JSON.parse(raw));
|
|
14
|
+
return parsed.success ? parsed.data : null;
|
|
15
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
// Agents: an AI agent (Claude Code, Codex, ...) driving Slopify through its MCP endpoint.
|
|
3
|
+
// Browser-safe: Settings → Connections → Agents (MCP) builds its setup lines from these.
|
|
4
|
+
export const actors = ["person", "agent"];
|
|
5
|
+
export const mcpPath = "/mcp";
|
|
6
|
+
export function mcpUrl(origin) {
|
|
7
|
+
return `${origin.replace(/\/+$/, "")}${mcpPath}`;
|
|
8
|
+
}
|
|
9
|
+
export function claudeAddCommand(url) {
|
|
10
|
+
return `claude mcp add --transport http slopify ${url}`;
|
|
11
|
+
}
|
|
12
|
+
// Codex reads MCP servers from ~/.codex/config.toml; a `url` makes it a Streamable HTTP one.
|
|
13
|
+
export function codexConfig(url) {
|
|
14
|
+
return `[mcp_servers.slopify]\nurl = "${url}"`;
|
|
15
|
+
}
|
|
16
|
+
const madeBody = z
|
|
17
|
+
.object({ projectIds: z.array(z.string()).optional(), projectId: z.string().optional() })
|
|
18
|
+
.loose();
|
|
19
|
+
// The projects an API answer says it made ({projectId} or {projectIds}), for recording who
|
|
20
|
+
// started them and what an agent changed.
|
|
21
|
+
export function projectsMadeBy(answer) {
|
|
22
|
+
const made = madeBody.safeParse(answer);
|
|
23
|
+
if (!made.success)
|
|
24
|
+
return [];
|
|
25
|
+
return [
|
|
26
|
+
...(made.data.projectIds ?? []),
|
|
27
|
+
...(made.data.projectId === undefined ? [] : [made.data.projectId]),
|
|
28
|
+
];
|
|
29
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { actors } from "./model.js";
|
|
3
|
+
// ceiling: Settings shows the latest twenty; a project's own are in its history.
|
|
4
|
+
export const recentAgentActionsMax = 20;
|
|
5
|
+
const actionRow = z.object({
|
|
6
|
+
id: z.string(),
|
|
7
|
+
project_id: z.string().nullable(),
|
|
8
|
+
schedule_id: z.string().nullable(),
|
|
9
|
+
tool: z.string(),
|
|
10
|
+
title: z.string(),
|
|
11
|
+
revision_id: z.string().nullable(),
|
|
12
|
+
created_at: z.string(),
|
|
13
|
+
});
|
|
14
|
+
function actionOf(value) {
|
|
15
|
+
const row = actionRow.parse(value);
|
|
16
|
+
return {
|
|
17
|
+
id: row.id,
|
|
18
|
+
projectId: row.project_id,
|
|
19
|
+
scheduleId: row.schedule_id,
|
|
20
|
+
tool: row.tool,
|
|
21
|
+
title: row.title,
|
|
22
|
+
revisionId: row.revision_id,
|
|
23
|
+
createdAt: row.created_at,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
// A project the call itself deleted for good is gone: the action is kept without it. A call
|
|
27
|
+
// sent again with its idempotencyKey was answered with its first answer, so it adds no second
|
|
28
|
+
// row (`agent_actions_once`); any other failure to record it is thrown.
|
|
29
|
+
export function recordAgentAction(db, action, idempotencyKey) {
|
|
30
|
+
db.prepare(`INSERT INTO agent_actions(id,project_id,schedule_id,tool,title,revision_id,created_at,idempotency_key)
|
|
31
|
+
VALUES (?,(SELECT id FROM projects WHERE id=?),?,?,?,?,?,?)
|
|
32
|
+
ON CONFLICT(tool,COALESCE(project_id,''),idempotency_key) WHERE idempotency_key IS NOT NULL
|
|
33
|
+
DO NOTHING`).run(action.id, action.projectId, action.scheduleId, action.tool, action.title, action.revisionId, action.createdAt, idempotencyKey ?? null);
|
|
34
|
+
}
|
|
35
|
+
export function agentActionsOf(db, projectId) {
|
|
36
|
+
return db
|
|
37
|
+
.prepare("SELECT * FROM agent_actions WHERE project_id=? ORDER BY created_at DESC, rowid DESC")
|
|
38
|
+
.all(projectId)
|
|
39
|
+
.map(actionOf);
|
|
40
|
+
}
|
|
41
|
+
export function agentRevisionIds(db, projectId) {
|
|
42
|
+
return new Set(db
|
|
43
|
+
.prepare("SELECT DISTINCT revision_id FROM agent_actions WHERE project_id=? AND revision_id IS NOT NULL")
|
|
44
|
+
.all(projectId)
|
|
45
|
+
.map((row) => String(row.revision_id)));
|
|
46
|
+
}
|
|
47
|
+
export function recentAgentActions(db) {
|
|
48
|
+
return db
|
|
49
|
+
.prepare(`SELECT a.*, COALESCE(p.title, s.name) AS subject FROM agent_actions a
|
|
50
|
+
LEFT JOIN projects p ON p.id=a.project_id LEFT JOIN schedules s ON s.id=a.schedule_id
|
|
51
|
+
ORDER BY a.created_at DESC, a.rowid DESC LIMIT ?`)
|
|
52
|
+
.all(recentAgentActionsMax)
|
|
53
|
+
.map((row) => ({
|
|
54
|
+
...actionOf(row),
|
|
55
|
+
subject: z.object({ subject: z.string().nullable() }).parse(row).subject,
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
export function recordStart(db, start) {
|
|
59
|
+
db.prepare(`INSERT INTO project_starts(project_id,actor,started_at)
|
|
60
|
+
SELECT ?,?,? WHERE EXISTS (SELECT 1 FROM projects WHERE id=?)
|
|
61
|
+
ON CONFLICT(project_id) DO UPDATE SET actor=excluded.actor, started_at=excluded.started_at`).run(start.projectId, start.actor, start.at, start.projectId);
|
|
62
|
+
}
|
|
63
|
+
export function lastStarter(db, projectId) {
|
|
64
|
+
const row = db.prepare("SELECT actor FROM project_starts WHERE project_id=?").get(projectId);
|
|
65
|
+
return row === undefined ? null : z.object({ actor: z.enum(actors) }).parse(row).actor;
|
|
66
|
+
}
|
|
@@ -2,9 +2,11 @@ import { z } from "zod";
|
|
|
2
2
|
import { transact } from "../../kernel/db/tx.js";
|
|
3
3
|
import { projectStates, stageKinds, } from "../../kernel/pipeline.js";
|
|
4
4
|
import { derive } from "../../kernel/runner/graph.js";
|
|
5
|
-
import {
|
|
5
|
+
import { projectExists, projectPaused, setProjectPaused, stagesOf } from "../admission/repo.js";
|
|
6
|
+
import { finishStage } from "../admission/stage-writes.js";
|
|
6
7
|
import { withProjectControl } from "../control/lock.js";
|
|
7
8
|
import { checkRevisionControl, rememberRevisionControl, } from "../control/revision-control.js";
|
|
9
|
+
import { stopWaitingReviews } from "../reviews/external-repo.js";
|
|
8
10
|
import { settleScheduleRunsForProject } from "../schedules/repo.js";
|
|
9
11
|
// Cancel on the project header. Every in-flight call of this project is aborted at once, every
|
|
10
12
|
// `done` output and every finished piece is kept for the resume, and the project sits
|
|
@@ -61,6 +63,7 @@ async function cancel(deps, projectId) {
|
|
|
61
63
|
deps.db
|
|
62
64
|
.prepare("UPDATE review_checkpoints SET state='canceled',approved_at=NULL WHERE project_id=? AND revision_id=(SELECT revision_id FROM project_heads WHERE project_id=?) AND state IN ('configured','pending-review','held')")
|
|
63
65
|
.run(projectId, projectId);
|
|
66
|
+
stopWaitingReviews(deps.db, projectId);
|
|
64
67
|
deps.settleCheckpoints(projectId);
|
|
65
68
|
// The invariant: after cancel commits no stage of the project is `running`.
|
|
66
69
|
// Usually the aborted runner already wrote these rows; the sweep covers a failed
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { transact } from "../../kernel/db/tx.js";
|
|
3
3
|
import { derive, satisfied } from "../../kernel/runner/graph.js";
|
|
4
|
-
import {
|
|
4
|
+
import { projectById, setProjectPaused, stagesOf, updateProjectConfig } from "../admission/repo.js";
|
|
5
|
+
import { finishStage, resetStage } from "../admission/stage-writes.js";
|
|
5
6
|
import { listCheckpoints } from "../checkpoints/repo.js";
|
|
6
7
|
import { sameChunking } from "../narration/chunk.js";
|
|
7
8
|
import { projectStandings } from "../rebuild/runtime-store.js";
|
|
@@ -3,6 +3,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
3
3
|
import { projectById } from "../admission/repo.js";
|
|
4
4
|
import { castMentions } from "../channels/cast-match.js";
|
|
5
5
|
import { castOfChannel, projectChannelId } from "../channels/repo.js";
|
|
6
|
+
import { reviewedByAgent } from "../reviews/model.js";
|
|
6
7
|
import { outputPath } from "../storage/layout.js";
|
|
7
8
|
import { outputsOf } from "../storage/repo.js";
|
|
8
9
|
import { episodeMemoryOn, memoryOfProject, saveGeneratedMemory } from "./repo.js";
|
|
@@ -115,7 +116,7 @@ async function summarize(deps, projectId, signal) {
|
|
|
115
116
|
function llmOf(config) {
|
|
116
117
|
if (config.llm !== undefined)
|
|
117
118
|
return config.llm;
|
|
118
|
-
return config.reviews === undefined
|
|
119
|
+
return config.reviews === undefined || reviewedByAgent(config.reviews)
|
|
119
120
|
? undefined
|
|
120
121
|
: { provider: config.reviews.provider, model: config.reviews.model };
|
|
121
122
|
}
|