@animastor/orchestration 0.3.1 → 0.3.2
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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.2 (2026-11-02) — эпоха dispatch в audio-orch, без ложного FAILED
|
|
4
|
+
|
|
5
|
+
Основание: `reports/2026-11-02/gpu-e2e-installer-025-logs.md` §6.1, §8 п.3, п.5 —
|
|
6
|
+
состояние audio-orch отменённой попытки (11:09) пережило отмену, новый
|
|
7
|
+
dispatch (12:25) унаследовал его `started_at`, и watchdog через 28 с после
|
|
8
|
+
dispatch'а пометил здоровый прогон FAILED (`chunk_stall_timeout:4619s`), затем
|
|
9
|
+
`[FAIL-STAGE] Rejected … missing_dispatch_id` и `[INVARIANT] 1 audio-orch violations`.
|
|
10
|
+
Описание: animastor-docs `architecture/COMMAND_ONLY_GENERATION.md` §«Эпоха dispatch».
|
|
11
|
+
|
|
12
|
+
### Changed (поведение)
|
|
13
|
+
|
|
14
|
+
- `scene-orchestrator.executeAudioDispatch` открывает новую эпоху на каждом
|
|
15
|
+
dispatch: `setWaitingChunks(…, { dispatchId, buildId, expectedCount })`.
|
|
16
|
+
Хост пишет `started_at=now`, `dispatch_id`, `expected_count`,
|
|
17
|
+
`chunks_received=0` и убирает `fail_reason`/`failed_at`/`last_chunk_at`
|
|
18
|
+
прошлой попытки. Хост без поддержки 5-го аргумента ведёт себя как раньше.
|
|
19
|
+
- `reconciliation-engine.checkStalledAudioScenes`: запись проваливается, только
|
|
20
|
+
если её `dispatch_id` равен живому `dispatch-meta` сцены. Запись без
|
|
21
|
+
`dispatch_id` или другого dispatch — `stale_state_ignored` (одна info-строка
|
|
22
|
+
на запись, не FAILED). `failWaitingScene` получает `dispatchId` записи.
|
|
23
|
+
- `orchestrator.failStage` без `dispatchId` → `{failed:false,
|
|
24
|
+
reason:'stale_state_ignored'}` (info), а не `Rejected … missing_dispatch_id`.
|
|
25
|
+
|
|
26
|
+
Fence, command-gate (Stage 155) и retry-политика не меняются.
|
|
27
|
+
|
|
3
28
|
## 0.3.1 (2026-11-01) — результат GPU обрабатывается один раз
|
|
4
29
|
|
|
5
30
|
Основание: `reports/2026-11-01/gpu-audio-image-run-analysis.md`, P1 — каждый
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@animastor/orchestration",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
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",
|
|
@@ -288,6 +288,14 @@ async function failStage(redis, bookId, chapterId, sceneId, stage, buildId, reas
|
|
|
288
288
|
throw new Error(`orchestrator.failStage: unknown stage '${stage}'`);
|
|
289
289
|
}
|
|
290
290
|
|
|
291
|
+
// GPU E2E 2026-11-02 (§6.1): a fail signal without dispatch_id comes
|
|
292
|
+
// from a legacy / stale record (no epoch) — it can never fail the live
|
|
293
|
+
// dispatch. A regular no-op, not a reject.
|
|
294
|
+
if (!dispatchId || typeof dispatchId !== 'string') {
|
|
295
|
+
log(`[FAIL-STAGE] stale_state_ignored ${bookId}/${chapterId}/${sceneId} ${stage}: no dispatch_id (reason=${reason})`);
|
|
296
|
+
return { failed: false, reason: 'stale_state_ignored' };
|
|
297
|
+
}
|
|
298
|
+
|
|
291
299
|
const identity = await dispatchEngine.verifyDispatchIdentity(
|
|
292
300
|
redis,
|
|
293
301
|
bookId,
|
|
@@ -280,7 +280,16 @@ async function executeAudioDispatch(redis, scene, loadedBook, buildId, dispatchI
|
|
|
280
280
|
// jobs, иначе первый чанк может вернуться быстрее чем setWaitingChunks выполнится,
|
|
281
281
|
// и completeChunk увидит фазу GENERATING → early return → чанк на диске есть,
|
|
282
282
|
// но merge не запускается → retry exhausting → failStage → re-dispatch → цикл.
|
|
283
|
-
|
|
283
|
+
//
|
|
284
|
+
// GPU E2E 2026-11-02 (§6.1): every dispatch opens a NEW audio-orch epoch
|
|
285
|
+
// (started_at=now, dispatch_id, expected_count; fail_reason/failed_at/
|
|
286
|
+
// last_chunk_at of an earlier or cancelled attempt are dropped). The stall
|
|
287
|
+
// watchdog compares this dispatch_id with the live dispatch metadata.
|
|
288
|
+
await audioFsmOp('setWaitingChunks')(redis, bookId, chapterId, sceneId, {
|
|
289
|
+
dispatchId,
|
|
290
|
+
buildId,
|
|
291
|
+
expectedCount: segments.buildSegments(sceneData).length,
|
|
292
|
+
});
|
|
284
293
|
|
|
285
294
|
const result = await audio.generateSceneAudio(redis, sceneData, bookData, buildId, bookId, dispatchId);
|
|
286
295
|
|
|
@@ -473,6 +473,33 @@ async function checkPartialBuilds(redis, bookId, chapterId, sceneId) {
|
|
|
473
473
|
// Важно: watchdog НЕ конкурирует с gpu-hub timeout — они ловят
|
|
474
474
|
// разные сценарии (стоп прогресса vs мёртвый воркер).
|
|
475
475
|
|
|
476
|
+
// GPU E2E 2026-11-02 (§6.1) — dispatch epoch of an audio-orch record.
|
|
477
|
+
// current=true only when the record carries a dispatch_id AND it equals the
|
|
478
|
+
// live dispatch metadata of the scene's audio stage. A metadata read error is
|
|
479
|
+
// "cannot prove" → not current (the watchdog never fails on a guess).
|
|
480
|
+
async function _audioDispatchEpoch(redis, bookId, chapterId, sceneId, orchState) {
|
|
481
|
+
if (!orchState || !orchState.dispatch_id) return { current: false, reason: 'no_dispatch_id' };
|
|
482
|
+
let meta = null;
|
|
483
|
+
try {
|
|
484
|
+
meta = await require('./dispatch-engine').getDispatchMetadata(redis, bookId, chapterId, sceneId, 'audio');
|
|
485
|
+
} catch (_) {
|
|
486
|
+
return { current: false, reason: 'dispatch_metadata_unreadable' };
|
|
487
|
+
}
|
|
488
|
+
if (!meta || !meta.dispatch_id) return { current: false, reason: 'no_active_dispatch' };
|
|
489
|
+
if (meta.dispatch_id !== orchState.dispatch_id) return { current: false, reason: 'other_dispatch' };
|
|
490
|
+
return { current: true, reason: 'current_dispatch' };
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// One info line per stale record (scene + its epoch), not one per cycle.
|
|
494
|
+
const _staleAudioOrchLogged = new Set();
|
|
495
|
+
function _logStaleAudioOrchOnce(sceneRef, orchState, reason) {
|
|
496
|
+
const k = `${sceneRef}|${orchState.dispatch_id || ''}|${orchState.started_at || ''}`;
|
|
497
|
+
if (_staleAudioOrchLogged.has(k)) return;
|
|
498
|
+
if (_staleAudioOrchLogged.size > 5000) _staleAudioOrchLogged.clear();
|
|
499
|
+
_staleAudioOrchLogged.add(k);
|
|
500
|
+
log(`[STALLED-AUDIO] ${sceneRef} — stale_state_ignored (${reason}): the record is not of the live dispatch, not failing it`);
|
|
501
|
+
}
|
|
502
|
+
|
|
476
503
|
async function checkStalledAudioScenes(redis, deps) {
|
|
477
504
|
// O-7: the audio FSM is consumed through the AudioFsmPort (host adapter:
|
|
478
505
|
// storage/audio-fsm-adapter). The pre-O-7 optional-load semantics are
|
|
@@ -513,12 +540,24 @@ async function checkStalledAudioScenes(redis, deps) {
|
|
|
513
540
|
const age = Date.now() - threshold;
|
|
514
541
|
if (age < AUDIO_CHUNK_STALL_MS) continue;
|
|
515
542
|
|
|
543
|
+
// GPU E2E 2026-11-02 (§6.1): the record must belong to the LIVE
|
|
544
|
+
// dispatch. A record without dispatch_id (legacy / pre-epoch) or
|
|
545
|
+
// of another dispatch is a stale epoch — failing it marked a
|
|
546
|
+
// healthy run FAILED (`chunk_stall_timeout:4619s`). It is ignored
|
|
547
|
+
// (one info line per record), never failed.
|
|
548
|
+
const epoch = await _audioDispatchEpoch(redis, bookId, chapterId, sceneId, orchState);
|
|
549
|
+
if (!epoch.current) {
|
|
550
|
+
_logStaleAudioOrchOnce(`${bookId}/${chapterId}/${sceneId}`, orchState, epoch.reason);
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
|
|
516
554
|
const reason = `chunk_stall_timeout:${Math.round(age / 1000)}s`;
|
|
517
|
-
warn(`[STALLED-AUDIO] ${bookId}/${chapterId}/${sceneId} — ${reason} (last_chunk_at=${!!lastChunkAt} started_at=${!!startedAt})`);
|
|
555
|
+
warn(`[STALLED-AUDIO] ${bookId}/${chapterId}/${sceneId} — ${reason} (last_chunk_at=${!!lastChunkAt} started_at=${!!startedAt} dispatch=${String(orchState.dispatch_id).slice(0, 24)})`);
|
|
518
556
|
|
|
519
557
|
try {
|
|
520
558
|
await audioOrch.failWaitingScene(redis, bookId, chapterId, sceneId, buildId, reason, {
|
|
521
|
-
orchestrator: deps.orchestrator
|
|
559
|
+
orchestrator: deps.orchestrator,
|
|
560
|
+
dispatchId: orchState.dispatch_id,
|
|
522
561
|
});
|
|
523
562
|
stalled++;
|
|
524
563
|
} catch (err) {
|
|
@@ -551,7 +590,7 @@ async function checkStalledAudioScenes(redis, deps) {
|
|
|
551
590
|
warn(`[STALLED-AUDIO] completeChunk recovery failed: ${err.message}`);
|
|
552
591
|
try {
|
|
553
592
|
await audioOrch.failWaitingScene(redis, bookId, chapterId, sceneId, buildId,
|
|
554
|
-
`recovery_fallback:${err.message}`, { orchestrator: deps.orchestrator });
|
|
593
|
+
`recovery_fallback:${err.message}`, { orchestrator: deps.orchestrator, dispatchId: orchState.dispatch_id });
|
|
555
594
|
stalled++;
|
|
556
595
|
} catch (fsErr) {
|
|
557
596
|
warn(`[STALLED-AUDIO] failWaitingScene fallback failed: ${fsErr.message}`);
|
|
@@ -561,7 +600,7 @@ async function checkStalledAudioScenes(redis, deps) {
|
|
|
561
600
|
warn(`[STALLED-AUDIO] ${bookId}/${chapterId}/${sceneId} — ${presentCount}/${expectedCount} on disk, no last_chunk_at. Partial chunks, failing scene.`);
|
|
562
601
|
try {
|
|
563
602
|
await audioOrch.failWaitingScene(redis, bookId, chapterId, sceneId, buildId,
|
|
564
|
-
`partial_chunks:${presentCount}/${expectedCount}_no_timestamps`, { orchestrator: deps.orchestrator });
|
|
603
|
+
`partial_chunks:${presentCount}/${expectedCount}_no_timestamps`, { orchestrator: deps.orchestrator, dispatchId: orchState.dispatch_id });
|
|
565
604
|
stalled++;
|
|
566
605
|
} catch (err) {
|
|
567
606
|
warn(`[STALLED-AUDIO] failWaitingScene failed: ${err.message}`);
|