@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
|
@@ -20,12 +20,24 @@ export async function attempt(ctx, call, opts) {
|
|
|
20
20
|
ctx.signal.throwIfAborted();
|
|
21
21
|
const limit = timeoutMs[opts.kind];
|
|
22
22
|
for (let n = 1;; n += 1) {
|
|
23
|
-
|
|
23
|
+
ctx.signal.throwIfAborted();
|
|
24
|
+
const retrieving = ctx.continuation?.read() !== undefined;
|
|
25
|
+
if (!retrieving && !ctx.maySubmit())
|
|
26
|
+
return { ok: false, reason: "held" };
|
|
27
|
+
const started = ctx.attempts.start({
|
|
24
28
|
stageId: ctx.stageId,
|
|
29
|
+
revisionId: ctx.work.revisionId,
|
|
30
|
+
workId: ctx.work.workId,
|
|
31
|
+
workPieceId: ctx.workPieceId ?? null,
|
|
32
|
+
work: ctx.work,
|
|
33
|
+
operation: retrieving ? "retrieve" : "submit",
|
|
25
34
|
pieceId: ctx.pieceId ?? null,
|
|
26
35
|
n,
|
|
27
36
|
startedAt: ctx.clock.now().toISOString(),
|
|
28
37
|
});
|
|
38
|
+
if (typeof started !== "string" && !started.ok)
|
|
39
|
+
return started;
|
|
40
|
+
const id = typeof started === "string" ? started : started.value;
|
|
29
41
|
const window = deadline(ctx.clock, ctx.signal, limit, opts.streaming === true);
|
|
30
42
|
let result;
|
|
31
43
|
try {
|
|
@@ -42,7 +54,7 @@ export async function attempt(ctx, call, opts) {
|
|
|
42
54
|
endedAt: ctx.clock.now().toISOString(),
|
|
43
55
|
errorText: null,
|
|
44
56
|
});
|
|
45
|
-
return result.value;
|
|
57
|
+
return { ok: true, value: result.value };
|
|
46
58
|
}
|
|
47
59
|
if (ctx.signal.aborted) {
|
|
48
60
|
// An aborted call counts nothing. The row is closed so the page is
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function createCheckpointAuthority(deps) {
|
|
2
|
+
return {
|
|
3
|
+
beforeClaim: deps.decide,
|
|
4
|
+
release: (projectId, checkpointId, identity) => {
|
|
5
|
+
// A nested savepoint cannot authorize dispatch before its caller commits.
|
|
6
|
+
if (deps.inTransaction())
|
|
7
|
+
return { ok: false, reason: "conflict" };
|
|
8
|
+
const result = deps.approve(projectId, checkpointId, identity);
|
|
9
|
+
if (result.ok) {
|
|
10
|
+
try {
|
|
11
|
+
deps.wake(projectId);
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
deps.log.write("error", "checkpoint.wake", {
|
|
15
|
+
projectId,
|
|
16
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return result;
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -16,6 +16,8 @@ export function createRunner(deps) {
|
|
|
16
16
|
const emitStage = (stage, state, reason) => {
|
|
17
17
|
deps.emit(stage.projectId, {
|
|
18
18
|
type: "stage.state",
|
|
19
|
+
revisionId: stage.work.revisionId,
|
|
20
|
+
workId: stage.work.workId,
|
|
19
21
|
projectId: stage.projectId,
|
|
20
22
|
stage: stage.kind,
|
|
21
23
|
state,
|
|
@@ -35,7 +37,7 @@ export function createRunner(deps) {
|
|
|
35
37
|
}
|
|
36
38
|
};
|
|
37
39
|
const announce = (projectId) => {
|
|
38
|
-
const state = derive(deps.stages.stagesOf(projectId), deps.stages.paused?.(projectId));
|
|
40
|
+
const state = derive(deps.stages.standingsOf?.(projectId) ?? deps.stages.stagesOf(projectId), deps.stages.paused?.(projectId));
|
|
39
41
|
if (announced.get(projectId) === state) {
|
|
40
42
|
return;
|
|
41
43
|
}
|
|
@@ -44,7 +46,7 @@ export function createRunner(deps) {
|
|
|
44
46
|
};
|
|
45
47
|
const conclude = (stage, state, reason) => {
|
|
46
48
|
try {
|
|
47
|
-
deps.stages.finish(stage.
|
|
49
|
+
deps.stages.finish(stage.work, state, reason);
|
|
48
50
|
}
|
|
49
51
|
catch (error) {
|
|
50
52
|
// The row stays `running` when the write fails. The event still goes out, and the
|
|
@@ -63,14 +65,25 @@ export function createRunner(deps) {
|
|
|
63
65
|
if (run === undefined) {
|
|
64
66
|
throw new Error(`no implementation is registered for the ${stage.kind} stage`);
|
|
65
67
|
}
|
|
66
|
-
await run({
|
|
68
|
+
const result = await run({
|
|
67
69
|
stage,
|
|
70
|
+
work: stage.work,
|
|
71
|
+
maySubmit: (pieceId) => !shuttingDown &&
|
|
72
|
+
!stopped.has(stage.projectId) &&
|
|
73
|
+
!deps.stages.paused?.(stage.projectId) &&
|
|
74
|
+
(deps.checkpoints === undefined ||
|
|
75
|
+
deps.checkpoints.beforeClaim(stage.work).kind === "eligible") &&
|
|
76
|
+
deps.stages.maySubmit(stage.work, pieceId),
|
|
68
77
|
signal: controller.signal,
|
|
69
78
|
emit: (event) => {
|
|
70
|
-
deps.emit(stage.projectId,
|
|
79
|
+
deps.emit(stage.projectId, {
|
|
80
|
+
...event,
|
|
81
|
+
revisionId: stage.work.revisionId,
|
|
82
|
+
workId: stage.work.workId,
|
|
83
|
+
});
|
|
71
84
|
},
|
|
72
85
|
});
|
|
73
|
-
conclude(stage, "done", null);
|
|
86
|
+
conclude(stage, result === "held" ? "pending" : "done", null);
|
|
74
87
|
}
|
|
75
88
|
catch (error) {
|
|
76
89
|
if (controller.signal.aborted) {
|
|
@@ -92,6 +105,7 @@ export function createRunner(deps) {
|
|
|
92
105
|
const controller = new AbortController();
|
|
93
106
|
let release = () => { };
|
|
94
107
|
const entry = {
|
|
108
|
+
work: stage.work,
|
|
95
109
|
projectId: stage.projectId,
|
|
96
110
|
controller,
|
|
97
111
|
settled: new Promise((resolve) => {
|
|
@@ -99,12 +113,12 @@ export function createRunner(deps) {
|
|
|
99
113
|
}),
|
|
100
114
|
};
|
|
101
115
|
// Registered before the body runs: a tick arriving mid-start has to see it in flight.
|
|
102
|
-
inflight.set(stage.
|
|
116
|
+
inflight.set(stage.work.workId, entry);
|
|
103
117
|
retally();
|
|
104
118
|
emitStage(stage, "running", null);
|
|
105
119
|
void execute(stage, controller)
|
|
106
120
|
.finally(() => {
|
|
107
|
-
inflight.delete(stage.
|
|
121
|
+
inflight.delete(stage.work.workId);
|
|
108
122
|
release();
|
|
109
123
|
try {
|
|
110
124
|
// Claim the next stage before reading the tally, so a hand-over never reports
|
|
@@ -136,17 +150,26 @@ export function createRunner(deps) {
|
|
|
136
150
|
}
|
|
137
151
|
function startEligible(projectId) {
|
|
138
152
|
const stages = deps.stages.stagesOf(projectId);
|
|
139
|
-
const
|
|
153
|
+
const satisfiedKind = (kind) => {
|
|
154
|
+
const matches = stages.filter((stage) => stage.kind === kind);
|
|
155
|
+
return matches.length > 0 && matches.every((stage) => satisfied(stage.state));
|
|
156
|
+
};
|
|
140
157
|
for (const stage of stages) {
|
|
141
|
-
if (stage.state !== "pending" || inflight.has(stage.
|
|
158
|
+
if (stage.state !== "pending" || inflight.has(stage.work.workId)) {
|
|
142
159
|
continue;
|
|
143
160
|
}
|
|
144
161
|
const dependencies = deps.stages.dependenciesOf?.(projectId, stage.kind) ?? graph[stage.kind];
|
|
145
|
-
if (
|
|
162
|
+
if (deps.stages.ready === undefined
|
|
163
|
+
? !dependencies.every(satisfiedKind)
|
|
164
|
+
: !deps.stages.ready(stage.work)) {
|
|
146
165
|
continue;
|
|
147
166
|
}
|
|
167
|
+
if (deps.stages.paused?.(projectId))
|
|
168
|
+
return;
|
|
169
|
+
if (deps.checkpoints && deps.checkpoints.beforeClaim(stage.work).kind !== "eligible")
|
|
170
|
+
continue;
|
|
148
171
|
// Nothing is awaited here: the fan-out starts audio, images and thumbnail together.
|
|
149
|
-
if (deps.stages.claim(stage.
|
|
172
|
+
if (deps.stages.claim(stage.work)) {
|
|
150
173
|
start({ ...stage, state: "running" });
|
|
151
174
|
}
|
|
152
175
|
}
|
|
@@ -166,6 +189,7 @@ export function createRunner(deps) {
|
|
|
166
189
|
}
|
|
167
190
|
}
|
|
168
191
|
return {
|
|
192
|
+
...(deps.checkpoints === undefined ? {} : { checkpoints: deps.checkpoints }),
|
|
169
193
|
tick,
|
|
170
194
|
settled,
|
|
171
195
|
hasInflight: (projectId) => [...inflight.values()].some((entry) => entry.projectId === projectId),
|
|
@@ -2,6 +2,9 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { attempt } from "./attempt.js";
|
|
3
3
|
export function stageProviders(deps, context, pieceId) {
|
|
4
4
|
const ctx = {
|
|
5
|
+
work: context.work,
|
|
6
|
+
maySubmit: () => context.maySubmit(pieceId),
|
|
7
|
+
...(pieceId === undefined ? {} : { workPieceId: pieceId }),
|
|
5
8
|
clock: deps.clock,
|
|
6
9
|
log: deps.log,
|
|
7
10
|
attempts: deps.attempts,
|
|
@@ -20,6 +23,7 @@ export function stageProviders(deps, context, pieceId) {
|
|
|
20
23
|
const callId = randomUUID();
|
|
21
24
|
const preview = (text, reset) => context.emit({
|
|
22
25
|
type: "llm.preview",
|
|
26
|
+
...(pieceId === undefined ? {} : { workPieceId: pieceId }),
|
|
23
27
|
projectId: context.stage.projectId,
|
|
24
28
|
stage: context.stage.kind,
|
|
25
29
|
callId,
|
|
@@ -34,6 +38,7 @@ export function stageProviders(deps, context, pieceId) {
|
|
|
34
38
|
let finishReason = null;
|
|
35
39
|
for await (const event of port.complete({
|
|
36
40
|
model: call.model,
|
|
41
|
+
...(call.thinkingConfig === undefined ? {} : { thinkingConfig: call.thinkingConfig }),
|
|
37
42
|
...(call.thinking === undefined ? {} : { thinking: call.thinking }),
|
|
38
43
|
messages: call.messages,
|
|
39
44
|
...(call.webSearch === undefined ? {} : { webSearch: call.webSearch }),
|
|
@@ -64,7 +69,17 @@ export function stageProviders(deps, context, pieceId) {
|
|
|
64
69
|
},
|
|
65
70
|
tts: (call, observe) => {
|
|
66
71
|
const port = deps.registry.tts(call.provider);
|
|
67
|
-
let
|
|
72
|
+
let transientContinuation;
|
|
73
|
+
const continuation = {
|
|
74
|
+
read: () => pieceId === undefined
|
|
75
|
+
? transientContinuation
|
|
76
|
+
: (deps.attempts.readContinuation?.(context.work, pieceId) ?? transientContinuation),
|
|
77
|
+
write: (token) => {
|
|
78
|
+
if (pieceId !== undefined)
|
|
79
|
+
deps.attempts.writeContinuation?.(context.work, pieceId, token);
|
|
80
|
+
transientContinuation = token;
|
|
81
|
+
},
|
|
82
|
+
};
|
|
68
83
|
const notify = (event) => {
|
|
69
84
|
try {
|
|
70
85
|
observe?.(event);
|
|
@@ -78,7 +93,7 @@ export function stageProviders(deps, context, pieceId) {
|
|
|
78
93
|
});
|
|
79
94
|
}
|
|
80
95
|
};
|
|
81
|
-
return schedule(call.provider, () => attempt(ctx, async (signal, progress) => {
|
|
96
|
+
return schedule(call.provider, () => attempt({ ...ctx, continuation }, async (signal, progress) => {
|
|
82
97
|
notify({ type: "start" });
|
|
83
98
|
try {
|
|
84
99
|
const spoken = await port.synthesize({
|
|
@@ -87,12 +102,7 @@ export function stageProviders(deps, context, pieceId) {
|
|
|
87
102
|
text: call.text,
|
|
88
103
|
signal,
|
|
89
104
|
onActivity: progress,
|
|
90
|
-
continuation
|
|
91
|
-
read: () => continuation,
|
|
92
|
-
write: (token) => {
|
|
93
|
-
continuation = token;
|
|
94
|
-
},
|
|
95
|
-
},
|
|
105
|
+
continuation,
|
|
96
106
|
});
|
|
97
107
|
const reader = spoken.audio.getReader();
|
|
98
108
|
const chunks = [];
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { transact } from "../db/tx.js";
|
|
2
|
+
import { maySubmit, ownsCurrentWork, workExists } from "./work-authority.js";
|
|
3
|
+
export function startWorkAttempt(db, ids, input, work, pieceId, operation = "submit") {
|
|
4
|
+
return transact(db, () => {
|
|
5
|
+
if (input.stageId !== work.stageId || !workExists(db, work))
|
|
6
|
+
return { ok: false, reason: "held" };
|
|
7
|
+
if (operation === "retrieve") {
|
|
8
|
+
if (pieceId === null || readWorkContinuation(db, work, pieceId) === null)
|
|
9
|
+
return { ok: false, reason: "held" };
|
|
10
|
+
const submitted = db
|
|
11
|
+
.prepare("SELECT id FROM attempts WHERE work_id=? AND work_piece_id=? ORDER BY rowid DESC LIMIT 1")
|
|
12
|
+
.get(work.workId, pieceId);
|
|
13
|
+
return typeof submitted?.id === "string"
|
|
14
|
+
? { ok: true, value: submitted.id }
|
|
15
|
+
: { ok: false, reason: "held" };
|
|
16
|
+
}
|
|
17
|
+
else if (!maySubmit(db, work, pieceId ?? undefined) ||
|
|
18
|
+
(pieceId !== null && readWorkContinuation(db, work, pieceId) !== null))
|
|
19
|
+
return { ok: false, reason: "held" };
|
|
20
|
+
const id = ids.next();
|
|
21
|
+
db.prepare(`INSERT INTO attempts(id,stage_id,piece_id,n,started_at,revision_id,work_id,work_piece_id) VALUES (?,?,?,?,?,?,?,?)`).run(id, work.stageId, null, input.n, input.startedAt, work.revisionId, work.workId, pieceId);
|
|
22
|
+
if (operation === "submit") {
|
|
23
|
+
if (pieceId !== null)
|
|
24
|
+
db.prepare(`UPDATE revision_work_pieces SET submitted_at=COALESCE(submitted_at,?),state='running' WHERE id=? AND work_id=?`).run(input.startedAt, pieceId, work.workId);
|
|
25
|
+
if (ownsCurrentWork(db, work, pieceId))
|
|
26
|
+
db.prepare("UPDATE stages SET attempt_count=? WHERE id=?").run(input.n, work.stageId);
|
|
27
|
+
}
|
|
28
|
+
return { ok: true, value: id };
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
export function readWorkContinuation(db, work, pieceId) {
|
|
32
|
+
if (!workExists(db, work))
|
|
33
|
+
return null;
|
|
34
|
+
const row = db
|
|
35
|
+
.prepare("SELECT continuation FROM revision_work_pieces WHERE id=? AND work_id=? AND submitted_at IS NOT NULL")
|
|
36
|
+
.get(pieceId, work.workId);
|
|
37
|
+
// A malformed persisted token still records an accepted operation. Let the adapter
|
|
38
|
+
// reject it rather than turning corruption into another paid submission.
|
|
39
|
+
return typeof row?.continuation === "string" ? row.continuation : null;
|
|
40
|
+
}
|
|
41
|
+
export function writeWorkContinuation(db, work, pieceId, continuation) {
|
|
42
|
+
if (!workExists(db, work) || continuation.length === 0)
|
|
43
|
+
return false;
|
|
44
|
+
return (db
|
|
45
|
+
.prepare(`UPDATE revision_work_pieces SET continuation=? WHERE id=? AND work_id=? AND submitted_at IS NOT NULL AND (continuation IS NULL OR continuation=?)`)
|
|
46
|
+
.run(continuation, pieceId, work.workId, continuation).changes === 1);
|
|
47
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export function workExists(db, work) {
|
|
2
|
+
return (db
|
|
3
|
+
.prepare(`SELECT 1 FROM revision_work WHERE id=? AND project_id=? AND revision_id=? AND stage_id=? AND kind=? AND fingerprint=?`)
|
|
4
|
+
.get(work.workId, work.projectId, work.revisionId, work.stageId, work.kind, work.fingerprint) !== undefined);
|
|
5
|
+
}
|
|
6
|
+
export function claimWork(db, work) {
|
|
7
|
+
return (db
|
|
8
|
+
.prepare(`UPDATE revision_work SET state='running',progress_current=NULL,progress_total=NULL WHERE id=? AND project_id=? AND revision_id=? AND stage_id=? AND kind=? AND fingerprint=? AND state='pending' AND dispatch_state='allowed'`)
|
|
9
|
+
.run(work.workId, work.projectId, work.revisionId, work.stageId, work.kind, work.fingerprint)
|
|
10
|
+
.changes === 1);
|
|
11
|
+
}
|
|
12
|
+
export function maySubmit(db, work, pieceId) {
|
|
13
|
+
if (!workExists(db, work))
|
|
14
|
+
return false;
|
|
15
|
+
const allowed = db
|
|
16
|
+
.prepare(`SELECT 1 FROM revision_work WHERE id=? AND state IN ('pending','running') AND dispatch_state='allowed'`)
|
|
17
|
+
.get(work.workId) !== undefined;
|
|
18
|
+
if (!allowed || !ownsCurrentWork(db, work, pieceId ?? null))
|
|
19
|
+
return false;
|
|
20
|
+
return (pieceId === undefined ||
|
|
21
|
+
db
|
|
22
|
+
.prepare(`SELECT 1 FROM revision_work_pieces WHERE id=? AND work_id=? AND dispatch_state='allowed' AND state IN ('pending','running')`)
|
|
23
|
+
.get(pieceId, work.workId) !== undefined);
|
|
24
|
+
}
|
|
25
|
+
export function ownsCurrentWork(db, work, pieceId) {
|
|
26
|
+
if (!workExists(db, work))
|
|
27
|
+
return false;
|
|
28
|
+
return (db
|
|
29
|
+
.prepare(`SELECT 1 FROM revision_work_reservations r JOIN project_heads h ON h.project_id=r.project_id AND h.revision_id=r.revision_id JOIN project_revisions v ON v.id=r.revision_id JOIN json_each(v.fingerprints) f ON f.key=COALESCE(r.logical_key,r.work_key) AND f.value=COALESCE(r.desired_fingerprint,r.fingerprint) WHERE r.project_id=? AND r.work_id=? AND r.piece_id IS ? AND (r.piece_id IS NULL OR EXISTS(SELECT 1 FROM revision_work_pieces p WHERE p.id=r.piece_id AND p.work_id=r.work_id AND p.work_key=r.work_key AND p.fingerprint=r.fingerprint))`)
|
|
30
|
+
.get(work.projectId, work.workId, pieceId) !== undefined);
|
|
31
|
+
}
|
|
32
|
+
export function finishWork(db, work, state, reason) {
|
|
33
|
+
if (!workExists(db, work))
|
|
34
|
+
return;
|
|
35
|
+
const normalized = state === "provided" || state === "skipped" ? "done" : state;
|
|
36
|
+
db.prepare(`UPDATE revision_work SET state=?,failure_reason=?,dispatch_state=CASE WHEN ?='pending' THEN 'held' ELSE dispatch_state END WHERE id=?`).run(normalized, reason, normalized, work.workId);
|
|
37
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
export function fingerprint(value) {
|
|
4
|
+
function canonical(item) {
|
|
5
|
+
if (typeof item === "number")
|
|
6
|
+
z.number().finite().parse(item);
|
|
7
|
+
if (item === null || typeof item !== "object")
|
|
8
|
+
return JSON.stringify(item);
|
|
9
|
+
if (Array.isArray(item))
|
|
10
|
+
return `[${item.map(canonical).join(",")}]`;
|
|
11
|
+
const record = item;
|
|
12
|
+
return `{${Object.keys(record)
|
|
13
|
+
.sort()
|
|
14
|
+
.map((key) => {
|
|
15
|
+
const part = record[key];
|
|
16
|
+
if (part === undefined)
|
|
17
|
+
throw new Error("Missing fingerprint input");
|
|
18
|
+
return `${JSON.stringify(key)}:${canonical(part)}`;
|
|
19
|
+
})
|
|
20
|
+
.join(",")}}`;
|
|
21
|
+
}
|
|
22
|
+
return createHash("sha256").update(canonical(value)).digest("hex");
|
|
23
|
+
}
|