@gentbajko/slopify 0.8.3 → 1.0.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/README.md +28 -6
- package/SUBTITLES.md +2 -0
- package/dist/adapters/alignment/window.js +46 -5
- package/dist/adapters/alignment/worker.js +2 -2
- package/dist/adapters/llm/claude-code.js +6 -3
- package/dist/adapters/llm/codex.js +102 -12
- package/dist/adapters/llm/gemini.js +11 -8
- package/dist/adapters/llm/run-cli.js +61 -6
- package/dist/adapters/tts/inworld-async.js +4 -4
- package/dist/adapters/tts/inworld.js +73 -68
- package/dist/catalog/registry.js +13 -73
- package/dist/edge/cli.js +5 -2
- package/dist/edge/events/hub.js +7 -2
- package/dist/edge/events/preview-cache.js +7 -4
- package/dist/edge/events/visibility.js +33 -0
- package/dist/edge/http/actions.js +74 -79
- package/dist/edge/http/app.js +39 -6
- package/dist/edge/http/audio-preview.js +9 -1
- package/dist/edge/http/checkpoints.js +95 -0
- package/dist/edge/http/diagnostics.js +31 -0
- package/dist/edge/http/draft-files.js +69 -0
- package/dist/edge/http/draft-problem.js +15 -0
- package/dist/edge/http/drafts.js +95 -0
- package/dist/edge/http/multipart.js +86 -0
- package/dist/edge/http/mutations.js +49 -0
- package/dist/edge/http/planning.js +12 -1
- package/dist/edge/http/project-templates.js +74 -0
- package/dist/edge/http/projects.js +19 -2
- package/dist/edge/http/revision-files.js +84 -0
- package/dist/edge/http/revisions.js +103 -0
- package/dist/edge/http/schedules.js +100 -0
- package/dist/edge/http/staging.js +11 -52
- package/dist/edge/http/storage.js +110 -0
- package/dist/edge/http/subtitles.js +9 -79
- package/dist/edge/http/tutorial.js +26 -0
- package/dist/edge/signal-shutdown.js +23 -0
- package/dist/kernel/audio-preview.js +11 -5
- package/dist/kernel/db/migrations/0004-project-revisions.sql +91 -0
- package/dist/kernel/db/migrations/0005-revision-work.sql +103 -0
- package/dist/kernel/db/migrations/0006-play-drafts.sql +38 -0
- package/dist/kernel/db/migrations/0007-review-checkpoints.sql +28 -0
- package/dist/kernel/db/migrations/0008-project-templates.sql +21 -0
- package/dist/kernel/db/migrations/0009-scheduled-jobs.sql +37 -0
- package/dist/kernel/db/migrations/0010-retain-schedule-history.sql +62 -0
- package/dist/kernel/ports/model.js +5 -0
- package/dist/kernel/runner/attempt-repo.js +15 -0
- package/dist/kernel/runner/attempt.js +14 -2
- package/dist/kernel/runner/checkpoint-authority.js +23 -0
- package/dist/kernel/runner/index.js +35 -11
- package/dist/kernel/runner/providers.js +18 -8
- package/dist/kernel/runner/work-attempt.js +47 -0
- package/dist/kernel/runner/work-authority.js +37 -0
- package/dist/kernel/runner/work.js +23 -0
- package/dist/main.js +227 -69
- package/dist/slices/admission/repo.js +5 -56
- package/dist/slices/admission/schema.js +64 -0
- package/dist/slices/admission/start.js +13 -4
- package/dist/slices/article/continuation.js +9 -3
- package/dist/slices/article/provided-entries.js +7 -3
- package/dist/slices/article/run.js +20 -7
- package/dist/slices/article/segments.js +13 -4
- package/dist/slices/batch/index.js +8 -11
- package/dist/slices/cancel/index.js +78 -34
- package/dist/slices/checkpoints/change.js +250 -0
- package/dist/slices/checkpoints/fingerprint.js +33 -0
- package/dist/slices/checkpoints/index.js +2 -0
- package/dist/slices/checkpoints/model.js +1 -0
- package/dist/slices/checkpoints/recovery.js +116 -0
- package/dist/slices/checkpoints/repo.js +132 -0
- package/dist/slices/checkpoints/rules.js +30 -0
- package/dist/slices/checkpoints/schema.js +59 -0
- package/dist/slices/control/index.js +88 -22
- package/dist/slices/control/providers.js +3 -2
- package/dist/slices/control/revision-control-schema.js +7 -0
- package/dist/slices/control/revision-control.js +59 -0
- package/dist/slices/estimate/index.js +77 -59
- package/dist/slices/estimate/requests.js +102 -0
- package/dist/slices/images/run.js +14 -3
- package/dist/slices/library/slots.js +11 -11
- package/dist/slices/library/snapshot.js +33 -0
- package/dist/slices/narration/legacy-plan.js +58 -0
- package/dist/slices/narration/live.js +2 -2
- package/dist/slices/narration/plan.js +48 -54
- package/dist/slices/narration/run.js +21 -8
- package/dist/slices/play-drafts/convert.js +120 -0
- package/dist/slices/play-drafts/model.js +1 -0
- package/dist/slices/play-drafts/readiness.js +80 -0
- package/dist/slices/play-drafts/repo.js +76 -0
- package/dist/slices/play-drafts/review-inputs.js +150 -0
- package/dist/slices/play-drafts/review.js +109 -0
- package/dist/slices/play-drafts/schema.js +218 -0
- package/dist/slices/play-drafts/service.js +242 -0
- package/dist/slices/play-drafts/start-repo.js +113 -0
- package/dist/slices/play-drafts/start.js +148 -0
- package/dist/slices/play-drafts/uploads.js +67 -0
- package/dist/slices/project-templates/from-project.js +159 -0
- package/dist/slices/project-templates/model.js +1 -0
- package/dist/slices/project-templates/repo.js +41 -0
- package/dist/slices/project-templates/schema.js +37 -0
- package/dist/slices/project-templates/service.js +136 -0
- package/dist/slices/project-templates/setup.js +55 -0
- package/dist/slices/rebuild/admission-repo.js +120 -0
- package/dist/slices/rebuild/dependencies.js +33 -0
- package/dist/slices/rebuild/model.js +94 -0
- package/dist/slices/rebuild/narration-history.js +43 -0
- package/dist/slices/rebuild/narration-reuse.js +20 -0
- package/dist/slices/rebuild/preview-details.js +144 -0
- package/dist/slices/rebuild/preview-plan.js +223 -0
- package/dist/slices/rebuild/preview-retained.js +128 -0
- package/dist/slices/rebuild/provided-review.js +32 -0
- package/dist/slices/rebuild/recipe-audio.js +172 -0
- package/dist/slices/rebuild/recipe-build.js +17 -0
- package/dist/slices/rebuild/recipe-checkpoints.js +75 -0
- package/dist/slices/rebuild/recipe-exports.js +65 -0
- package/dist/slices/rebuild/recipe-fixture.js +150 -0
- package/dist/slices/rebuild/recipe-input-schema.js +82 -0
- package/dist/slices/rebuild/recipe-legacy.js +135 -0
- package/dist/slices/rebuild/recipe-model.js +77 -0
- package/dist/slices/rebuild/recipe-provider-choice.js +14 -0
- package/dist/slices/rebuild/recipe-save.js +221 -0
- package/dist/slices/rebuild/recipe-text.js +170 -0
- package/dist/slices/rebuild/recipe-validation.js +86 -0
- package/dist/slices/rebuild/recipe-visual.js +111 -0
- package/dist/slices/rebuild/recipe-work.js +212 -0
- package/dist/slices/rebuild/recipes.js +4 -0
- package/dist/slices/rebuild/repo.js +64 -0
- package/dist/slices/rebuild/runtime-actions.js +76 -0
- package/dist/slices/rebuild/runtime-admission.js +71 -0
- package/dist/slices/rebuild/runtime-article.js +117 -0
- package/dist/slices/rebuild/runtime-checkpoints.js +45 -0
- package/dist/slices/rebuild/runtime-export-inputs.js +109 -0
- package/dist/slices/rebuild/runtime-export.js +139 -0
- package/dist/slices/rebuild/runtime-instructions.js +51 -0
- package/dist/slices/rebuild/runtime-local.js +137 -0
- package/dist/slices/rebuild/runtime-materialize.js +105 -0
- package/dist/slices/rebuild/runtime-narration-reuse.js +134 -0
- package/dist/slices/rebuild/runtime-plan.js +103 -0
- package/dist/slices/rebuild/runtime-provider.js +184 -0
- package/dist/slices/rebuild/runtime-publication.js +172 -0
- package/dist/slices/rebuild/runtime-run.js +35 -0
- package/dist/slices/rebuild/runtime-store.js +166 -0
- package/dist/slices/rebuild/runtime-subtitles.js +202 -0
- package/dist/slices/rebuild/service-readiness.js +110 -0
- package/dist/slices/rebuild/service.js +149 -0
- package/dist/slices/rebuild/transition-repo.js +108 -0
- package/dist/slices/rebuild/work-records.js +44 -0
- package/dist/slices/reruns/index.js +5 -0
- package/dist/slices/research/run.js +33 -12
- package/dist/slices/revisions/adopt-assets.js +31 -0
- package/dist/slices/revisions/adopt-content.js +112 -0
- package/dist/slices/revisions/adopt.js +100 -0
- package/dist/slices/revisions/downloads.js +80 -0
- package/dist/slices/revisions/index.js +5 -0
- package/dist/slices/revisions/manifest-repo.js +125 -0
- package/dist/slices/revisions/model.js +1 -0
- package/dist/slices/revisions/mutation-assets.js +143 -0
- package/dist/slices/revisions/mutation-cues.js +95 -0
- package/dist/slices/revisions/mutation-prepare.js +182 -0
- package/dist/slices/revisions/mutation-request.js +58 -0
- package/dist/slices/revisions/mutation-work.js +47 -0
- package/dist/slices/revisions/mutations.js +194 -0
- package/dist/slices/revisions/projection.js +82 -0
- package/dist/slices/revisions/publication-model.js +1 -0
- package/dist/slices/revisions/publication-rules.js +166 -0
- package/dist/slices/revisions/publish.js +115 -0
- package/dist/slices/revisions/repo.js +83 -0
- package/dist/slices/revisions/restore.js +60 -0
- package/dist/slices/revisions/rules.js +99 -0
- package/dist/slices/revisions/schema.js +160 -0
- package/dist/slices/revisions/view.js +44 -0
- package/dist/slices/schedules/calendar.js +47 -0
- package/dist/slices/schedules/model.js +1 -0
- package/dist/slices/schedules/repo.js +281 -0
- package/dist/slices/schedules/scheduler.js +144 -0
- package/dist/slices/schedules/schema.js +80 -0
- package/dist/slices/schedules/service.js +178 -0
- package/dist/slices/settings/cli-paths.js +1 -1
- package/dist/slices/settings/cli-status.js +45 -5
- package/dist/slices/settings/tutorial-schema.js +39 -0
- package/dist/slices/settings/tutorial.js +66 -0
- package/dist/slices/storage/assets.js +108 -0
- package/dist/slices/storage/delete-project.js +2 -1
- package/dist/slices/storage/downloads.js +4 -1
- package/dist/slices/storage/portable.js +871 -0
- package/dist/slices/storage/prepare.js +67 -0
- package/dist/slices/storage/reconcile.js +25 -5
- package/dist/slices/storage/repo.js +2 -15
- package/dist/slices/storage/schema.js +35 -0
- package/dist/slices/storage/staging-refs.js +26 -0
- package/dist/slices/storage/staging.js +16 -11
- package/dist/slices/thumbnail/run.js +26 -9
- package/dist/slices/video/audio-export-args.js +33 -0
- package/dist/slices/video/audio-export.js +13 -36
- package/dist/slices/video/audio-inputs.js +4 -3
- package/dist/slices/video/ffmpeg.js +6 -0
- package/dist/slices/video/reuse-audio.js +11 -4
- package/dist/updater/install-flow.js +27 -2
- package/dist/updater/plan.js +28 -2
- package/dist/updater/service.js +27 -2
- package/dist/updater/worker.js +2 -1
- package/dist/web/assets/index-CO7DnVX9.js +137 -0
- package/dist/web/assets/index-DX1_chKs.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +2 -2
- package/dist/adapters/tts/inworld-text.js +0 -32
- package/dist/web/assets/index-Ce2HApus.css +0 -1
- package/dist/web/assets/index-xbD0MZOY.js +0 -130
package/dist/main.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { randomBytes } from "node:crypto";
|
|
1
|
+
import { randomBytes, randomUUID } from "node:crypto";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { serve } from "@hono/node-server";
|
|
@@ -10,37 +10,45 @@ import { nodeRunCli } from "./adapters/llm/run-cli.js";
|
|
|
10
10
|
import { curateRegistry } from "./catalog/registry.js";
|
|
11
11
|
import { createCatalogueStore } from "./catalog/store.js";
|
|
12
12
|
import { createHub } from "./edge/events/hub.js";
|
|
13
|
+
import { currentProjectEvent } from "./edge/events/visibility.js";
|
|
13
14
|
import { createApp } from "./edge/http/app.js";
|
|
15
|
+
import { createMutationLifecycle, drainMutationsWithDeadline } from "./edge/http/mutations.js";
|
|
14
16
|
import { openFolder } from "./edge/open-folder.js";
|
|
15
17
|
import { createAudioPreviewStore } from "./kernel/audio-preview.js";
|
|
16
18
|
import { systemClock } from "./kernel/clock.js";
|
|
17
19
|
import { openDb } from "./kernel/db/index.js";
|
|
18
20
|
import { migrate } from "./kernel/db/migrate.js";
|
|
21
|
+
import { transact } from "./kernel/db/tx.js";
|
|
19
22
|
import { ulidIds } from "./kernel/ids.js";
|
|
20
23
|
import { acquireInstanceLock } from "./kernel/lock.js";
|
|
21
24
|
import { openLog } from "./kernel/log.js";
|
|
22
25
|
import { ensureDirs, layout } from "./kernel/paths.js";
|
|
23
26
|
import { sqliteAttempts } from "./kernel/runner/attempt-repo.js";
|
|
24
|
-
import {
|
|
27
|
+
import { createCheckpointAuthority, } from "./kernel/runner/checkpoint-authority.js";
|
|
25
28
|
import { createRunner } from "./kernel/runner/index.js";
|
|
26
29
|
import { stageProviders } from "./kernel/runner/providers.js";
|
|
27
30
|
import { createProviderQueue } from "./kernel/runner/queue.js";
|
|
28
31
|
import { readVersion } from "./kernel/version.js";
|
|
29
32
|
import { modelSources } from "./model-catalog.js";
|
|
30
|
-
import {
|
|
31
|
-
import { prepareProvidedArticleSegments } from "./slices/article/provided-entries.js";
|
|
32
|
-
import { runArticle } from "./slices/article/run.js";
|
|
33
|
+
import { projectPaused } from "./slices/admission/repo.js";
|
|
33
34
|
import { pumpQueue, queueWaiting } from "./slices/batch/index.js";
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
36
|
-
import {
|
|
35
|
+
import { approveCheckpoint } from "./slices/checkpoints/index.js";
|
|
36
|
+
import { checkpointDecisionForWork, recoverCheckpointWork, settleReleasedCheckpoints, } from "./slices/checkpoints/recovery.js";
|
|
37
|
+
import { resolveFont } from "./slices/fonts/index.js";
|
|
38
|
+
import { templateById } from "./slices/project-templates/repo.js";
|
|
39
|
+
import { claimWork, finishWork, maySubmit } from "./slices/rebuild/repo.js";
|
|
40
|
+
import { materializeAdmittedWork } from "./slices/rebuild/runtime-materialize.js";
|
|
41
|
+
import { runRevisionInvocation } from "./slices/rebuild/runtime-run.js";
|
|
42
|
+
import { executionStages, executionStandings, invocationReady, projectStandings, recordWorkProgress, } from "./slices/rebuild/runtime-store.js";
|
|
43
|
+
import { settleTerminalScheduleRuns } from "./slices/schedules/repo.js";
|
|
44
|
+
import { createScheduleRunner } from "./slices/schedules/scheduler.js";
|
|
37
45
|
import { nodeCliProbe } from "./slices/settings/cli-status.js";
|
|
46
|
+
import { providerStatuses } from "./slices/settings/readiness.js";
|
|
38
47
|
import { reconcileStorage } from "./slices/storage/reconcile.js";
|
|
39
48
|
import { collectorEndpoint, httpPostEvents } from "./slices/telemetry/collector-client.js";
|
|
40
49
|
import { createFlusher } from "./slices/telemetry/flush.js";
|
|
41
50
|
import { record } from "./slices/telemetry/record.js";
|
|
42
|
-
import {
|
|
43
|
-
import { renderVideo } from "./slices/video/run.js";
|
|
51
|
+
import { probeDurationMs } from "./slices/video/ffmpeg.js";
|
|
44
52
|
import { watchActivation } from "./updater/candidate.js";
|
|
45
53
|
import { launchUpdate } from "./updater/install.js";
|
|
46
54
|
import { isUpdateToken } from "./updater/model.js";
|
|
@@ -52,10 +60,58 @@ import { createUpdater } from "./updater/service.js";
|
|
|
52
60
|
// stay queued.
|
|
53
61
|
const flushDelayMs = 1000;
|
|
54
62
|
const collectorTimeoutMs = 10_000;
|
|
63
|
+
const mutationDrainTimeoutMs = 5_000;
|
|
64
|
+
export function createScheduleTickLifecycle(deps) {
|
|
65
|
+
let stopping = false;
|
|
66
|
+
let inflight;
|
|
67
|
+
const tick = () => {
|
|
68
|
+
if (stopping)
|
|
69
|
+
return Promise.resolve();
|
|
70
|
+
if (inflight !== undefined)
|
|
71
|
+
return inflight;
|
|
72
|
+
const release = deps.beginMutation();
|
|
73
|
+
if (release === undefined)
|
|
74
|
+
return Promise.resolve();
|
|
75
|
+
const running = (async () => {
|
|
76
|
+
try {
|
|
77
|
+
await deps.tick();
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
try {
|
|
81
|
+
deps.report();
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// A logging failure must not turn a timer callback into an unhandled rejection.
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
release();
|
|
89
|
+
}
|
|
90
|
+
})();
|
|
91
|
+
inflight = running;
|
|
92
|
+
void running.then(() => {
|
|
93
|
+
if (inflight === running)
|
|
94
|
+
inflight = undefined;
|
|
95
|
+
}, () => {
|
|
96
|
+
if (inflight === running)
|
|
97
|
+
inflight = undefined;
|
|
98
|
+
});
|
|
99
|
+
return running;
|
|
100
|
+
};
|
|
101
|
+
return {
|
|
102
|
+
tick,
|
|
103
|
+
stop: () => {
|
|
104
|
+
stopping = true;
|
|
105
|
+
return inflight ?? Promise.resolve();
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
55
109
|
export async function boot(config) {
|
|
56
110
|
const clock = systemClock;
|
|
57
111
|
const ids = ulidIds;
|
|
58
112
|
const paths = layout(config.dataDir);
|
|
113
|
+
const candidateToken = process.env.SLOPIFY_UPDATE_TOKEN ?? "";
|
|
114
|
+
const pendingActivation = isUpdateToken(candidateToken) && process.env.SLOPIFY_UPDATE_PENDING === "1";
|
|
59
115
|
ensureDirs(paths, { mode: 0o700 });
|
|
60
116
|
const lock = acquireInstanceLock(paths.lock);
|
|
61
117
|
let db;
|
|
@@ -68,13 +124,26 @@ export async function boot(config) {
|
|
|
68
124
|
});
|
|
69
125
|
db = openDb(paths.db);
|
|
70
126
|
migrate(db, clock);
|
|
127
|
+
const runtimeDb = db;
|
|
71
128
|
const interrupted = markInterruptedStages(db, clock);
|
|
72
|
-
|
|
129
|
+
recoverCheckpointWork(db);
|
|
130
|
+
const settledSchedules = settleTerminalScheduleRuns(db, clock.now().toISOString());
|
|
131
|
+
// The updater can restore the database after a failed candidate boot, but it cannot
|
|
132
|
+
// restore files deleted by reconciliation. Leave the filesystem untouched until the
|
|
133
|
+
// candidate's committed activation pointer has been verified.
|
|
134
|
+
const reconciled = pendingActivation ? undefined : reconcileStorage(db, paths);
|
|
73
135
|
const log = openLog(paths.logs, clock);
|
|
74
136
|
log.write("info", "boot", {
|
|
75
|
-
detail:
|
|
137
|
+
detail: reconciled === undefined
|
|
138
|
+
? `interrupted stages ${interrupted}, settled schedules ${settledSchedules}, storage reconciliation deferred during update activation`
|
|
139
|
+
: `interrupted stages ${interrupted}, settled schedules ${settledSchedules}, orphan files ${reconciled.orphanFiles}, staged files ${reconciled.stagedFiles}`,
|
|
140
|
+
});
|
|
141
|
+
const eventDb = db;
|
|
142
|
+
const hub = createHub({
|
|
143
|
+
ids,
|
|
144
|
+
log,
|
|
145
|
+
acceptEvent: (event) => currentProjectEvent(eventDb, event),
|
|
76
146
|
});
|
|
77
|
-
const hub = createHub({ ids, log });
|
|
78
147
|
const version = readVersion();
|
|
79
148
|
const telemetry = { db, ids, clock, log, appVersion: version };
|
|
80
149
|
const flusher = createFlusher({
|
|
@@ -92,7 +161,7 @@ export async function boot(config) {
|
|
|
92
161
|
probe: nodeCliProbe,
|
|
93
162
|
}), catalogue);
|
|
94
163
|
const audioPreviews = createAudioPreviewStore();
|
|
95
|
-
const runner =
|
|
164
|
+
const runner = wireRunner({
|
|
96
165
|
db,
|
|
97
166
|
paths,
|
|
98
167
|
clock,
|
|
@@ -122,8 +191,6 @@ export async function boot(config) {
|
|
|
122
191
|
throw new Error("The server is not ready.");
|
|
123
192
|
};
|
|
124
193
|
let listeningPort = config.port;
|
|
125
|
-
const candidateToken = process.env.SLOPIFY_UPDATE_TOKEN ?? "";
|
|
126
|
-
const pendingActivation = isUpdateToken(candidateToken) && process.env.SLOPIFY_UPDATE_PENDING === "1";
|
|
127
194
|
const updater = createUpdater({
|
|
128
195
|
...(isUpdateToken(candidateToken)
|
|
129
196
|
? {
|
|
@@ -131,6 +198,21 @@ export async function boot(config) {
|
|
|
131
198
|
token: candidateToken,
|
|
132
199
|
pending: pendingActivation,
|
|
133
200
|
committed: () => updateCommitted(paths.dataDir, version, candidateToken),
|
|
201
|
+
settle: () => {
|
|
202
|
+
try {
|
|
203
|
+
const settled = reconcileStorage(updateDb, paths);
|
|
204
|
+
log.write("info", "update", {
|
|
205
|
+
detail: `activation storage reconciliation removed ${settled.orphanFiles} orphan files and ${settled.stagedFiles} staged files`,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
// The pointer is already committed, so storage maintenance can no longer
|
|
210
|
+
// safely reject or roll back this candidate. A normal restart retries it.
|
|
211
|
+
log.write("error", "update", {
|
|
212
|
+
detail: "Storage reconciliation failed after update activation.",
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
},
|
|
134
216
|
},
|
|
135
217
|
}
|
|
136
218
|
: {}),
|
|
@@ -169,13 +251,64 @@ export async function boot(config) {
|
|
|
169
251
|
});
|
|
170
252
|
},
|
|
171
253
|
});
|
|
254
|
+
const rebuild = {
|
|
255
|
+
db,
|
|
256
|
+
paths,
|
|
257
|
+
ids,
|
|
258
|
+
clock,
|
|
259
|
+
log,
|
|
260
|
+
runner,
|
|
261
|
+
catalogue,
|
|
262
|
+
measureAudio: (path, signal) => probeDurationMs(ffmpeg, path, signal ?? AbortSignal.timeout(30_000), log),
|
|
263
|
+
providers: () => providerStatuses({ db: updateDb, probe: nodeCliProbe }),
|
|
264
|
+
modelsFor: modelSources(registry).modelsFor,
|
|
265
|
+
emit: (projectId, event) => hub.emit(projectId, event),
|
|
266
|
+
};
|
|
267
|
+
const draftDeps = {
|
|
268
|
+
db,
|
|
269
|
+
paths,
|
|
270
|
+
ids,
|
|
271
|
+
clock,
|
|
272
|
+
log,
|
|
273
|
+
runner,
|
|
274
|
+
catalogue,
|
|
275
|
+
uuid: randomUUID,
|
|
276
|
+
resolveFont: (fontId) => resolveFont(paths, fontId),
|
|
277
|
+
providers: rebuild.providers,
|
|
278
|
+
modelsFor: rebuild.modelsFor,
|
|
279
|
+
emit: (event) => hub.emitGlobal(event),
|
|
280
|
+
recordStarted: (projectIds) => {
|
|
281
|
+
for (const _projectId of projectIds)
|
|
282
|
+
record(telemetry, "project.created", {});
|
|
283
|
+
flusher.soon();
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
const scheduleDeps = {
|
|
287
|
+
...draftDeps,
|
|
288
|
+
template: (id, templateVersion) => templateById(runtimeDb, id, templateVersion),
|
|
289
|
+
};
|
|
290
|
+
const scheduleRunner = createScheduleRunner(scheduleDeps);
|
|
291
|
+
scheduleRunner.recover(clock.now());
|
|
292
|
+
const scheduleTicks = createScheduleTickLifecycle({
|
|
293
|
+
beginMutation: updater.beginMutation,
|
|
294
|
+
tick: scheduleRunner.tick,
|
|
295
|
+
report: () => log.write("error", "schedule.tick", {
|
|
296
|
+
detail: "Scheduled jobs could not be advanced.",
|
|
297
|
+
}),
|
|
298
|
+
});
|
|
299
|
+
const mutations = createMutationLifecycle();
|
|
172
300
|
const app = createApp({
|
|
301
|
+
rebuild,
|
|
302
|
+
drafts: draftDeps,
|
|
303
|
+
schedules: scheduleDeps,
|
|
304
|
+
...(rebuild.measureAudio === undefined ? {} : { measureAudio: rebuild.measureAudio }),
|
|
173
305
|
openFolder,
|
|
174
306
|
db,
|
|
175
307
|
paths,
|
|
176
308
|
hub,
|
|
177
309
|
runner,
|
|
178
310
|
updater,
|
|
311
|
+
mutations,
|
|
179
312
|
audioPreviews,
|
|
180
313
|
...modelSources(registry),
|
|
181
314
|
catalogue,
|
|
@@ -202,6 +335,10 @@ export async function boot(config) {
|
|
|
202
335
|
release();
|
|
203
336
|
}
|
|
204
337
|
}, 1000);
|
|
338
|
+
const scheduleTimer = setInterval(() => {
|
|
339
|
+
void scheduleTicks.tick();
|
|
340
|
+
}, 15_000);
|
|
341
|
+
void scheduleTicks.tick();
|
|
205
342
|
listeningPort = portOf(server) ?? config.port;
|
|
206
343
|
// Whatever last run left queued goes out at start. Nothing waits for
|
|
207
344
|
// it, and an unreachable collector costs one refused socket.
|
|
@@ -212,25 +349,34 @@ export async function boot(config) {
|
|
|
212
349
|
shutdown = () => {
|
|
213
350
|
stopping ??= (async () => {
|
|
214
351
|
clearInterval(queueTimer);
|
|
352
|
+
clearInterval(scheduleTimer);
|
|
353
|
+
const mutationDrain = mutations.stop();
|
|
354
|
+
const scheduleDrain = scheduleTicks.stop();
|
|
355
|
+
const serverClose = beginServerClose(server);
|
|
215
356
|
stopActivation();
|
|
216
357
|
audioPreviews.close();
|
|
217
358
|
try {
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
await close(server);
|
|
359
|
+
// Admission closes before the listener. Requests already admitted keep the
|
|
360
|
+
// database until their response settles; anything racing shutdown receives 503.
|
|
361
|
+
await drainMutationsWithDeadline(mutationDrain, serverClose.terminate, mutationDrainTimeoutMs);
|
|
362
|
+
await scheduleDrain;
|
|
223
363
|
await runner.abortAll();
|
|
224
364
|
}
|
|
225
365
|
finally {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
366
|
+
serverClose.terminate();
|
|
367
|
+
try {
|
|
368
|
+
await serverClose.closed;
|
|
369
|
+
}
|
|
370
|
+
finally {
|
|
371
|
+
// The pending timer is cancelled rather than awaited: a shutdown must not wait
|
|
372
|
+
// on the collector. A flush already in flight may land after the database
|
|
373
|
+
// closes and fail to mark its batch delivered, which costs one re-send that
|
|
374
|
+
// the collector deduplicates by event id.
|
|
375
|
+
flusher.stop();
|
|
376
|
+
log.write("info", "shutdown");
|
|
377
|
+
open.close();
|
|
378
|
+
lock.release();
|
|
379
|
+
}
|
|
234
380
|
}
|
|
235
381
|
})();
|
|
236
382
|
return stopping;
|
|
@@ -245,7 +391,7 @@ export async function boot(config) {
|
|
|
245
391
|
throw error;
|
|
246
392
|
}
|
|
247
393
|
}
|
|
248
|
-
function
|
|
394
|
+
export function wireRunner({ audioPreviews, db, paths, clock, ids, log, hub, telemetry, flusher, registry, catalogue, ffmpeg, }) {
|
|
249
395
|
// A stage counts what it did and the queue is flushed after each new event. `record`
|
|
250
396
|
// swallows its own failures, so this can neither fail a stage nor widen what leaves the
|
|
251
397
|
// machine - the payload allow-list is checked inside it.
|
|
@@ -253,12 +399,7 @@ function wire({ audioPreviews, db, paths, clock, ids, log, hub, telemetry, flush
|
|
|
253
399
|
record(telemetry, type, counters);
|
|
254
400
|
flusher.soon();
|
|
255
401
|
};
|
|
256
|
-
|
|
257
|
-
// the same seven dependencies.
|
|
258
|
-
const video = { db, paths, ids, clock, log, ffmpeg, count, alignSubtitles };
|
|
259
|
-
// Research, the article, the images and the thumbnail all write into the same project
|
|
260
|
-
// folder from the same database handle, so they take the same six dependencies.
|
|
261
|
-
const writing = { db, paths, ids, clock, log, count };
|
|
402
|
+
const execution = { db, paths, ids, clock, log, ffmpeg, alignSubtitles, audioPreviews, count };
|
|
262
403
|
// A stage slice is handed the wrapped calls, never the registry: every provider call
|
|
263
404
|
// it makes is already inside the retry policy (kernel/runner/providers.ts).
|
|
264
405
|
const providers = {
|
|
@@ -268,43 +409,55 @@ function wire({ audioPreviews, db, paths, clock, ids, log, hub, telemetry, flush
|
|
|
268
409
|
log,
|
|
269
410
|
queue: createProviderQueue((provider) => catalogue.read().providers[provider]?.maxConcurrent ?? 1),
|
|
270
411
|
};
|
|
271
|
-
|
|
412
|
+
const checkpoints = createCheckpointAuthority({
|
|
413
|
+
decide: (work) => checkpointDecisionForWork(execution, work),
|
|
414
|
+
approve: (projectId, checkpointId, identity) => approveCheckpoint(db, {
|
|
415
|
+
...identity,
|
|
416
|
+
projectId,
|
|
417
|
+
checkpointId,
|
|
418
|
+
approvedAt: clock.now().toISOString(),
|
|
419
|
+
}),
|
|
420
|
+
inTransaction: () => db.isTransaction,
|
|
421
|
+
wake: (projectId) => runner.tick(projectId),
|
|
422
|
+
log,
|
|
423
|
+
});
|
|
424
|
+
const runner = createRunner({
|
|
425
|
+
checkpoints,
|
|
272
426
|
stages: {
|
|
273
|
-
stagesOf: (projectId) =>
|
|
427
|
+
stagesOf: (projectId) => {
|
|
428
|
+
materializeAdmittedWork(execution, projectId);
|
|
429
|
+
projectStandings(execution, projectId);
|
|
430
|
+
return executionStages(execution, projectId);
|
|
431
|
+
},
|
|
432
|
+
standingsOf: (projectId) => executionStandings(execution, projectId),
|
|
433
|
+
ready: (work) => invocationReady(execution, work),
|
|
274
434
|
paused: (projectId) => projectPaused(db, projectId) || queueWaiting(db, projectId),
|
|
275
|
-
|
|
276
|
-
const
|
|
277
|
-
|
|
435
|
+
claim: (work) => {
|
|
436
|
+
const claimed = claimWork(db, work);
|
|
437
|
+
projectStandings(execution, work.projectId);
|
|
438
|
+
return claimed;
|
|
278
439
|
},
|
|
279
|
-
|
|
280
|
-
finish: (
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
audio: async (context) => {
|
|
288
|
-
const wrapped = stageProviders(providers, context);
|
|
289
|
-
await prepareProvidedArticleSegments(writing, context, wrapped);
|
|
290
|
-
await runNarration({
|
|
291
|
-
...video,
|
|
292
|
-
audioPreviews,
|
|
293
|
-
maxCharacters: (provider, model) => {
|
|
294
|
-
const entry = catalogue.models(provider, "tts").find((m) => m.id === model);
|
|
295
|
-
return entry && "tts" in entry ? entry.tts.maxCharacters : 4000;
|
|
296
|
-
},
|
|
297
|
-
}, context, wrapped);
|
|
440
|
+
maySubmit: (work, pieceId) => maySubmit(db, work, pieceId),
|
|
441
|
+
finish: (work, state, reason) => {
|
|
442
|
+
transact(db, () => {
|
|
443
|
+
finishWork(db, work, state, reason);
|
|
444
|
+
materializeAdmittedWork(execution, work.projectId);
|
|
445
|
+
projectStandings(execution, work.projectId);
|
|
446
|
+
settleReleasedCheckpoints(execution, work.projectId);
|
|
447
|
+
});
|
|
298
448
|
},
|
|
299
|
-
images: (context) => runImages(writing, context, stageProviders(providers, context)),
|
|
300
|
-
thumbnail: (context) => runThumbnail(writing, context, stageProviders(providers, context)),
|
|
301
|
-
video: (context) => renderVideo(video, context),
|
|
302
449
|
},
|
|
450
|
+
runs: Object.fromEntries(["research", "article", "audio", "images", "thumbnail", "video"].map((kind) => [
|
|
451
|
+
kind,
|
|
452
|
+
(context) => runRevisionInvocation(execution, context, stageProviders(providers, context)),
|
|
453
|
+
])),
|
|
303
454
|
// Counting is finer than a stage reaching `done` - each intro and outro text, each
|
|
304
455
|
// narrated segment - and the counters are provider names, token usage and durations
|
|
305
456
|
// only the stage slice ever sees. Each slice records its own units through `count`
|
|
306
457
|
// above, and the runner counts nothing: the kernel may not import a slice.
|
|
307
458
|
emit: (projectId, event) => {
|
|
459
|
+
if (event.type === "stage.progress")
|
|
460
|
+
recordWorkProgress(execution, event);
|
|
308
461
|
hub.emit(projectId, event);
|
|
309
462
|
},
|
|
310
463
|
emitRunningCount: (running) => {
|
|
@@ -312,6 +465,7 @@ function wire({ audioPreviews, db, paths, clock, ids, log, hub, telemetry, flush
|
|
|
312
465
|
},
|
|
313
466
|
log,
|
|
314
467
|
});
|
|
468
|
+
return { ...runner, checkpoints };
|
|
315
469
|
}
|
|
316
470
|
function listen(app, config, log) {
|
|
317
471
|
return new Promise((resolve, reject) => {
|
|
@@ -325,13 +479,10 @@ function listen(app, config, log) {
|
|
|
325
479
|
server.once("error", reject);
|
|
326
480
|
});
|
|
327
481
|
}
|
|
328
|
-
function
|
|
482
|
+
function beginServerClose(server) {
|
|
329
483
|
// An SSE response never ends by itself, and close() waits for every open connection,
|
|
330
|
-
// so
|
|
331
|
-
|
|
332
|
-
server.closeAllConnections();
|
|
333
|
-
}
|
|
334
|
-
return new Promise((resolve, reject) => {
|
|
484
|
+
// so admitted mutations drain first and then the remaining sockets are terminated.
|
|
485
|
+
const closed = new Promise((resolve, reject) => {
|
|
335
486
|
server.close((error) => {
|
|
336
487
|
if (error === undefined || error === null) {
|
|
337
488
|
resolve();
|
|
@@ -340,6 +491,13 @@ function close(server) {
|
|
|
340
491
|
reject(error);
|
|
341
492
|
});
|
|
342
493
|
});
|
|
494
|
+
return {
|
|
495
|
+
closed,
|
|
496
|
+
terminate: () => {
|
|
497
|
+
if ("closeAllConnections" in server)
|
|
498
|
+
server.closeAllConnections();
|
|
499
|
+
},
|
|
500
|
+
};
|
|
343
501
|
}
|
|
344
502
|
function portOf(server) {
|
|
345
503
|
const address = server.address();
|
|
@@ -1,61 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { stageKinds, stageStates } from "../../kernel/pipeline.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
import { entryModes, formats, stageSources } from "./model.js";
|
|
7
|
-
const providerChoice = z.object({
|
|
8
|
-
provider: z.string(),
|
|
9
|
-
model: z.string(),
|
|
10
|
-
thinking: z.enum(thinkingModes).optional(),
|
|
11
|
-
});
|
|
12
|
-
const entryChoice = z.object({ name: z.string(), mode: z.enum(entryModes) });
|
|
13
|
-
// The shape Play posts and the shape `projects.config` holds, in one place: the second
|
|
14
|
-
// is the first plus the rendered prompt texts.
|
|
15
|
-
export const runDraftSchema = z.object({
|
|
16
|
-
title: z.string(),
|
|
17
|
-
format: z.enum(formats),
|
|
18
|
-
// Spelled out rather than built from stageKinds, so the inferred type carries the six
|
|
19
|
-
// keys and this schema stays assignable to the domain type without a cast.
|
|
20
|
-
sources: z.object({
|
|
21
|
-
research: z.enum(stageSources),
|
|
22
|
-
article: z.enum(stageSources),
|
|
23
|
-
audio: z.enum(stageSources),
|
|
24
|
-
images: z.enum(stageSources),
|
|
25
|
-
thumbnail: z.enum(stageSources),
|
|
26
|
-
video: z.enum(stageSources),
|
|
27
|
-
}),
|
|
28
|
-
llm: providerChoice.optional(),
|
|
29
|
-
audio: providerChoice.extend({ voice: z.string() }).optional(),
|
|
30
|
-
images: providerChoice.optional(),
|
|
31
|
-
articlePrompt: z.string().optional(),
|
|
32
|
-
imagePrompts: z.array(z.object({ name: z.string(), number: z.number() })),
|
|
33
|
-
thumbnailPrompt: z.string().optional(),
|
|
34
|
-
intro: entryChoice.optional(),
|
|
35
|
-
outro: entryChoice.optional(),
|
|
36
|
-
values: z.record(z.string(), z.string()),
|
|
37
|
-
provided: z.object({
|
|
38
|
-
research: z.string().optional(),
|
|
39
|
-
article: z.string().optional(),
|
|
40
|
-
audio: z.string().optional(),
|
|
41
|
-
images: z.array(z.string()).optional(),
|
|
42
|
-
thumbnail: z.string().optional(),
|
|
43
|
-
}),
|
|
44
|
-
// Optional until Play carries the control; unknown keys are stripped by this schema, so
|
|
45
|
-
// a mode not listed here would never reach the audio stage.
|
|
46
|
-
chunking: z
|
|
47
|
-
.object({
|
|
48
|
-
mode: z.enum(chunkModes),
|
|
49
|
-
words: z.number().optional(),
|
|
50
|
-
characters: z.number().int().min(1).max(1000000).optional(),
|
|
51
|
-
})
|
|
52
|
-
.optional(),
|
|
53
|
-
silenceGapSeconds: z.number(),
|
|
54
|
-
subtitles: subtitleConfigSchema.optional(),
|
|
55
|
-
});
|
|
56
|
-
export const runConfigSchema = runDraftSchema.extend({
|
|
57
|
-
rendered: z.record(z.string(), z.string()),
|
|
58
|
-
});
|
|
3
|
+
import { formats, stageSources } from "./model.js";
|
|
4
|
+
import { runConfigSchema } from "./schema.js";
|
|
5
|
+
export { runConfigSchema, runDraftSchema } from "./schema.js";
|
|
59
6
|
const projectRow = z.object({
|
|
60
7
|
id: z.string(),
|
|
61
8
|
title: z.string(),
|
|
@@ -182,6 +129,8 @@ export function setProjectPaused(db, projectId, paused, at) {
|
|
|
182
129
|
db.prepare("UPDATE projects SET updated_at = ? WHERE id = ?").run(at, projectId);
|
|
183
130
|
}
|
|
184
131
|
export function updateProjectConfig(db, projectId, config, at) {
|
|
132
|
+
if (db.prepare("SELECT 1 FROM project_heads WHERE project_id=?").get(projectId) !== undefined)
|
|
133
|
+
throw new Error("Versioned project configuration must be saved through revisions.");
|
|
185
134
|
db.prepare("UPDATE projects SET config = ?, updated_at = ? WHERE id = ?").run(JSON.stringify(config), at, projectId);
|
|
186
135
|
}
|
|
187
136
|
// A JSON column never reaches a caller as a string.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { thinkingModes } from "../../kernel/ports/llm.js";
|
|
3
|
+
import { checkpointStageSchema } from "../checkpoints/schema.js";
|
|
4
|
+
import { chunkModes } from "../narration/chunk.js";
|
|
5
|
+
import { subtitleConfigSchema } from "../subtitles/model.js";
|
|
6
|
+
import { entryModes, formats, stageSources } from "./model.js";
|
|
7
|
+
const providerChoice = z.object({
|
|
8
|
+
provider: z.string(),
|
|
9
|
+
model: z.string(),
|
|
10
|
+
thinking: z.enum(thinkingModes).optional(),
|
|
11
|
+
});
|
|
12
|
+
const entryChoice = z.object({ name: z.string(), mode: z.enum(entryModes) });
|
|
13
|
+
// The shape Play posts and the shape `projects.config` holds, in one place: the second
|
|
14
|
+
// is the first plus the rendered prompt texts.
|
|
15
|
+
export const runDraftSchema = z.object({
|
|
16
|
+
checkpoints: z
|
|
17
|
+
.array(checkpointStageSchema)
|
|
18
|
+
.max(3)
|
|
19
|
+
.refine((stages) => new Set(stages).size === stages.length)
|
|
20
|
+
.readonly()
|
|
21
|
+
.optional(),
|
|
22
|
+
title: z.string(),
|
|
23
|
+
format: z.enum(formats),
|
|
24
|
+
// Spelled out rather than built from stageKinds, so the inferred type carries the six
|
|
25
|
+
// keys and this schema stays assignable to the domain type without a cast.
|
|
26
|
+
sources: z.object({
|
|
27
|
+
research: z.enum(stageSources),
|
|
28
|
+
article: z.enum(stageSources),
|
|
29
|
+
audio: z.enum(stageSources),
|
|
30
|
+
images: z.enum(stageSources),
|
|
31
|
+
thumbnail: z.enum(stageSources),
|
|
32
|
+
video: z.enum(stageSources),
|
|
33
|
+
}),
|
|
34
|
+
llm: providerChoice.optional(),
|
|
35
|
+
audio: providerChoice.extend({ voice: z.string() }).optional(),
|
|
36
|
+
images: providerChoice.optional(),
|
|
37
|
+
articlePrompt: z.string().optional(),
|
|
38
|
+
imagePrompts: z.array(z.object({ name: z.string(), number: z.number() })),
|
|
39
|
+
thumbnailPrompt: z.string().optional(),
|
|
40
|
+
intro: entryChoice.optional(),
|
|
41
|
+
outro: entryChoice.optional(),
|
|
42
|
+
values: z.record(z.string(), z.string()),
|
|
43
|
+
provided: z.object({
|
|
44
|
+
research: z.string().optional(),
|
|
45
|
+
article: z.string().optional(),
|
|
46
|
+
audio: z.string().optional(),
|
|
47
|
+
images: z.array(z.string()).optional(),
|
|
48
|
+
thumbnail: z.string().optional(),
|
|
49
|
+
}),
|
|
50
|
+
// Optional until Play carries the control; unknown keys are stripped by this schema, so
|
|
51
|
+
// a mode not listed here would never reach the audio stage.
|
|
52
|
+
chunking: z
|
|
53
|
+
.object({
|
|
54
|
+
mode: z.enum(chunkModes),
|
|
55
|
+
words: z.number().optional(),
|
|
56
|
+
characters: z.number().int().min(1).max(1000000).optional(),
|
|
57
|
+
})
|
|
58
|
+
.optional(),
|
|
59
|
+
silenceGapSeconds: z.number(),
|
|
60
|
+
subtitles: subtitleConfigSchema.optional(),
|
|
61
|
+
});
|
|
62
|
+
export const runConfigSchema = runDraftSchema.extend({
|
|
63
|
+
rendered: z.record(z.string(), z.string()),
|
|
64
|
+
});
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { transact } from "../../kernel/db/tx.js";
|
|
2
2
|
import { stageKinds } from "../../kernel/pipeline.js";
|
|
3
3
|
import { storeArticleText } from "../article/store.js";
|
|
4
|
-
import {
|
|
4
|
+
import { admitInitialRevision } from "../rebuild/runtime-admission.js";
|
|
5
|
+
import { adoptBaseline } from "../revisions/adopt.js";
|
|
6
|
+
import { attachStagedFile, storeText } from "../storage/staging.js";
|
|
7
|
+
import { releaseStagedFile } from "../storage/staging-refs.js";
|
|
5
8
|
import { insertProject, insertStage } from "./repo.js";
|
|
6
9
|
// Provide → `provided` with its output attached; Off → `skipped`;
|
|
7
10
|
// everything else → `pending`.
|
|
@@ -13,7 +16,7 @@ export function initialState(source) {
|
|
|
13
16
|
}
|
|
14
17
|
// The project row, its six stages and the provided content all land together or not at
|
|
15
18
|
// all. The caller ticks the runner after this returns, never inside it.
|
|
16
|
-
export function startRun(deps, draft, rendered, retainStaged = false) {
|
|
19
|
+
export function startRun(deps, draft, rendered, retainStaged = false, templates) {
|
|
17
20
|
const id = deps.ids.next();
|
|
18
21
|
const at = deps.clock.now().toISOString();
|
|
19
22
|
const config = { ...draft, rendered };
|
|
@@ -50,9 +53,15 @@ export function startRun(deps, draft, rendered, retainStaged = false) {
|
|
|
50
53
|
insertStage(deps.db, stage);
|
|
51
54
|
}
|
|
52
55
|
attachProvided(deps, id, draft, moved, retainStaged);
|
|
56
|
+
if (deps.catalogue !== undefined) {
|
|
57
|
+
const baseline = adoptBaseline(deps, id, templates);
|
|
58
|
+
if (!baseline.ok)
|
|
59
|
+
throw new Error("The new project has no revision.");
|
|
60
|
+
admitInitialRevision(deps, baseline.view, deps.catalogue.read());
|
|
61
|
+
}
|
|
53
62
|
});
|
|
54
63
|
for (const source of retainStaged ? [] : moved) {
|
|
55
|
-
|
|
64
|
+
releaseStagedFile(deps, source);
|
|
56
65
|
}
|
|
57
66
|
return { project, stages };
|
|
58
67
|
}
|
|
@@ -101,5 +110,5 @@ function attach(deps, projectId, kind, stagedFileId, role, collected, retainStag
|
|
|
101
110
|
// the file went away between the check and the write.
|
|
102
111
|
throw new Error(`the ${kind} stage's file could not be attached: ${result.reason}`);
|
|
103
112
|
}
|
|
104
|
-
collected.push(
|
|
113
|
+
collected.push(stagedFileId);
|
|
105
114
|
}
|