@gentbajko/slopify 0.8.4 → 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 (204) hide show
  1. package/README.md +28 -6
  2. package/dist/adapters/llm/claude-code.js +6 -3
  3. package/dist/adapters/llm/codex.js +102 -12
  4. package/dist/adapters/llm/gemini.js +11 -8
  5. package/dist/adapters/llm/run-cli.js +61 -6
  6. package/dist/adapters/tts/inworld-async.js +4 -4
  7. package/dist/adapters/tts/inworld.js +73 -68
  8. package/dist/catalog/registry.js +13 -73
  9. package/dist/edge/cli.js +5 -2
  10. package/dist/edge/events/hub.js +7 -2
  11. package/dist/edge/events/preview-cache.js +7 -4
  12. package/dist/edge/events/visibility.js +33 -0
  13. package/dist/edge/http/actions.js +74 -79
  14. package/dist/edge/http/app.js +39 -6
  15. package/dist/edge/http/audio-preview.js +9 -1
  16. package/dist/edge/http/checkpoints.js +95 -0
  17. package/dist/edge/http/diagnostics.js +31 -0
  18. package/dist/edge/http/draft-files.js +69 -0
  19. package/dist/edge/http/draft-problem.js +15 -0
  20. package/dist/edge/http/drafts.js +95 -0
  21. package/dist/edge/http/multipart.js +86 -0
  22. package/dist/edge/http/mutations.js +49 -0
  23. package/dist/edge/http/planning.js +12 -1
  24. package/dist/edge/http/project-templates.js +74 -0
  25. package/dist/edge/http/projects.js +19 -2
  26. package/dist/edge/http/revision-files.js +84 -0
  27. package/dist/edge/http/revisions.js +103 -0
  28. package/dist/edge/http/schedules.js +100 -0
  29. package/dist/edge/http/staging.js +11 -52
  30. package/dist/edge/http/storage.js +110 -0
  31. package/dist/edge/http/subtitles.js +9 -79
  32. package/dist/edge/http/tutorial.js +26 -0
  33. package/dist/edge/signal-shutdown.js +23 -0
  34. package/dist/kernel/audio-preview.js +11 -5
  35. package/dist/kernel/db/migrations/0004-project-revisions.sql +91 -0
  36. package/dist/kernel/db/migrations/0005-revision-work.sql +103 -0
  37. package/dist/kernel/db/migrations/0006-play-drafts.sql +38 -0
  38. package/dist/kernel/db/migrations/0007-review-checkpoints.sql +28 -0
  39. package/dist/kernel/db/migrations/0008-project-templates.sql +21 -0
  40. package/dist/kernel/db/migrations/0009-scheduled-jobs.sql +37 -0
  41. package/dist/kernel/db/migrations/0010-retain-schedule-history.sql +62 -0
  42. package/dist/kernel/ports/model.js +5 -0
  43. package/dist/kernel/runner/attempt-repo.js +15 -0
  44. package/dist/kernel/runner/attempt.js +14 -2
  45. package/dist/kernel/runner/checkpoint-authority.js +23 -0
  46. package/dist/kernel/runner/index.js +35 -11
  47. package/dist/kernel/runner/providers.js +18 -8
  48. package/dist/kernel/runner/work-attempt.js +47 -0
  49. package/dist/kernel/runner/work-authority.js +37 -0
  50. package/dist/kernel/runner/work.js +23 -0
  51. package/dist/main.js +227 -69
  52. package/dist/slices/admission/repo.js +5 -56
  53. package/dist/slices/admission/schema.js +64 -0
  54. package/dist/slices/admission/start.js +13 -4
  55. package/dist/slices/article/continuation.js +9 -3
  56. package/dist/slices/article/provided-entries.js +7 -3
  57. package/dist/slices/article/run.js +20 -7
  58. package/dist/slices/article/segments.js +13 -4
  59. package/dist/slices/batch/index.js +8 -11
  60. package/dist/slices/cancel/index.js +78 -34
  61. package/dist/slices/checkpoints/change.js +250 -0
  62. package/dist/slices/checkpoints/fingerprint.js +33 -0
  63. package/dist/slices/checkpoints/index.js +2 -0
  64. package/dist/slices/checkpoints/model.js +1 -0
  65. package/dist/slices/checkpoints/recovery.js +116 -0
  66. package/dist/slices/checkpoints/repo.js +132 -0
  67. package/dist/slices/checkpoints/rules.js +30 -0
  68. package/dist/slices/checkpoints/schema.js +59 -0
  69. package/dist/slices/control/index.js +88 -22
  70. package/dist/slices/control/providers.js +3 -2
  71. package/dist/slices/control/revision-control-schema.js +7 -0
  72. package/dist/slices/control/revision-control.js +59 -0
  73. package/dist/slices/estimate/index.js +77 -59
  74. package/dist/slices/estimate/requests.js +102 -0
  75. package/dist/slices/images/run.js +14 -3
  76. package/dist/slices/library/slots.js +11 -11
  77. package/dist/slices/library/snapshot.js +33 -0
  78. package/dist/slices/narration/legacy-plan.js +58 -0
  79. package/dist/slices/narration/live.js +2 -2
  80. package/dist/slices/narration/plan.js +48 -54
  81. package/dist/slices/narration/run.js +21 -8
  82. package/dist/slices/play-drafts/convert.js +120 -0
  83. package/dist/slices/play-drafts/model.js +1 -0
  84. package/dist/slices/play-drafts/readiness.js +80 -0
  85. package/dist/slices/play-drafts/repo.js +76 -0
  86. package/dist/slices/play-drafts/review-inputs.js +150 -0
  87. package/dist/slices/play-drafts/review.js +109 -0
  88. package/dist/slices/play-drafts/schema.js +218 -0
  89. package/dist/slices/play-drafts/service.js +242 -0
  90. package/dist/slices/play-drafts/start-repo.js +113 -0
  91. package/dist/slices/play-drafts/start.js +148 -0
  92. package/dist/slices/play-drafts/uploads.js +67 -0
  93. package/dist/slices/project-templates/from-project.js +159 -0
  94. package/dist/slices/project-templates/model.js +1 -0
  95. package/dist/slices/project-templates/repo.js +41 -0
  96. package/dist/slices/project-templates/schema.js +37 -0
  97. package/dist/slices/project-templates/service.js +136 -0
  98. package/dist/slices/project-templates/setup.js +55 -0
  99. package/dist/slices/rebuild/admission-repo.js +120 -0
  100. package/dist/slices/rebuild/dependencies.js +33 -0
  101. package/dist/slices/rebuild/model.js +94 -0
  102. package/dist/slices/rebuild/narration-history.js +43 -0
  103. package/dist/slices/rebuild/narration-reuse.js +20 -0
  104. package/dist/slices/rebuild/preview-details.js +144 -0
  105. package/dist/slices/rebuild/preview-plan.js +223 -0
  106. package/dist/slices/rebuild/preview-retained.js +128 -0
  107. package/dist/slices/rebuild/provided-review.js +32 -0
  108. package/dist/slices/rebuild/recipe-audio.js +172 -0
  109. package/dist/slices/rebuild/recipe-build.js +17 -0
  110. package/dist/slices/rebuild/recipe-checkpoints.js +75 -0
  111. package/dist/slices/rebuild/recipe-exports.js +65 -0
  112. package/dist/slices/rebuild/recipe-fixture.js +150 -0
  113. package/dist/slices/rebuild/recipe-input-schema.js +82 -0
  114. package/dist/slices/rebuild/recipe-legacy.js +135 -0
  115. package/dist/slices/rebuild/recipe-model.js +77 -0
  116. package/dist/slices/rebuild/recipe-provider-choice.js +14 -0
  117. package/dist/slices/rebuild/recipe-save.js +221 -0
  118. package/dist/slices/rebuild/recipe-text.js +170 -0
  119. package/dist/slices/rebuild/recipe-validation.js +86 -0
  120. package/dist/slices/rebuild/recipe-visual.js +111 -0
  121. package/dist/slices/rebuild/recipe-work.js +212 -0
  122. package/dist/slices/rebuild/recipes.js +4 -0
  123. package/dist/slices/rebuild/repo.js +64 -0
  124. package/dist/slices/rebuild/runtime-actions.js +76 -0
  125. package/dist/slices/rebuild/runtime-admission.js +71 -0
  126. package/dist/slices/rebuild/runtime-article.js +117 -0
  127. package/dist/slices/rebuild/runtime-checkpoints.js +45 -0
  128. package/dist/slices/rebuild/runtime-export-inputs.js +109 -0
  129. package/dist/slices/rebuild/runtime-export.js +139 -0
  130. package/dist/slices/rebuild/runtime-instructions.js +51 -0
  131. package/dist/slices/rebuild/runtime-local.js +137 -0
  132. package/dist/slices/rebuild/runtime-materialize.js +105 -0
  133. package/dist/slices/rebuild/runtime-narration-reuse.js +134 -0
  134. package/dist/slices/rebuild/runtime-plan.js +103 -0
  135. package/dist/slices/rebuild/runtime-provider.js +184 -0
  136. package/dist/slices/rebuild/runtime-publication.js +172 -0
  137. package/dist/slices/rebuild/runtime-run.js +35 -0
  138. package/dist/slices/rebuild/runtime-store.js +166 -0
  139. package/dist/slices/rebuild/runtime-subtitles.js +202 -0
  140. package/dist/slices/rebuild/service-readiness.js +110 -0
  141. package/dist/slices/rebuild/service.js +149 -0
  142. package/dist/slices/rebuild/transition-repo.js +108 -0
  143. package/dist/slices/rebuild/work-records.js +44 -0
  144. package/dist/slices/reruns/index.js +5 -0
  145. package/dist/slices/research/run.js +33 -12
  146. package/dist/slices/revisions/adopt-assets.js +31 -0
  147. package/dist/slices/revisions/adopt-content.js +112 -0
  148. package/dist/slices/revisions/adopt.js +100 -0
  149. package/dist/slices/revisions/downloads.js +80 -0
  150. package/dist/slices/revisions/index.js +5 -0
  151. package/dist/slices/revisions/manifest-repo.js +125 -0
  152. package/dist/slices/revisions/model.js +1 -0
  153. package/dist/slices/revisions/mutation-assets.js +143 -0
  154. package/dist/slices/revisions/mutation-cues.js +95 -0
  155. package/dist/slices/revisions/mutation-prepare.js +182 -0
  156. package/dist/slices/revisions/mutation-request.js +58 -0
  157. package/dist/slices/revisions/mutation-work.js +47 -0
  158. package/dist/slices/revisions/mutations.js +194 -0
  159. package/dist/slices/revisions/projection.js +82 -0
  160. package/dist/slices/revisions/publication-model.js +1 -0
  161. package/dist/slices/revisions/publication-rules.js +166 -0
  162. package/dist/slices/revisions/publish.js +115 -0
  163. package/dist/slices/revisions/repo.js +83 -0
  164. package/dist/slices/revisions/restore.js +60 -0
  165. package/dist/slices/revisions/rules.js +99 -0
  166. package/dist/slices/revisions/schema.js +160 -0
  167. package/dist/slices/revisions/view.js +44 -0
  168. package/dist/slices/schedules/calendar.js +47 -0
  169. package/dist/slices/schedules/model.js +1 -0
  170. package/dist/slices/schedules/repo.js +281 -0
  171. package/dist/slices/schedules/scheduler.js +144 -0
  172. package/dist/slices/schedules/schema.js +80 -0
  173. package/dist/slices/schedules/service.js +178 -0
  174. package/dist/slices/settings/cli-paths.js +1 -1
  175. package/dist/slices/settings/cli-status.js +45 -5
  176. package/dist/slices/settings/tutorial-schema.js +39 -0
  177. package/dist/slices/settings/tutorial.js +66 -0
  178. package/dist/slices/storage/assets.js +108 -0
  179. package/dist/slices/storage/delete-project.js +2 -1
  180. package/dist/slices/storage/downloads.js +4 -1
  181. package/dist/slices/storage/portable.js +871 -0
  182. package/dist/slices/storage/prepare.js +67 -0
  183. package/dist/slices/storage/reconcile.js +25 -5
  184. package/dist/slices/storage/repo.js +2 -15
  185. package/dist/slices/storage/schema.js +35 -0
  186. package/dist/slices/storage/staging-refs.js +26 -0
  187. package/dist/slices/storage/staging.js +16 -11
  188. package/dist/slices/thumbnail/run.js +26 -9
  189. package/dist/slices/video/audio-export-args.js +33 -0
  190. package/dist/slices/video/audio-export.js +13 -36
  191. package/dist/slices/video/audio-inputs.js +4 -3
  192. package/dist/slices/video/ffmpeg.js +6 -0
  193. package/dist/slices/video/reuse-audio.js +11 -4
  194. package/dist/updater/install-flow.js +27 -2
  195. package/dist/updater/plan.js +28 -2
  196. package/dist/updater/service.js +27 -2
  197. package/dist/updater/worker.js +2 -1
  198. package/dist/web/assets/index-CO7DnVX9.js +137 -0
  199. package/dist/web/assets/index-DX1_chKs.css +1 -0
  200. package/dist/web/index.html +2 -2
  201. package/package.json +2 -2
  202. package/dist/adapters/tts/inworld-text.js +0 -32
  203. package/dist/web/assets/index-Ce2HApus.css +0 -1
  204. package/dist/web/assets/index-xbD0MZOY.js +0 -130
@@ -39,12 +39,16 @@ export function createAudioPreviewStore(limits = {}) {
39
39
  const expiry = setInterval(prune, Math.min(keepMs, 30_000));
40
40
  expiry.unref();
41
41
  const store = {
42
- begin(projectId, key, label) {
42
+ begin(projectId, key, label, origin) {
43
43
  if (closed)
44
44
  return inactive;
45
45
  prune();
46
46
  for (const entry of entries.values()) {
47
- if (entry.projectId === projectId && entry.key === key)
47
+ if (entry.projectId === projectId &&
48
+ entry.key === key &&
49
+ entry.origin?.revisionId === origin?.revisionId &&
50
+ entry.origin?.workId === origin?.workId &&
51
+ entry.origin?.workPieceId === origin?.workPieceId)
48
52
  remove(entry);
49
53
  }
50
54
  // At capacity, completed previews yield to current generation. Otherwise
@@ -57,6 +61,7 @@ export function createAudioPreviewStore(limits = {}) {
57
61
  if (entries.size >= maxEntries)
58
62
  return inactive;
59
63
  const entry = {
64
+ origin,
60
65
  id: randomUUID(),
61
66
  projectId,
62
67
  key,
@@ -103,7 +108,7 @@ export function createAudioPreviewStore(limits = {}) {
103
108
  prune();
104
109
  return [...entries.values()]
105
110
  .filter((entry) => entry.projectId === projectId)
106
- .map(({ id, label, state, bytes }) => ({ id, label, state, bytes }));
111
+ .map(({ id, label, state, bytes, origin }) => ({ id, label, state, bytes, ...origin }));
107
112
  },
108
113
  stream(projectId, id, signal) {
109
114
  prune();
@@ -115,9 +120,10 @@ export function createAudioPreviewStore(limits = {}) {
115
120
  return undefined;
116
121
  return follow(entry, signal);
117
122
  },
118
- clear(projectId) {
123
+ clear(projectId, workId) {
119
124
  for (const entry of entries.values())
120
- if (entry.projectId === projectId)
125
+ if (entry.projectId === projectId &&
126
+ (workId === undefined || entry.origin?.workId === workId))
121
127
  remove(entry);
122
128
  },
123
129
  close() {
@@ -0,0 +1,91 @@
1
+ CREATE TABLE project_revisions (
2
+ id TEXT PRIMARY KEY,
3
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
4
+ parent_id TEXT,
5
+ restored_from_id TEXT,
6
+ config TEXT NOT NULL CHECK(json_valid(config)),
7
+ content TEXT NOT NULL CHECK(json_valid(content)),
8
+ fingerprints TEXT NOT NULL CHECK(json_valid(fingerprints)),
9
+ created_at TEXT NOT NULL,
10
+ UNIQUE(project_id, id),
11
+ FOREIGN KEY(project_id, parent_id)
12
+ REFERENCES project_revisions(project_id, id),
13
+ FOREIGN KEY(project_id, restored_from_id)
14
+ REFERENCES project_revisions(project_id, id)
15
+ );
16
+ CREATE INDEX project_revisions_project ON project_revisions(project_id, created_at, id);
17
+ CREATE TABLE project_heads (
18
+ project_id TEXT PRIMARY KEY REFERENCES projects(id) ON DELETE CASCADE,
19
+ revision_id TEXT NOT NULL,
20
+ FOREIGN KEY(project_id, revision_id)
21
+ REFERENCES project_revisions(project_id, id) ON DELETE CASCADE
22
+ );
23
+ CREATE TABLE project_assets (
24
+ id TEXT PRIMARY KEY,
25
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
26
+ path TEXT NOT NULL CHECK(length(path) > 0),
27
+ bytes INTEGER CHECK(bytes IS NULL OR bytes >= 0),
28
+ created_at TEXT NOT NULL,
29
+ UNIQUE(project_id, id),
30
+ UNIQUE(project_id, path)
31
+ );
32
+ CREATE TABLE revision_outputs (
33
+ id TEXT PRIMARY KEY,
34
+ project_id TEXT NOT NULL,
35
+ revision_id TEXT NOT NULL,
36
+ slot TEXT NOT NULL,
37
+ work_key TEXT NOT NULL,
38
+ asset_id TEXT NOT NULL,
39
+ fingerprint TEXT NOT NULL,
40
+ state TEXT NOT NULL CHECK(state IN ('ready','outdated','review')),
41
+ descriptor TEXT NOT NULL CHECK(json_valid(descriptor)),
42
+ publication_id TEXT,
43
+ selected INTEGER NOT NULL CHECK(selected IN (0,1)),
44
+ created_at TEXT NOT NULL,
45
+ FOREIGN KEY(project_id, revision_id)
46
+ REFERENCES project_revisions(project_id, id) ON DELETE CASCADE,
47
+ FOREIGN KEY(project_id, asset_id)
48
+ REFERENCES project_assets(project_id, id) ON DELETE CASCADE
49
+ );
50
+ CREATE UNIQUE INDEX revision_outputs_selected ON revision_outputs(revision_id, slot)
51
+ WHERE selected = 1;
52
+ CREATE INDEX revision_outputs_revision ON revision_outputs(revision_id, created_at, id);
53
+ CREATE UNIQUE INDEX revision_outputs_publication ON revision_outputs(revision_id, publication_id, slot)
54
+ WHERE publication_id IS NOT NULL;
55
+ CREATE TABLE revision_pieces (
56
+ id TEXT PRIMARY KEY,
57
+ project_id TEXT NOT NULL,
58
+ revision_id TEXT NOT NULL,
59
+ piece_key TEXT NOT NULL,
60
+ stage_kind TEXT NOT NULL CHECK(stage_kind IN
61
+ ('research','article','audio','images','thumbnail','video')),
62
+ asset_id TEXT,
63
+ fingerprint TEXT NOT NULL,
64
+ descriptor TEXT NOT NULL CHECK(json_valid(descriptor)),
65
+ publication_id TEXT,
66
+ selected INTEGER NOT NULL CHECK(selected IN (0,1)),
67
+ created_at TEXT NOT NULL,
68
+ FOREIGN KEY(project_id, revision_id)
69
+ REFERENCES project_revisions(project_id, id) ON DELETE CASCADE,
70
+ FOREIGN KEY(project_id, asset_id)
71
+ REFERENCES project_assets(project_id, id) ON DELETE CASCADE
72
+ );
73
+ CREATE UNIQUE INDEX revision_pieces_selected ON revision_pieces(revision_id, piece_key)
74
+ WHERE selected = 1;
75
+ CREATE INDEX revision_pieces_revision ON revision_pieces(revision_id, created_at, id);
76
+ CREATE UNIQUE INDEX revision_pieces_publication ON revision_pieces(revision_id, publication_id, piece_key)
77
+ WHERE publication_id IS NOT NULL;
78
+ CREATE TABLE revision_mutations (
79
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
80
+ idempotency_key TEXT NOT NULL,
81
+ operation TEXT NOT NULL CHECK(operation IN ('save','restore')),
82
+ request_hash TEXT NOT NULL,
83
+ base_revision_id TEXT NOT NULL,
84
+ result_revision_id TEXT NOT NULL,
85
+ created_at TEXT NOT NULL,
86
+ PRIMARY KEY(project_id, idempotency_key),
87
+ FOREIGN KEY(project_id, base_revision_id)
88
+ REFERENCES project_revisions(project_id, id) ON DELETE CASCADE,
89
+ FOREIGN KEY(project_id, result_revision_id)
90
+ REFERENCES project_revisions(project_id, id) ON DELETE CASCADE
91
+ );
@@ -0,0 +1,103 @@
1
+ CREATE UNIQUE INDEX stages_project_identity ON stages(project_id,id,kind);
2
+ CREATE TABLE revision_work (
3
+ id TEXT PRIMARY KEY,
4
+ project_id TEXT NOT NULL,
5
+ revision_id TEXT NOT NULL,
6
+ stage_id TEXT NOT NULL,
7
+ kind TEXT NOT NULL,
8
+ fingerprint TEXT NOT NULL,
9
+ admission_id TEXT,
10
+ recipe_context TEXT CHECK(recipe_context IS NULL OR json_valid(recipe_context)),
11
+ progress_current REAL,
12
+ progress_total REAL,
13
+ state TEXT NOT NULL CHECK(state IN ('pending','running','done','failed','canceled')),
14
+ dispatch_state TEXT NOT NULL CHECK(dispatch_state IN ('held','allowed','draining')),
15
+ failure_reason TEXT,
16
+ created_at TEXT NOT NULL,
17
+ UNIQUE(project_id,id),
18
+ FOREIGN KEY(project_id,revision_id) REFERENCES project_revisions(project_id,id) ON DELETE CASCADE,
19
+ FOREIGN KEY(project_id,stage_id,kind) REFERENCES stages(project_id,id,kind) ON DELETE CASCADE
20
+ );
21
+ CREATE INDEX revision_work_stage ON revision_work(revision_id,stage_id);
22
+ CREATE INDEX revision_work_dispatch ON revision_work(project_id,dispatch_state,state);
23
+ CREATE TABLE revision_work_pieces (
24
+ id TEXT PRIMARY KEY,
25
+ work_id TEXT NOT NULL REFERENCES revision_work(id) ON DELETE CASCADE,
26
+ work_key TEXT NOT NULL,
27
+ request_fingerprint TEXT NOT NULL,
28
+ fingerprint TEXT NOT NULL,
29
+ input_json TEXT NOT NULL CHECK(json_valid(input_json)),
30
+ logical_fingerprint TEXT,
31
+ result_json TEXT CHECK(result_json IS NULL OR json_valid(result_json)),
32
+ continuation TEXT,
33
+ generation_token TEXT,
34
+ state TEXT NOT NULL CHECK(state IN ('pending','running','done','failed','held')),
35
+ dispatch_state TEXT NOT NULL CHECK(dispatch_state IN ('held','allowed','draining')),
36
+ submitted_at TEXT,
37
+ UNIQUE(work_id,id),
38
+ UNIQUE(work_id,work_key)
39
+ );
40
+ CREATE INDEX revision_piece_dispatch ON revision_work_pieces(work_id,dispatch_state,state);
41
+ CREATE TABLE revision_work_reservations (
42
+ project_id TEXT NOT NULL,
43
+ revision_id TEXT NOT NULL,
44
+ work_key TEXT NOT NULL,
45
+ work_id TEXT NOT NULL,
46
+ piece_id TEXT,
47
+ fingerprint TEXT NOT NULL,
48
+ logical_key TEXT,
49
+ desired_fingerprint TEXT,
50
+ CHECK((logical_key IS NULL) = (desired_fingerprint IS NULL)),
51
+ PRIMARY KEY(revision_id,work_key),
52
+ FOREIGN KEY(project_id,revision_id) REFERENCES project_revisions(project_id,id) ON DELETE CASCADE,
53
+ FOREIGN KEY(project_id,work_id) REFERENCES revision_work(project_id,id) ON DELETE CASCADE,
54
+ FOREIGN KEY(work_id,piece_id) REFERENCES revision_work_pieces(work_id,id) ON DELETE CASCADE
55
+ );
56
+ CREATE TABLE rebuild_previews (
57
+ id TEXT PRIMARY KEY,
58
+ project_id TEXT NOT NULL,
59
+ revision_id TEXT NOT NULL,
60
+ plan_fingerprint TEXT NOT NULL,
61
+ body_json TEXT NOT NULL CHECK(json_valid(body_json)),
62
+ execution_json TEXT CHECK(execution_json IS NULL OR json_valid(execution_json)),
63
+ created_at TEXT NOT NULL,
64
+ UNIQUE(project_id,revision_id,id),
65
+ FOREIGN KEY(project_id,revision_id) REFERENCES project_revisions(project_id,id) ON DELETE CASCADE
66
+ );
67
+ CREATE TABLE rebuild_admissions (
68
+ id TEXT PRIMARY KEY,
69
+ project_id TEXT NOT NULL,
70
+ revision_id TEXT NOT NULL,
71
+ idempotency_key TEXT NOT NULL,
72
+ request_hash TEXT NOT NULL,
73
+ preview_id TEXT NOT NULL,
74
+ response_json TEXT NOT NULL CHECK(json_valid(response_json)),
75
+ created_at TEXT NOT NULL,
76
+ UNIQUE(project_id,idempotency_key),
77
+ FOREIGN KEY(project_id,revision_id) REFERENCES project_revisions(project_id,id) ON DELETE CASCADE,
78
+ FOREIGN KEY(project_id,revision_id,preview_id) REFERENCES rebuild_previews(project_id,revision_id,id)
79
+ );
80
+ ALTER TABLE attempts ADD COLUMN revision_id TEXT REFERENCES project_revisions(id) ON DELETE SET NULL;
81
+ ALTER TABLE attempts ADD COLUMN work_id TEXT REFERENCES revision_work(id) ON DELETE SET NULL;
82
+ ALTER TABLE attempts ADD COLUMN work_piece_id TEXT REFERENCES revision_work_pieces(id) ON DELETE SET NULL;
83
+
84
+ CREATE TABLE project_control_receipts (
85
+ project_id TEXT NOT NULL,
86
+ idempotency_key TEXT NOT NULL,
87
+ operation TEXT NOT NULL CHECK(operation IN ('pause','cancel')),
88
+ request_hash TEXT NOT NULL,
89
+ base_revision_id TEXT NOT NULL,
90
+ response_json TEXT NOT NULL CHECK(json_valid(response_json)),
91
+ created_at TEXT NOT NULL,
92
+ PRIMARY KEY(project_id,idempotency_key),
93
+ FOREIGN KEY(project_id,base_revision_id) REFERENCES project_revisions(project_id,id) ON DELETE CASCADE
94
+ );
95
+ CREATE TABLE revision_provided_reviews (
96
+ project_id TEXT NOT NULL,
97
+ revision_id TEXT NOT NULL,
98
+ work_key TEXT NOT NULL,
99
+ dependency_fingerprint TEXT NOT NULL,
100
+ admission_id TEXT NOT NULL REFERENCES rebuild_admissions(id) ON DELETE CASCADE,
101
+ PRIMARY KEY(revision_id,work_key),
102
+ FOREIGN KEY(project_id,revision_id) REFERENCES project_revisions(project_id,id) ON DELETE CASCADE
103
+ );
@@ -0,0 +1,38 @@
1
+ CREATE TABLE play_drafts (
2
+ id TEXT PRIMARY KEY,
3
+ schema_version INTEGER NOT NULL,
4
+ version INTEGER NOT NULL CHECK(version >= 1),
5
+ title TEXT NOT NULL,
6
+ document_json TEXT NOT NULL CHECK(json_valid(document_json)),
7
+ creation_hash TEXT NOT NULL,
8
+ save_mutation_id TEXT,
9
+ save_request_hash TEXT,
10
+ created_at TEXT NOT NULL,
11
+ updated_at TEXT NOT NULL,
12
+ state TEXT NOT NULL CHECK(state IN ('active','starting','started')),
13
+ review_id TEXT UNIQUE,
14
+ review_json TEXT CHECK(review_json IS NULL OR json_valid(review_json)),
15
+ review_fingerprint TEXT,
16
+ start_id TEXT
17
+ );
18
+ CREATE TABLE play_draft_attachments (
19
+ id TEXT PRIMARY KEY,
20
+ draft_id TEXT NOT NULL REFERENCES play_drafts(id) ON DELETE CASCADE,
21
+ staged_file_id TEXT REFERENCES staged_files(id) ON DELETE SET NULL,
22
+ kind TEXT NOT NULL CHECK(kind IN ('audio','images','thumbnail')),
23
+ original_filename TEXT NOT NULL,
24
+ status TEXT NOT NULL CHECK(status IN ('pending','ready','reattach')),
25
+ error TEXT
26
+ );
27
+ CREATE INDEX play_draft_attachment_file ON play_draft_attachments(staged_file_id);
28
+ CREATE INDEX play_draft_attachment_owner ON play_draft_attachments(draft_id);
29
+ CREATE TABLE play_start_receipts (
30
+ id TEXT PRIMARY KEY,
31
+ draft_id TEXT NOT NULL,
32
+ draft_version INTEGER NOT NULL,
33
+ request_hash TEXT NOT NULL,
34
+ result_json TEXT NOT NULL CHECK(json_valid(result_json)),
35
+ created_at TEXT NOT NULL,
36
+ UNIQUE(draft_id,draft_version)
37
+ );
38
+ CREATE INDEX play_start_receipt_draft ON play_start_receipts(draft_id);
@@ -0,0 +1,28 @@
1
+ CREATE UNIQUE INDEX revision_work_revision_identity ON revision_work(project_id,revision_id,id);
2
+ CREATE TABLE review_checkpoints (
3
+ project_id TEXT NOT NULL,
4
+ revision_id TEXT NOT NULL,
5
+ checkpoint_id TEXT NOT NULL,
6
+ stage TEXT NOT NULL CHECK(stage IN ('audio','images','video')),
7
+ work_id TEXT NOT NULL,
8
+ fingerprint TEXT NOT NULL CHECK(length(fingerprint)=64),
9
+ state TEXT NOT NULL CHECK(state IN ('configured','pending-review','held','released','satisfied','invalidated','canceled')),
10
+ created_at TEXT NOT NULL,
11
+ approved_at TEXT,
12
+ PRIMARY KEY(project_id,revision_id,checkpoint_id),
13
+ UNIQUE(project_id,revision_id,stage),
14
+ FOREIGN KEY(project_id,revision_id) REFERENCES project_revisions(project_id,id) ON DELETE CASCADE,
15
+ FOREIGN KEY(project_id,revision_id,work_id) REFERENCES revision_work(project_id,revision_id,id) ON DELETE CASCADE
16
+ );
17
+ CREATE INDEX review_checkpoint_work ON review_checkpoints(work_id);
18
+ CREATE TABLE review_checkpoint_approvals (
19
+ project_id TEXT NOT NULL,
20
+ idempotency_key TEXT NOT NULL,
21
+ revision_id TEXT NOT NULL,
22
+ checkpoint_id TEXT NOT NULL,
23
+ fingerprint TEXT NOT NULL CHECK(length(fingerprint)=64),
24
+ approved_at TEXT NOT NULL,
25
+ PRIMARY KEY(project_id,idempotency_key),
26
+ UNIQUE(project_id,revision_id,checkpoint_id),
27
+ FOREIGN KEY(project_id,revision_id,checkpoint_id) REFERENCES review_checkpoints(project_id,revision_id,checkpoint_id) ON DELETE CASCADE
28
+ );
@@ -0,0 +1,21 @@
1
+ CREATE TABLE project_templates (
2
+ id TEXT PRIMARY KEY,
3
+ head_version INTEGER NOT NULL CHECK(head_version >= 1),
4
+ creation_hash TEXT NOT NULL,
5
+ created_at TEXT NOT NULL,
6
+ mutation_id TEXT,
7
+ mutation_hash TEXT
8
+ );
9
+ CREATE TABLE project_template_revisions (
10
+ template_id TEXT NOT NULL REFERENCES project_templates(id) ON DELETE CASCADE,
11
+ version INTEGER NOT NULL CHECK(version >= 1),
12
+ name TEXT NOT NULL CHECK(length(trim(name)) BETWEEN 1 AND 200),
13
+ document_json TEXT NOT NULL CHECK(json_valid(document_json)),
14
+ created_at TEXT NOT NULL,
15
+ PRIMARY KEY(template_id,version)
16
+ );
17
+ CREATE TABLE project_template_instantiations (
18
+ draft_id TEXT PRIMARY KEY REFERENCES play_drafts(id) ON DELETE CASCADE,
19
+ template_id TEXT NOT NULL,
20
+ template_version INTEGER NOT NULL CHECK(template_version >= 1)
21
+ );
@@ -0,0 +1,37 @@
1
+ CREATE TABLE schedules (
2
+ id TEXT PRIMARY KEY,
3
+ name TEXT NOT NULL CHECK(length(trim(name)) BETWEEN 1 AND 200),
4
+ template_id TEXT NOT NULL,
5
+ template_version INTEGER NOT NULL CHECK(template_version >= 1),
6
+ cadence_json TEXT NOT NULL CHECK(json_valid(cadence_json)),
7
+ timezone TEXT NOT NULL,
8
+ missed_policy TEXT NOT NULL CHECK(missed_policy IN ('skip','run-once')),
9
+ overlap_policy TEXT NOT NULL CHECK(overlap_policy IN ('skip')),
10
+ spend_limit_cents INTEGER CHECK(spend_limit_cents IS NULL OR spend_limit_cents >= 0),
11
+ items_json TEXT NOT NULL CHECK(json_valid(items_json)),
12
+ status TEXT NOT NULL CHECK(status IN ('active','paused','completed','canceled')),
13
+ version INTEGER NOT NULL CHECK(version >= 1),
14
+ creation_hash TEXT NOT NULL,
15
+ next_run_at TEXT,
16
+ created_at TEXT NOT NULL,
17
+ updated_at TEXT NOT NULL,
18
+ mutation_id TEXT,
19
+ mutation_hash TEXT,
20
+ FOREIGN KEY(template_id) REFERENCES project_templates(id)
21
+ );
22
+ CREATE INDEX schedules_due ON schedules(status, next_run_at);
23
+ CREATE TABLE schedule_runs (
24
+ id TEXT PRIMARY KEY,
25
+ schedule_id TEXT NOT NULL REFERENCES schedules(id) ON DELETE CASCADE,
26
+ scheduled_for TEXT NOT NULL,
27
+ status TEXT NOT NULL CHECK(status IN ('running','succeeded','failed','skipped')),
28
+ request_id TEXT,
29
+ project_ids_json TEXT NOT NULL CHECK(json_valid(project_ids_json)),
30
+ estimate_json TEXT CHECK(estimate_json IS NULL OR json_valid(estimate_json)),
31
+ started_at TEXT NOT NULL,
32
+ ended_at TEXT,
33
+ error TEXT,
34
+ UNIQUE(schedule_id, scheduled_for),
35
+ UNIQUE(request_id)
36
+ );
37
+ CREATE INDEX schedule_runs_schedule ON schedule_runs(schedule_id, started_at DESC);
@@ -0,0 +1,62 @@
1
+ CREATE TABLE retained_schedule_runs (
2
+ id TEXT PRIMARY KEY,
3
+ schedule_id TEXT NOT NULL,
4
+ scheduled_for TEXT NOT NULL,
5
+ status TEXT NOT NULL CHECK(status IN ('running','succeeded','failed','skipped')),
6
+ request_id TEXT,
7
+ project_ids_json TEXT NOT NULL CHECK(json_valid(project_ids_json)),
8
+ estimate_json TEXT CHECK(estimate_json IS NULL OR json_valid(estimate_json)),
9
+ started_at TEXT NOT NULL,
10
+ ended_at TEXT,
11
+ projects_settled_at TEXT,
12
+ error TEXT,
13
+ UNIQUE(schedule_id, scheduled_for),
14
+ UNIQUE(request_id)
15
+ );
16
+ INSERT INTO retained_schedule_runs (
17
+ id,schedule_id,scheduled_for,status,request_id,project_ids_json,estimate_json,started_at,
18
+ ended_at,projects_settled_at,error
19
+ )
20
+ SELECT
21
+ id,schedule_id,scheduled_for,status,request_id,project_ids_json,estimate_json,started_at,
22
+ ended_at,NULL,error
23
+ FROM schedule_runs;
24
+ DROP TABLE schedule_runs;
25
+ ALTER TABLE retained_schedule_runs RENAME TO schedule_runs;
26
+ CREATE INDEX schedule_runs_schedule ON schedule_runs(schedule_id, started_at DESC);
27
+
28
+ CREATE TABLE retained_schedules (
29
+ id TEXT PRIMARY KEY,
30
+ name TEXT NOT NULL CHECK(length(trim(name)) BETWEEN 1 AND 200),
31
+ template_id TEXT NOT NULL,
32
+ template_version INTEGER NOT NULL CHECK(template_version >= 1),
33
+ cadence_json TEXT NOT NULL CHECK(json_valid(cadence_json)),
34
+ timezone TEXT NOT NULL,
35
+ missed_policy TEXT NOT NULL CHECK(missed_policy IN ('skip','run-once')),
36
+ overlap_policy TEXT NOT NULL CHECK(overlap_policy IN ('skip')),
37
+ spend_limit_cents INTEGER CHECK(spend_limit_cents IS NULL OR spend_limit_cents >= 0),
38
+ items_json TEXT NOT NULL CHECK(json_valid(items_json)),
39
+ status TEXT NOT NULL CHECK(status IN ('active','paused','completed','canceled')),
40
+ version INTEGER NOT NULL CHECK(version >= 1),
41
+ creation_hash TEXT NOT NULL,
42
+ next_run_at TEXT,
43
+ created_at TEXT NOT NULL,
44
+ updated_at TEXT NOT NULL,
45
+ mutation_id TEXT,
46
+ mutation_hash TEXT,
47
+ deleted_at TEXT,
48
+ CHECK(deleted_at IS NULL OR (status IN ('completed','canceled') AND next_run_at IS NULL))
49
+ );
50
+ INSERT INTO retained_schedules (
51
+ id,name,template_id,template_version,cadence_json,timezone,missed_policy,overlap_policy,
52
+ spend_limit_cents,items_json,status,version,creation_hash,next_run_at,created_at,updated_at,
53
+ mutation_id,mutation_hash,deleted_at
54
+ )
55
+ SELECT
56
+ id,name,template_id,template_version,cadence_json,timezone,missed_policy,overlap_policy,
57
+ spend_limit_cents,items_json,status,version,creation_hash,next_run_at,created_at,updated_at,
58
+ mutation_id,mutation_hash,NULL
59
+ FROM schedules;
60
+ DROP TABLE schedules;
61
+ ALTER TABLE retained_schedules RENAME TO schedules;
62
+ CREATE INDEX schedules_due ON schedules(status, next_run_at) WHERE deleted_at IS NULL;
@@ -1,6 +1,11 @@
1
1
  // The vocabulary that crosses the provider seam. Domain types only: a vendor's payload
2
2
  // shape never leaves its adapter.
3
3
  export const providerFamilies = ["llm", "tts", "image"];
4
+ export function readinessIsUsable(readiness) {
5
+ return readiness.kind === "keyed"
6
+ ? readiness.hasKey
7
+ : readiness.installed && readiness.issue === undefined;
8
+ }
4
9
  export const providerErrorKinds = [
5
10
  "auth",
6
11
  // Distinct from `auth`, a key the provider rejected: a bad key runs the whole retry
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { providerErrorKinds } from "../ports/model.js";
3
+ import { readWorkContinuation, startWorkAttempt, writeWorkContinuation } from "./work-attempt.js";
3
4
  // One row per provider call attempt. The project page reads the last error text off these rows
4
5
  // and Usage counts them, so an attempt is written when it opens rather than when it ends: a
5
6
  // process that dies mid-call leaves the evidence behind.
@@ -13,10 +14,20 @@ const attemptRow = z.object({
13
14
  ended_at: z.string().nullable(),
14
15
  outcome: z.enum(attemptOutcomes).nullable(),
15
16
  error_text: z.string().nullable(),
17
+ revision_id: z.string().nullable(),
18
+ work_id: z.string().nullable(),
19
+ work_piece_id: z.string().nullable(),
16
20
  });
17
21
  export function sqliteAttempts(db, ids) {
18
22
  return {
23
+ readContinuation: (work, pieceId) => readWorkContinuation(db, work, pieceId) ?? undefined,
24
+ writeContinuation: (work, pieceId, token) => {
25
+ if (!writeWorkContinuation(db, work, pieceId, token))
26
+ throw new Error("Could not persist the submitted narration job");
27
+ },
19
28
  start: (attempt) => {
29
+ if (attempt.work !== undefined)
30
+ return startWorkAttempt(db, ids, attempt, attempt.work, attempt.workPieceId ?? null, attempt.operation);
20
31
  const id = ids.next();
21
32
  db.prepare("INSERT INTO attempts (id, stage_id, piece_id, n, started_at) VALUES (?, ?, ?, ?, ?)").run(id, attempt.stageId, attempt.pieceId, attempt.n, attempt.startedAt);
22
33
  // The stage shows the attempt count beside the error. A stage with pieces (twenty images)
@@ -38,6 +49,9 @@ export function attemptsOf(db, stageId) {
38
49
  function toAttempt(row) {
39
50
  return {
40
51
  id: row.id,
52
+ revisionId: row.revision_id,
53
+ workId: row.work_id,
54
+ workPieceId: row.work_piece_id,
41
55
  stageId: row.stage_id,
42
56
  pieceId: row.piece_id,
43
57
  n: row.n,
@@ -47,3 +61,4 @@ function toAttempt(row) {
47
61
  errorText: row.error_text,
48
62
  };
49
63
  }
64
+ export { readWorkContinuation, startWorkAttempt, writeWorkContinuation };
@@ -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),