@animastor/orchestration 0.1.5 → 0.1.7
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/package.json +1 -1
- package/src/orchestration/orchestrator.js +29 -10
- package/src/orchestration/scene-callbacks.js +3 -55
- package/src/orchestration/scene-orchestrator.js +7 -70
- package/src/runtime/dispatch-engine.js +5 -310
- package/src/runtime/hub-cancel-port.js +0 -6
- package/src/runtime/persistence-port.js +5 -7
- package/src/runtime/reconciliation-engine.js +6 -57
- package/src/runtime/runtime-scheduler.js +1 -18
- package/src/runtime/scene-window.js +0 -28
- package/test/stage146-d1-artifact-naming.test.js +103 -0
- package/test/stage147-a1-null-build-gate.test.js +211 -0
- package/src/runtime/dispatch-outcomes.js +0 -124
- package/test/capacity-deferral.test.js +0 -265
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@animastor/orchestration",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "Animastor Orchestration — the generation orchestration/runtime contour of the Animastor backend, physically extracted from the host (§32.30): the unified generation lifecycle owner (orchestrator facade, stage executor, scene callbacks/restoration), the dispatch engine and its lease/quota/retry managers, the reconciliation engine and counter reconciliation, the scene window/scheduler/worker-health, the runtime-result emitter/consumer, and the eight O-2..O-10 port contracts (Persistence, SceneData, PlaceholderAudio, ProgressEvents, AudioFsm, VideoFsm, HubCancel, LayerConfig). The package reaches the host (runtime config slices, Redis-domain state layer, media facades, the S-5 seam registry, the artifact root, the PW-2 workspace resolver) ONLY through the composition-root host-bindings seam — no backend/src require, no storage/book/routes/services, no Redis, no PG, no Express inside.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"animastor",
|
|
@@ -20,6 +20,10 @@
|
|
|
20
20
|
|
|
21
21
|
// S-2: media-type knowledge (stage lists, fail-event map) resolved via registry
|
|
22
22
|
const mediaRegistry = require('@animastor/generation').mediaRegistry;
|
|
23
|
+
// S-4: filename grammar composed from the canonical owner (bytes unchanged).
|
|
24
|
+
// D1 (stage146): must live at module scope — resetScenes() uses it outside the
|
|
25
|
+
// beginStage() body, so a per-function declaration threw ReferenceError there.
|
|
26
|
+
const artifactNaming = require('@animastor/generation').artifactNaming;
|
|
23
27
|
// S-5: pure FSM-writer ownership (markDirtyScene/setScene*) — re-exported from
|
|
24
28
|
// the state layer; bodies moved verbatim (see state/scene-state-ops.js header).
|
|
25
29
|
const { lazyHostBinding } = require('../host/host-bindings');
|
|
@@ -56,8 +60,6 @@ async function planScene(redis, bookId, chapterId, sceneId) {
|
|
|
56
60
|
// для исполнения генерации).
|
|
57
61
|
async function beginStage(redis, scene, loadedBook, buildId, stage) {
|
|
58
62
|
const dispatchEngine = require('../runtime/dispatch-engine');
|
|
59
|
-
// S-4: filename grammar composed from the canonical owner (bytes unchanged)
|
|
60
|
-
const artifactNaming = require('@animastor/generation').artifactNaming;
|
|
61
63
|
const bookId = scene.book_id;
|
|
62
64
|
const chapterId = scene.chapter_id;
|
|
63
65
|
const sceneId = scene.scene_id;
|
|
@@ -157,20 +159,37 @@ async function completeStage(redis, bookId, chapterId, sceneId, stage, buildId,
|
|
|
157
159
|
|
|
158
160
|
if (sceneRows.length === 1) {
|
|
159
161
|
const sv = sceneRows[0];
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
if (asset) {
|
|
162
|
+
const versionsConfirm = (row) => {
|
|
163
163
|
const contentVersionConfirmed =
|
|
164
|
-
|
|
164
|
+
row.scene_content_version != null &&
|
|
165
165
|
sv.content_version != null &&
|
|
166
|
-
|
|
166
|
+
row.scene_content_version >= sv.content_version;
|
|
167
167
|
const audioVersionConfirmed =
|
|
168
168
|
stage !== 'audio' || (
|
|
169
|
-
|
|
169
|
+
row.scene_audio_config_version != null &&
|
|
170
170
|
sv.audio_config_version != null &&
|
|
171
|
-
|
|
171
|
+
row.scene_audio_config_version >= sv.audio_config_version
|
|
172
172
|
);
|
|
173
|
-
|
|
173
|
+
return contentVersionConfirmed && audioVersionConfirmed;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
let asset = await persist('sceneAssets.getAsset')(bookId, chapterId, sceneId, stage, buildId);
|
|
177
|
+
|
|
178
|
+
if (!asset) {
|
|
179
|
+
// A1 (stage147): единственная строка ключа может быть
|
|
180
|
+
// легаси-строкой с build_id = NULL (Stage 73/76 семейство):
|
|
181
|
+
// getAsset(buildId) её не видит, а markReady — единственный
|
|
182
|
+
// писатель build_id — вызывается ПОСЛЕ gate (замкнутый цикл
|
|
183
|
+
// DIRTY каждые 5 с, stage146 §5.4). РОВНО одно дополнительное
|
|
184
|
+
// чтение строки того же ключа с build_id IS NULL — не строки
|
|
185
|
+
// другого билда и не «последней любой». Версии проверяются
|
|
186
|
+
// теми же правилами; после пропуска markReady проставит
|
|
187
|
+
// build_id по существующей логике (buildIdFromArtifactPath).
|
|
188
|
+
asset = await persist('sceneAssets.getAssetNullBuild')(bookId, chapterId, sceneId, stage);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (asset) {
|
|
192
|
+
shouldWriteReady = versionsConfirm(asset);
|
|
174
193
|
}
|
|
175
194
|
} else if (sceneRows.length === 0) {
|
|
176
195
|
// Legacy-книга без version-схемы — READY разрешён (сцена
|
|
@@ -277,17 +277,9 @@ async function handleImageCompleted(redis, bookId, chapterId, sceneId, buildId)
|
|
|
277
277
|
// T1: возвращает { ok, retryable, reason, artifact }
|
|
278
278
|
log(`IMAGE_CALLBACK: ${bookId}/${chapterId}/${sceneId}`);
|
|
279
279
|
|
|
280
|
-
|
|
281
|
-
// path every host stub knows), then — flag on and scan empty — the
|
|
282
|
-
// published pointer of this scene build as the completion proof.
|
|
283
|
-
let sceneImage = image.resolveCanonicalSceneImage(
|
|
280
|
+
const sceneImage = image.resolveCanonicalSceneImage(
|
|
284
281
|
artifactRoot(), buildId, bookId, chapterId, sceneId
|
|
285
282
|
);
|
|
286
|
-
if (!sceneImage && typeof image.resolveVersionedSceneProof === 'function') {
|
|
287
|
-
sceneImage = await image.resolveVersionedSceneProof(
|
|
288
|
-
artifactRoot(), buildId, bookId, chapterId, sceneId
|
|
289
|
-
);
|
|
290
|
-
}
|
|
291
283
|
|
|
292
284
|
// Per-asset check
|
|
293
285
|
const assetStates = await state.getAssetStates(redis, bookId, chapterId, sceneId);
|
|
@@ -342,28 +334,7 @@ async function handleImageCompleted(redis, bookId, chapterId, sceneId, buildId)
|
|
|
342
334
|
const stillPending = [];
|
|
343
335
|
for (const uid of dirtyIds) {
|
|
344
336
|
const pngPath = path.join(buildDir, artifactNaming.sceneImageName(bookId, chapterId, sceneId, uid));
|
|
345
|
-
if (fs.existsSync(pngPath))
|
|
346
|
-
// ── Stage 143a (phase 2a · N1) ─────────────────────
|
|
347
|
-
// A versioned-only build never writes the canonical file, so
|
|
348
|
-
// the published pointer is the completion proof for this
|
|
349
|
-
// unit. Without it the dirty marker is never cleared and the
|
|
350
|
-
// unit is force-regenerated on EVERY reconcile pass (infinite
|
|
351
|
-
// regeneration loop with the flag on).
|
|
352
|
-
// Flag OFF ⇒ the facade answers null with zero PG calls → the
|
|
353
|
-
// legacy check above decides, exactly as before.
|
|
354
|
-
// A pointer-store ERROR keeps the unit pending (never cleared
|
|
355
|
-
// on an unknown verdict) and is logged.
|
|
356
|
-
let provenByPointer = false;
|
|
357
|
-
try {
|
|
358
|
-
if (image && typeof image.resolveIuImagePath === 'function') {
|
|
359
|
-
const published = await image.resolveIuImagePath(
|
|
360
|
-
artifactRoot(), buildId, bookId, chapterId, sceneId, uid);
|
|
361
|
-
provenByPointer = !!(published && published.status === 'ok');
|
|
362
|
-
}
|
|
363
|
-
} catch (pgErr) {
|
|
364
|
-
warn(`[DIRTY-UNITS] pointer proof unavailable for ${uid}: ${pgErr.message} — keeping it pending`);
|
|
365
|
-
}
|
|
366
|
-
if (!provenByPointer) {
|
|
337
|
+
if (!fs.existsSync(pngPath)) {
|
|
367
338
|
stillPending.push(uid);
|
|
368
339
|
}
|
|
369
340
|
}
|
|
@@ -372,31 +343,8 @@ async function handleImageCompleted(redis, bookId, chapterId, sceneId, buildId)
|
|
|
372
343
|
log(`[DIRTY-UNITS-CLEARED] ${bookId}/${chapterId}/${sceneId}: all ${dirtyIds.length} dirty unit(s) completed, cleared`);
|
|
373
344
|
try {
|
|
374
345
|
const iuPrefix = artifactNaming.iuImagePrefix(bookId, chapterId, sceneId);
|
|
375
|
-
const scenePrefix = artifactNaming.scenePrefix(bookId, chapterId, sceneId);
|
|
376
346
|
const allFiles = fs.existsSync(buildDir) ? fs.readdirSync(buildDir) : [];
|
|
377
|
-
|
|
378
|
-
// Ready units = canonical file names ∪ published pointer
|
|
379
|
-
// ids (versioned files are invisible to this scan). The
|
|
380
|
-
// counter is a DISPLAY source (consumers clamp it), so a
|
|
381
|
-
// pointer-store error here only logs and leaves the
|
|
382
|
-
// legacy count — never a readiness decision.
|
|
383
|
-
const readyIds = new Set();
|
|
384
|
-
for (const f of allFiles) {
|
|
385
|
-
if (f.startsWith(iuPrefix) && f.endsWith('.png')) {
|
|
386
|
-
readyIds.add(f.startsWith(`${scenePrefix}_`)
|
|
387
|
-
? f.slice(scenePrefix.length + 1, -4)
|
|
388
|
-
: f);
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
if (image && typeof image.listPublishedIuIdsForScene === 'function') {
|
|
392
|
-
try {
|
|
393
|
-
const publishedIds = await image.listPublishedIuIdsForScene(buildId, bookId, chapterId, sceneId);
|
|
394
|
-
for (const id of publishedIds) readyIds.add(String(id));
|
|
395
|
-
} catch (pgErr) {
|
|
396
|
-
warn(`[IU-COUNTER-RESET] published ids unavailable for ${bookId}/${chapterId}/${sceneId}: ${pgErr.message} — counting canonical files only`);
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
const iuFileCount = readyIds.size;
|
|
347
|
+
const iuFileCount = allFiles.filter(f => f.startsWith(iuPrefix) && f.endsWith('.png')).length;
|
|
400
348
|
if (iuFileCount > 0) {
|
|
401
349
|
const progKey = `animastor:iu-progress:${bookId}:${chapterId}:${sceneId}:image`;
|
|
402
350
|
await redis.set(progKey, String(iuFileCount), 'EX', 14400);
|
|
@@ -13,15 +13,10 @@ const chunks = lazyHostBinding('media.audio.chunks');
|
|
|
13
13
|
// orchestrator never requires gpu-dispatcher or the workflow connector.
|
|
14
14
|
const provider = require('@animastor/generation').comfyuiProvider;
|
|
15
15
|
const runtimeScheduler = require('../runtime/runtime-scheduler');
|
|
16
|
-
// Stage 118 (M3): the shared outcome taxonomy — the dispatch engine (runtime
|
|
17
|
-
// tier) and the stage executors (orchestration tier) must classify a
|
|
18
|
-
// "nothing was enqueued" outcome IDENTICALLY.
|
|
19
|
-
const { classifyNotSentReasons } = require('../runtime/dispatch-outcomes');
|
|
20
16
|
// O-2: persistence arrives ONLY through the PersistencePort (host adapter:
|
|
21
17
|
// storage/runtime-persistence-adapter) — the scene-assets repository left
|
|
22
18
|
// this file. SQL lives host-side.
|
|
23
19
|
const persist = require('../runtime/persistence-port').persist;
|
|
24
|
-
const isPersistencePortWired = require('../runtime/persistence-port').isPersistencePortWired;
|
|
25
20
|
// O-3: scene content arrives ONLY through the SceneDataPort (host adapter:
|
|
26
21
|
// storage/scene-data-adapter) — the Book Model facade (../book →
|
|
27
22
|
// @animastor/vbook-runtime) left this file. Aliased `sceneDataPort` because
|
|
@@ -305,22 +300,8 @@ async function executeAudioDispatch(redis, scene, loadedBook, buildId, dispatchI
|
|
|
305
300
|
const completion = await completeStage(redis, bookId, chapterId, sceneId, 'audio', buildId, dispatchId);
|
|
306
301
|
return { dispatched: false, jobs: 0, completed: completion.completed, reason: 'no_segments' };
|
|
307
302
|
} else {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
// ambiguous POST) so the dispatch engine can apply a BOUNDED deferral
|
|
311
|
-
// instead of a fatal cancel.
|
|
312
|
-
//
|
|
313
|
-
// Stage 118 (B3): the merged (pure-dialogue) path and the per-segment
|
|
314
|
-
// path must produce the SAME classification — the merged branch used
|
|
315
|
-
// to leave the reason empty, so a pure-dialogue scene kept the
|
|
316
|
-
// 5-second retry loop. `deferReason` is computed by the host (audio
|
|
317
|
-
// generation) for EVERY path; the fallback below keeps the historic
|
|
318
|
-
// 'no_jobs_generated' semantics when the host could not classify it.
|
|
319
|
-
const deferReason = (result && result.reason === 'no_jobs_accepted' && result.deferReason)
|
|
320
|
-
? result.deferReason
|
|
321
|
-
: 'no_jobs_generated';
|
|
322
|
-
warn(`AUDIO_DISPATCH: generateSceneAudio did not send jobs for ${bookId}/${chapterId}/${sceneId} (${deferReason})`);
|
|
323
|
-
return { dispatched: false, jobs: 0, reason: deferReason, retryable: true };
|
|
303
|
+
warn(`AUDIO_DISPATCH: generateSceneAudio did not send jobs for ${bookId}/${chapterId}/${sceneId}`);
|
|
304
|
+
return { dispatched: false, jobs: 0, reason: 'no_jobs_generated' };
|
|
324
305
|
}
|
|
325
306
|
}
|
|
326
307
|
|
|
@@ -371,11 +352,8 @@ async function executeImageDispatch(redis, scene, loadedBook, buildId, dispatchI
|
|
|
371
352
|
|
|
372
353
|
// T3: generateSceneIUImages возвращает количество отправленных jobs
|
|
373
354
|
let jobsSent = 0;
|
|
374
|
-
// Stage 116 (D3): the result is needed AFTER the try/catch (the deferral
|
|
375
|
-
// reason) — it must be visible outside the block.
|
|
376
|
-
let genResult = null;
|
|
377
355
|
try {
|
|
378
|
-
genResult = await image.generateSceneIUImages(
|
|
356
|
+
const genResult = await image.generateSceneIUImages(
|
|
379
357
|
redis,
|
|
380
358
|
sceneData,
|
|
381
359
|
bookData,
|
|
@@ -399,16 +377,6 @@ async function executeImageDispatch(redis, scene, loadedBook, buildId, dispatchI
|
|
|
399
377
|
return { dispatched: true, jobs: jobsSent, reason: null };
|
|
400
378
|
}
|
|
401
379
|
|
|
402
|
-
// Stage 116 (D3) / Stage 118 (M3): the executor sent NOTHING. That is
|
|
403
|
-
// not a lost task and not a fatal cancellation — it is a bounded
|
|
404
|
-
// deferral. The precise reason (no live worker / everything already
|
|
405
|
-
// queued / ambiguous POST) is carried through so the deferral can be
|
|
406
|
-
// classified by the shared taxonomy (runtime/dispatch-outcomes).
|
|
407
|
-
if (genResult && genResult.deferReason) {
|
|
408
|
-
warn(`IMAGE_DISPATCH: ${bookId}/${chapterId}/${sceneId} — nothing enqueued (${genResult.deferReason}); deferring`);
|
|
409
|
-
return { dispatched: false, jobs: 0, reason: genResult.deferReason, retryable: true };
|
|
410
|
-
}
|
|
411
|
-
|
|
412
380
|
log(`IMAGE_DISPATCH: ${bookId}/${chapterId}/${sceneId} — no jobs sent (cache hit or empty units)`);
|
|
413
381
|
return { dispatched: false, jobs: 0, reason: 'no_jobs_sent' };
|
|
414
382
|
}
|
|
@@ -583,41 +551,15 @@ async function executeVideoDispatch(redis, scene, loadedBook, buildId, dispatchI
|
|
|
583
551
|
await videoOrch.setWaitingChunks(redis, bookId, chapterId, sceneId);
|
|
584
552
|
|
|
585
553
|
let sentCount = 0;
|
|
586
|
-
// Stage 116 (D3) / Stage 118 (M3): collect WHY a job was not sent — the
|
|
587
|
-
// dominant reason becomes the deferral reason (bounded backoff) instead
|
|
588
|
-
// of the generic 'send_failed'.
|
|
589
|
-
const notSentReasons = new Map();
|
|
590
554
|
for (const jobSpec of jobsToSend) {
|
|
591
555
|
try {
|
|
592
|
-
|
|
593
|
-
// Stage 122 (N2): the group file was probed absent above (a cached
|
|
594
|
-
// group never reaches this loop), so an `already_completed` verdict
|
|
595
|
-
// is by construction an ORPHANED completion marker. Free it and
|
|
596
|
-
// retry ONCE so the video regenerates instead of looping on a
|
|
597
|
-
// 900 s deferral with the scene stuck in the active index (audit
|
|
598
|
-
// 121 · N2). The release rides the persistence port — the
|
|
599
|
-
// orchestration tier cannot require the backend helper directly.
|
|
600
|
-
// Skipped when the port is unwired (harnesses that don't exercise
|
|
601
|
-
// orphan cleanup); production always wires it in backend.cjs.
|
|
602
|
-
if (isPersistencePortWired()) {
|
|
603
|
-
let released = false;
|
|
604
|
-
try {
|
|
605
|
-
released = await persist('idem.releaseTaskDoneIfOrphan')(redis, sendResult);
|
|
606
|
-
} catch (_) { released = false; }
|
|
607
|
-
if (released) {
|
|
608
|
-
log(`VIDEO_DISPATCH: orphaned completion marker released for ${jobSpec.job_id} — regenerating`);
|
|
609
|
-
sendResult = await provider.generate(jobSpec);
|
|
610
|
-
}
|
|
611
|
-
}
|
|
556
|
+
const sendResult = await provider.generate(jobSpec);
|
|
612
557
|
if (sendResult.sent) {
|
|
613
558
|
sentCount++;
|
|
614
559
|
} else {
|
|
615
|
-
|
|
616
|
-
notSentReasons.set(reason, (notSentReasons.get(reason) || 0) + 1);
|
|
617
|
-
warn(`VIDEO_DISPATCH: Hub rejected job ${jobSpec.job_id}: ${sendResult.error || reason}`);
|
|
560
|
+
warn(`VIDEO_DISPATCH: Hub rejected job ${jobSpec.job_id}: ${sendResult.error || 'unknown'}`);
|
|
618
561
|
}
|
|
619
562
|
} catch (sendErr) {
|
|
620
|
-
notSentReasons.set('send_failed', (notSentReasons.get('send_failed') || 0) + 1);
|
|
621
563
|
warn(`VIDEO_DISPATCH: provider generate failed for job ${jobSpec.job_id}: ${sendErr.message}`);
|
|
622
564
|
}
|
|
623
565
|
}
|
|
@@ -627,13 +569,8 @@ async function executeVideoDispatch(redis, scene, loadedBook, buildId, dispatchI
|
|
|
627
569
|
return { dispatched: true, jobs: sentCount, reason: null };
|
|
628
570
|
}
|
|
629
571
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
// a single real failure (send_failed) keeps the historic 'send_failed'
|
|
633
|
-
// semantics (an immediate retry on the next tick).
|
|
634
|
-
const deferReason = classifyNotSentReasons(notSentReasons) || 'send_failed';
|
|
635
|
-
warn(`VIDEO_DISPATCH: ${bookId}/${chapterId}/${sceneId}: nothing enqueued (${deferReason})`);
|
|
636
|
-
return { dispatched: false, jobs: 0, reason: deferReason, retryable: true };
|
|
572
|
+
warn(`VIDEO_DISPATCH: ${bookId}/${chapterId}/${sceneId}: all provider.generate calls failed`);
|
|
573
|
+
return { dispatched: false, jobs: 0, reason: 'send_failed' };
|
|
637
574
|
}
|
|
638
575
|
|
|
639
576
|
// T4: dispatchStage принимает dispatchId (из dispatch-engine) и передаёт executors
|