@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.
Files changed (207) hide show
  1. package/README.md +28 -6
  2. package/SUBTITLES.md +2 -0
  3. package/dist/adapters/alignment/window.js +46 -5
  4. package/dist/adapters/alignment/worker.js +2 -2
  5. package/dist/adapters/llm/claude-code.js +6 -3
  6. package/dist/adapters/llm/codex.js +102 -12
  7. package/dist/adapters/llm/gemini.js +11 -8
  8. package/dist/adapters/llm/run-cli.js +61 -6
  9. package/dist/adapters/tts/inworld-async.js +4 -4
  10. package/dist/adapters/tts/inworld.js +73 -68
  11. package/dist/catalog/registry.js +13 -73
  12. package/dist/edge/cli.js +5 -2
  13. package/dist/edge/events/hub.js +7 -2
  14. package/dist/edge/events/preview-cache.js +7 -4
  15. package/dist/edge/events/visibility.js +33 -0
  16. package/dist/edge/http/actions.js +74 -79
  17. package/dist/edge/http/app.js +39 -6
  18. package/dist/edge/http/audio-preview.js +9 -1
  19. package/dist/edge/http/checkpoints.js +95 -0
  20. package/dist/edge/http/diagnostics.js +31 -0
  21. package/dist/edge/http/draft-files.js +69 -0
  22. package/dist/edge/http/draft-problem.js +15 -0
  23. package/dist/edge/http/drafts.js +95 -0
  24. package/dist/edge/http/multipart.js +86 -0
  25. package/dist/edge/http/mutations.js +49 -0
  26. package/dist/edge/http/planning.js +12 -1
  27. package/dist/edge/http/project-templates.js +74 -0
  28. package/dist/edge/http/projects.js +19 -2
  29. package/dist/edge/http/revision-files.js +84 -0
  30. package/dist/edge/http/revisions.js +103 -0
  31. package/dist/edge/http/schedules.js +100 -0
  32. package/dist/edge/http/staging.js +11 -52
  33. package/dist/edge/http/storage.js +110 -0
  34. package/dist/edge/http/subtitles.js +9 -79
  35. package/dist/edge/http/tutorial.js +26 -0
  36. package/dist/edge/signal-shutdown.js +23 -0
  37. package/dist/kernel/audio-preview.js +11 -5
  38. package/dist/kernel/db/migrations/0004-project-revisions.sql +91 -0
  39. package/dist/kernel/db/migrations/0005-revision-work.sql +103 -0
  40. package/dist/kernel/db/migrations/0006-play-drafts.sql +38 -0
  41. package/dist/kernel/db/migrations/0007-review-checkpoints.sql +28 -0
  42. package/dist/kernel/db/migrations/0008-project-templates.sql +21 -0
  43. package/dist/kernel/db/migrations/0009-scheduled-jobs.sql +37 -0
  44. package/dist/kernel/db/migrations/0010-retain-schedule-history.sql +62 -0
  45. package/dist/kernel/ports/model.js +5 -0
  46. package/dist/kernel/runner/attempt-repo.js +15 -0
  47. package/dist/kernel/runner/attempt.js +14 -2
  48. package/dist/kernel/runner/checkpoint-authority.js +23 -0
  49. package/dist/kernel/runner/index.js +35 -11
  50. package/dist/kernel/runner/providers.js +18 -8
  51. package/dist/kernel/runner/work-attempt.js +47 -0
  52. package/dist/kernel/runner/work-authority.js +37 -0
  53. package/dist/kernel/runner/work.js +23 -0
  54. package/dist/main.js +227 -69
  55. package/dist/slices/admission/repo.js +5 -56
  56. package/dist/slices/admission/schema.js +64 -0
  57. package/dist/slices/admission/start.js +13 -4
  58. package/dist/slices/article/continuation.js +9 -3
  59. package/dist/slices/article/provided-entries.js +7 -3
  60. package/dist/slices/article/run.js +20 -7
  61. package/dist/slices/article/segments.js +13 -4
  62. package/dist/slices/batch/index.js +8 -11
  63. package/dist/slices/cancel/index.js +78 -34
  64. package/dist/slices/checkpoints/change.js +250 -0
  65. package/dist/slices/checkpoints/fingerprint.js +33 -0
  66. package/dist/slices/checkpoints/index.js +2 -0
  67. package/dist/slices/checkpoints/model.js +1 -0
  68. package/dist/slices/checkpoints/recovery.js +116 -0
  69. package/dist/slices/checkpoints/repo.js +132 -0
  70. package/dist/slices/checkpoints/rules.js +30 -0
  71. package/dist/slices/checkpoints/schema.js +59 -0
  72. package/dist/slices/control/index.js +88 -22
  73. package/dist/slices/control/providers.js +3 -2
  74. package/dist/slices/control/revision-control-schema.js +7 -0
  75. package/dist/slices/control/revision-control.js +59 -0
  76. package/dist/slices/estimate/index.js +77 -59
  77. package/dist/slices/estimate/requests.js +102 -0
  78. package/dist/slices/images/run.js +14 -3
  79. package/dist/slices/library/slots.js +11 -11
  80. package/dist/slices/library/snapshot.js +33 -0
  81. package/dist/slices/narration/legacy-plan.js +58 -0
  82. package/dist/slices/narration/live.js +2 -2
  83. package/dist/slices/narration/plan.js +48 -54
  84. package/dist/slices/narration/run.js +21 -8
  85. package/dist/slices/play-drafts/convert.js +120 -0
  86. package/dist/slices/play-drafts/model.js +1 -0
  87. package/dist/slices/play-drafts/readiness.js +80 -0
  88. package/dist/slices/play-drafts/repo.js +76 -0
  89. package/dist/slices/play-drafts/review-inputs.js +150 -0
  90. package/dist/slices/play-drafts/review.js +109 -0
  91. package/dist/slices/play-drafts/schema.js +218 -0
  92. package/dist/slices/play-drafts/service.js +242 -0
  93. package/dist/slices/play-drafts/start-repo.js +113 -0
  94. package/dist/slices/play-drafts/start.js +148 -0
  95. package/dist/slices/play-drafts/uploads.js +67 -0
  96. package/dist/slices/project-templates/from-project.js +159 -0
  97. package/dist/slices/project-templates/model.js +1 -0
  98. package/dist/slices/project-templates/repo.js +41 -0
  99. package/dist/slices/project-templates/schema.js +37 -0
  100. package/dist/slices/project-templates/service.js +136 -0
  101. package/dist/slices/project-templates/setup.js +55 -0
  102. package/dist/slices/rebuild/admission-repo.js +120 -0
  103. package/dist/slices/rebuild/dependencies.js +33 -0
  104. package/dist/slices/rebuild/model.js +94 -0
  105. package/dist/slices/rebuild/narration-history.js +43 -0
  106. package/dist/slices/rebuild/narration-reuse.js +20 -0
  107. package/dist/slices/rebuild/preview-details.js +144 -0
  108. package/dist/slices/rebuild/preview-plan.js +223 -0
  109. package/dist/slices/rebuild/preview-retained.js +128 -0
  110. package/dist/slices/rebuild/provided-review.js +32 -0
  111. package/dist/slices/rebuild/recipe-audio.js +172 -0
  112. package/dist/slices/rebuild/recipe-build.js +17 -0
  113. package/dist/slices/rebuild/recipe-checkpoints.js +75 -0
  114. package/dist/slices/rebuild/recipe-exports.js +65 -0
  115. package/dist/slices/rebuild/recipe-fixture.js +150 -0
  116. package/dist/slices/rebuild/recipe-input-schema.js +82 -0
  117. package/dist/slices/rebuild/recipe-legacy.js +135 -0
  118. package/dist/slices/rebuild/recipe-model.js +77 -0
  119. package/dist/slices/rebuild/recipe-provider-choice.js +14 -0
  120. package/dist/slices/rebuild/recipe-save.js +221 -0
  121. package/dist/slices/rebuild/recipe-text.js +170 -0
  122. package/dist/slices/rebuild/recipe-validation.js +86 -0
  123. package/dist/slices/rebuild/recipe-visual.js +111 -0
  124. package/dist/slices/rebuild/recipe-work.js +212 -0
  125. package/dist/slices/rebuild/recipes.js +4 -0
  126. package/dist/slices/rebuild/repo.js +64 -0
  127. package/dist/slices/rebuild/runtime-actions.js +76 -0
  128. package/dist/slices/rebuild/runtime-admission.js +71 -0
  129. package/dist/slices/rebuild/runtime-article.js +117 -0
  130. package/dist/slices/rebuild/runtime-checkpoints.js +45 -0
  131. package/dist/slices/rebuild/runtime-export-inputs.js +109 -0
  132. package/dist/slices/rebuild/runtime-export.js +139 -0
  133. package/dist/slices/rebuild/runtime-instructions.js +51 -0
  134. package/dist/slices/rebuild/runtime-local.js +137 -0
  135. package/dist/slices/rebuild/runtime-materialize.js +105 -0
  136. package/dist/slices/rebuild/runtime-narration-reuse.js +134 -0
  137. package/dist/slices/rebuild/runtime-plan.js +103 -0
  138. package/dist/slices/rebuild/runtime-provider.js +184 -0
  139. package/dist/slices/rebuild/runtime-publication.js +172 -0
  140. package/dist/slices/rebuild/runtime-run.js +35 -0
  141. package/dist/slices/rebuild/runtime-store.js +166 -0
  142. package/dist/slices/rebuild/runtime-subtitles.js +202 -0
  143. package/dist/slices/rebuild/service-readiness.js +110 -0
  144. package/dist/slices/rebuild/service.js +149 -0
  145. package/dist/slices/rebuild/transition-repo.js +108 -0
  146. package/dist/slices/rebuild/work-records.js +44 -0
  147. package/dist/slices/reruns/index.js +5 -0
  148. package/dist/slices/research/run.js +33 -12
  149. package/dist/slices/revisions/adopt-assets.js +31 -0
  150. package/dist/slices/revisions/adopt-content.js +112 -0
  151. package/dist/slices/revisions/adopt.js +100 -0
  152. package/dist/slices/revisions/downloads.js +80 -0
  153. package/dist/slices/revisions/index.js +5 -0
  154. package/dist/slices/revisions/manifest-repo.js +125 -0
  155. package/dist/slices/revisions/model.js +1 -0
  156. package/dist/slices/revisions/mutation-assets.js +143 -0
  157. package/dist/slices/revisions/mutation-cues.js +95 -0
  158. package/dist/slices/revisions/mutation-prepare.js +182 -0
  159. package/dist/slices/revisions/mutation-request.js +58 -0
  160. package/dist/slices/revisions/mutation-work.js +47 -0
  161. package/dist/slices/revisions/mutations.js +194 -0
  162. package/dist/slices/revisions/projection.js +82 -0
  163. package/dist/slices/revisions/publication-model.js +1 -0
  164. package/dist/slices/revisions/publication-rules.js +166 -0
  165. package/dist/slices/revisions/publish.js +115 -0
  166. package/dist/slices/revisions/repo.js +83 -0
  167. package/dist/slices/revisions/restore.js +60 -0
  168. package/dist/slices/revisions/rules.js +99 -0
  169. package/dist/slices/revisions/schema.js +160 -0
  170. package/dist/slices/revisions/view.js +44 -0
  171. package/dist/slices/schedules/calendar.js +47 -0
  172. package/dist/slices/schedules/model.js +1 -0
  173. package/dist/slices/schedules/repo.js +281 -0
  174. package/dist/slices/schedules/scheduler.js +144 -0
  175. package/dist/slices/schedules/schema.js +80 -0
  176. package/dist/slices/schedules/service.js +178 -0
  177. package/dist/slices/settings/cli-paths.js +1 -1
  178. package/dist/slices/settings/cli-status.js +45 -5
  179. package/dist/slices/settings/tutorial-schema.js +39 -0
  180. package/dist/slices/settings/tutorial.js +66 -0
  181. package/dist/slices/storage/assets.js +108 -0
  182. package/dist/slices/storage/delete-project.js +2 -1
  183. package/dist/slices/storage/downloads.js +4 -1
  184. package/dist/slices/storage/portable.js +871 -0
  185. package/dist/slices/storage/prepare.js +67 -0
  186. package/dist/slices/storage/reconcile.js +25 -5
  187. package/dist/slices/storage/repo.js +2 -15
  188. package/dist/slices/storage/schema.js +35 -0
  189. package/dist/slices/storage/staging-refs.js +26 -0
  190. package/dist/slices/storage/staging.js +16 -11
  191. package/dist/slices/thumbnail/run.js +26 -9
  192. package/dist/slices/video/audio-export-args.js +33 -0
  193. package/dist/slices/video/audio-export.js +13 -36
  194. package/dist/slices/video/audio-inputs.js +4 -3
  195. package/dist/slices/video/ffmpeg.js +6 -0
  196. package/dist/slices/video/reuse-audio.js +11 -4
  197. package/dist/updater/install-flow.js +27 -2
  198. package/dist/updater/plan.js +28 -2
  199. package/dist/updater/service.js +27 -2
  200. package/dist/updater/worker.js +2 -1
  201. package/dist/web/assets/index-CO7DnVX9.js +137 -0
  202. package/dist/web/assets/index-DX1_chKs.css +1 -0
  203. package/dist/web/index.html +2 -2
  204. package/package.json +2 -2
  205. package/dist/adapters/tts/inworld-text.js +0 -32
  206. package/dist/web/assets/index-Ce2HApus.css +0 -1
  207. 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
- const id = ctx.attempts.start({
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.id, state, reason);
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, event);
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.id, entry);
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.id);
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 stateOf = (kind) => stages.find((stage) => stage.kind === kind)?.state ?? "pending";
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.id)) {
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 (!dependencies.every((kind) => satisfied(stateOf(kind)))) {
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.id)) {
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 continuation;
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
+ }