@animastor/orchestration 0.3.2 → 0.3.4
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,61 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.4 (2026-11-04) — 0.3.3 + «ready» аудио только при коммите GPU
|
|
4
|
+
|
|
5
|
+
Одна публикация вместо двух: 0.3.3 (уборка хаба при `/regenerate`, см. ниже) **не
|
|
6
|
+
публиковался** в npm отдельно и входит в 0.3.4 целиком. Основание:
|
|
7
|
+
`reports/2026-11-04/audio-placeholder-ready.md` (сцена `sc-4bb85e02` — тишина-заглушка
|
|
8
|
+
стала `ready` без GPU), интеграция: `reports/2026-11-04/integration-A-B-C.md`.
|
|
9
|
+
Описание инварианта: animastor-docs `02-orchestration/AUDIO_READY_SEMANTICS.md`.
|
|
10
|
+
|
|
11
|
+
### Changed (поведение)
|
|
12
|
+
|
|
13
|
+
- `scene-orchestrator.executeAudioDispatch`: `AUDIO_ALREADY_DONE` пропускает работу только
|
|
14
|
+
если хост подтверждает коммит (`audio.isSceneAudioCommitted(...).ready === true`:
|
|
15
|
+
канонический файл без маркера заглушки **и** чанки сцены на диске). Иначе — лог
|
|
16
|
+
`AUDIO_ALREADY_DONE rejected … DONE without a committed audio chunk`, FSM-состояние
|
|
17
|
+
удаляется, стадия идёт обычным путём (dispatch на GPU). То же — в safety-net ветке
|
|
18
|
+
«stale `DONE`». Пробник недоступен или упал → считается «не закоммичено» (fail-closed:
|
|
19
|
+
лишняя генерация безопасна, пропуск — нет).
|
|
20
|
+
- `scene-callbacks.handleAudioCompleted`: проверка после завершения — `isSceneAudioCommitted`
|
|
21
|
+
(legacy `isSceneAudioReady` только если хост не дал нового пробника); в
|
|
22
|
+
`replacePlaceholderWithRealAudio` передаётся `{ committed: true }` — хост без этого
|
|
23
|
+
доказательства не поднимает запись в `ready`.
|
|
24
|
+
|
|
25
|
+
### Host contract (новое, обратно совместимо)
|
|
26
|
+
|
|
27
|
+
- `media.audio.isSceneAudioCommitted(buildId, bookId, chapterId, sceneId, expectedCount|null)`
|
|
28
|
+
→ `{ ready, reason, chunks, path }`. Backend ≥ интеграции A+B его даёт; на старом хосте
|
|
29
|
+
пакет откатывается к file-only проверке.
|
|
30
|
+
- `placeholderAudio.replacePlaceholderWithRealAudio(…, realDuration, options)` — 7-й аргумент
|
|
31
|
+
`options.committed`.
|
|
32
|
+
|
|
33
|
+
Инварианты 0.3.3 (sweep только своих dispatch-id, fail-closed book-wide sweep) не менялись;
|
|
34
|
+
fence, command-gate и retry-политика — тоже.
|
|
35
|
+
|
|
36
|
+
## 0.3.3 (2026-11-04, не публиковался — вошёл в 0.3.4) — /regenerate не сносит чужие задачи на хабе
|
|
37
|
+
|
|
38
|
+
Основание: `reports/2026-11-04/gpu-generation-stall.md` §5.3, §9.1 — `/regenerate`
|
|
39
|
+
одной сцены (audio) закончился `clearHubDispatches([], { book_id })` →
|
|
40
|
+
`DELETE /queue/clear?book_id=`; хаб удалил running-запись image-задачи другой
|
|
41
|
+
сцены (результат отклонён 409 и потерян) и queued-задачу (восстановлена
|
|
42
|
+
stale-lease recovery через 7 мин). Отчёт исправления:
|
|
43
|
+
`reports/2026-11-04/regenerate-sweep-fix.md`. Описание: animastor-docs
|
|
44
|
+
`architecture/COMMAND_ONLY_GENERATION.md` §«Уборка хаба при /regenerate».
|
|
45
|
+
|
|
46
|
+
### Changed (поведение)
|
|
47
|
+
|
|
48
|
+
- `dispatch-engine.clearHubDispatches`: book-scoped форма (пустой список id +
|
|
49
|
+
`book_id`) — **fail-closed**: запрос `?book_id=` уходит только при явном
|
|
50
|
+
`allowBookSweep: true`; иначе ни одного запроса, warn
|
|
51
|
+
`book-wide GPU hub sweep … REFUSED`, результат `{requested:0, cleared:0,
|
|
52
|
+
failed:0, refused:1}`. Per-id форма не менялась.
|
|
53
|
+
- `orchestrator.resetScenes` возвращает `dispatchIds` — dispatch-id,
|
|
54
|
+
отменённые этим reset'ом (только сбрасываемые сцена×стадия); вызывающий
|
|
55
|
+
повторно убирает на хабе только их.
|
|
56
|
+
|
|
57
|
+
Fence, command-gate и retry-политика не меняются.
|
|
58
|
+
|
|
3
59
|
## 0.3.2 (2026-11-02) — эпоха dispatch в audio-orch, без ложного FAILED
|
|
4
60
|
|
|
5
61
|
Основание: `reports/2026-11-02/gpu-e2e-installer-025-logs.md` §6.1, §8 п.3, п.5 —
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@animastor/orchestration",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
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",
|
|
@@ -499,7 +499,7 @@ async function resetScenes(redis, bookId, buildId, scenes, layerCfg, options = {
|
|
|
499
499
|
|
|
500
500
|
if (!scenes || scenes.length === 0) {
|
|
501
501
|
log('[RESET-SCENES] No scenes to reset');
|
|
502
|
-
return { marked: 0, reset_scenes: 0 };
|
|
502
|
+
return { marked: 0, reset_scenes: 0, dispatchIds: [] };
|
|
503
503
|
}
|
|
504
504
|
|
|
505
505
|
const scopeDisplay = `${scope}${chapterId ? '/' + chapterId : ''}${sceneId ? '/' + sceneId : ''}`;
|
|
@@ -635,7 +635,10 @@ async function resetScenes(redis, bookId, buildId, scenes, layerCfg, options = {
|
|
|
635
635
|
{ scenes_count: scenes.length, marked: marked.marked }
|
|
636
636
|
).catch(() => {});
|
|
637
637
|
|
|
638
|
-
|
|
638
|
+
// 0.3.3 (reports/2026-11-04/regenerate-sweep-fix.md): the dispatch ids
|
|
639
|
+
// cancelled by THIS reset — the only hub entries a caller may sweep again
|
|
640
|
+
// (a book-wide sweep would remove other scenes' in-flight jobs).
|
|
641
|
+
return { ...marked, reset_scenes: scenes.length, dispatchIds: [...new Set(cancellation.dispatchIds || [])] };
|
|
639
642
|
}
|
|
640
643
|
|
|
641
644
|
// ── S-5: pure FSM-writer ownership moved to the state layer ──
|
|
@@ -109,9 +109,18 @@ async function handleAudioCompleted(redis, bookId, chapterId, sceneId, buildId)
|
|
|
109
109
|
return { ok: false, retryable: false, reason: 'invalid_asset_state' };
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
|
|
112
|
+
// Ready means "a fenced GPU commit of this generation produced the
|
|
113
|
+
// chunks" — NOT "a file exists at the canonical path". A silent
|
|
114
|
+
// placeholder is a valid 10.85 s MP3 and passed the old file-only check,
|
|
115
|
+
// which is how sc-4bb85e02 reached READY with silence on 2026-11-04.
|
|
116
|
+
// The legacy probe remains only for hosts that have not wired the
|
|
117
|
+
// committed-audio probe yet.
|
|
118
|
+
const committedProbe = typeof audio.isSceneAudioCommitted === 'function'
|
|
119
|
+
? await audio.isSceneAudioCommitted(buildId, bookId, chapterId, sceneId, null)
|
|
120
|
+
: { ready: await audio.isSceneAudioReady(buildId, bookId, chapterId, sceneId, require('music-metadata')), reason: 'legacy_file_check', chunks: [] };
|
|
121
|
+
const isReady = !!(committedProbe && committedProbe.ready === true);
|
|
113
122
|
if (!isReady) {
|
|
114
|
-
error(`Audio not ready after completion: ${bookId}/${chapterId}/${sceneId}`);
|
|
123
|
+
error(`Audio not ready after completion: ${bookId}/${chapterId}/${sceneId} (reason=${committedProbe && committedProbe.reason}, chunks=[${(committedProbe && committedProbe.chunks || []).join(',')}])`);
|
|
115
124
|
const scene = { book_id: bookId, chapter_id: chapterId, scene_id: sceneId };
|
|
116
125
|
await logEvent(redis, scene, 'AUDIO_FAILED', 'audio_ready', {
|
|
117
126
|
reason: 'not_ready_validation'
|
|
@@ -149,8 +158,11 @@ async function handleAudioCompleted(redis, bookId, chapterId, sceneId, buildId)
|
|
|
149
158
|
const metadata = await mm.parseFile(audioPath);
|
|
150
159
|
realDuration = metadata.format.duration || 0;
|
|
151
160
|
} catch {}
|
|
161
|
+
// The commit proof travels down: the placeholder service refuses to
|
|
162
|
+
// upgrade a record to 'ready' without it (2026-11-04 regression).
|
|
152
163
|
await placeholderAudioOp('replacePlaceholderWithRealAudio')(
|
|
153
|
-
bookId, chapterId, sceneId, buildId, audioPath, realDuration
|
|
164
|
+
bookId, chapterId, sceneId, buildId, audioPath, realDuration,
|
|
165
|
+
{ committed: isReady }
|
|
154
166
|
);
|
|
155
167
|
} catch (err) {
|
|
156
168
|
warn(`Failed to replace placeholder audio: ${err.message}`);
|
|
@@ -53,6 +53,28 @@ const orchestrator = require('./orchestrator');
|
|
|
53
53
|
// S-2: media registry for executor dispatch
|
|
54
54
|
const mediaRegistry = require('@animastor/generation').mediaRegistry;
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Fenced-commit probe for the audio FSM guards.
|
|
58
|
+
*
|
|
59
|
+
* A DONE record is only authoritative when a real GPU commit backs it. On
|
|
60
|
+
* 2026-11-04 the already_ready fast-track wrote DONE for a scene that had no
|
|
61
|
+
* committed chunk at all, and AUDIO_ALREADY_DONE then swallowed every later
|
|
62
|
+
* "audio" command. Returns false when the probe is unavailable (fail closed
|
|
63
|
+
* for the guard — regenerating is safe, skipping is not).
|
|
64
|
+
*/
|
|
65
|
+
async function hasCommittedAudio(buildId, bookId, chapterId, sceneId, expectedCount) {
|
|
66
|
+
try {
|
|
67
|
+
if (typeof audio.isSceneAudioCommitted === 'function') {
|
|
68
|
+
const probe = await audio.isSceneAudioCommitted(buildId, bookId, chapterId, sceneId, expectedCount || null);
|
|
69
|
+
return !!(probe && probe.ready === true);
|
|
70
|
+
}
|
|
71
|
+
return await audio.isSceneAudioReady(buildId, bookId, chapterId, sceneId);
|
|
72
|
+
} catch (err) {
|
|
73
|
+
warn(`AUDIO_READY_PROBE failed for ${bookId}/${chapterId}/${sceneId}: ${err.message}`);
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
56
78
|
// ======================================================
|
|
57
79
|
// SCENE ORCHESTRATOR
|
|
58
80
|
// ======================================================
|
|
@@ -211,8 +233,16 @@ async function executeAudioDispatch(redis, scene, loadedBook, buildId, dispatchI
|
|
|
211
233
|
const orchState = await audioFsmOp('getState')(redis, bookId, chapterId, sceneId);
|
|
212
234
|
if (orchState) {
|
|
213
235
|
if (orchState.phase === audioFsmPhases().DONE) {
|
|
214
|
-
|
|
215
|
-
|
|
236
|
+
// DONE is only authoritative with a committed chunk behind it.
|
|
237
|
+
if (await hasCommittedAudio(buildId, bookId, chapterId, sceneId, orchState.expected_count)) {
|
|
238
|
+
log(`AUDIO_ALREADY_DONE: ${bookId}/${chapterId}/${sceneId} — skipping dispatch`);
|
|
239
|
+
return { dispatched: false, jobs: 0, completed: true, reason: 'already_done' };
|
|
240
|
+
}
|
|
241
|
+
log(
|
|
242
|
+
`AUDIO_ALREADY_DONE rejected: ${bookId}/${chapterId}/${sceneId} — DONE without a committed audio chunk ` +
|
|
243
|
+
`(expected=${orchState.expected_count || '?'}), resetting state to regenerate`
|
|
244
|
+
);
|
|
245
|
+
await audioFsmOp('deleteState')(redis, bookId, chapterId, sceneId);
|
|
216
246
|
}
|
|
217
247
|
}
|
|
218
248
|
|
|
@@ -234,8 +264,11 @@ async function executeAudioDispatch(redis, scene, loadedBook, buildId, dispatchI
|
|
|
234
264
|
const stalePhase = transResult.from;
|
|
235
265
|
// DONE — дополнительный safety net (выше уже должен был отсечься)
|
|
236
266
|
if (stalePhase === audioFsmPhases().DONE) {
|
|
237
|
-
|
|
238
|
-
|
|
267
|
+
if (await hasCommittedAudio(buildId, bookId, chapterId, sceneId, orchState && orchState.expected_count)) {
|
|
268
|
+
warn(`AUDIO_ORCH: DONE guard prevented stale reset for ${bookId}/${chapterId}/${sceneId}`);
|
|
269
|
+
return { dispatched: false, jobs: 0, completed: true, reason: 'already_done' };
|
|
270
|
+
}
|
|
271
|
+
warn(`AUDIO_ORCH: stale DONE without a committed chunk for ${bookId}/${chapterId}/${sceneId} — resetting to PLACEHOLDER_READY`);
|
|
239
272
|
}
|
|
240
273
|
// WAITING_CHUNKS / MERGING (stale recovery): с A2 timeout formula lease
|
|
241
274
|
// переживает watchdog — если дошли до stale recovery, значит lease истёк
|
|
@@ -1623,8 +1623,11 @@ async function clearLeasesForScenes(redis, bookId, scenes) {
|
|
|
1623
1623
|
*
|
|
1624
1624
|
* The contract now is:
|
|
1625
1625
|
* - ids present → one `?dispatch_id=` DELETE per id (unchanged);
|
|
1626
|
-
* - no ids + book_id
|
|
1626
|
+
* - no ids + book_id + `allowBookSweep: true`
|
|
1627
|
+
* → exactly ONE `?book_id=` DELETE (the whole-book sweep
|
|
1627
1628
|
* the hub already supports and cache/delete paths use);
|
|
1629
|
+
* - no ids + book_id WITHOUT `allowBookSweep` (0.3.3, fail-closed)
|
|
1630
|
+
* → NO request, a warning, `{requested:0, refused:1}`;
|
|
1628
1631
|
* - neither → NO request at all (never a fake HTTP call).
|
|
1629
1632
|
*
|
|
1630
1633
|
* A book-scoped sweep is checked against the hub's echoed `scope.book_id`:
|
|
@@ -1659,6 +1662,19 @@ async function clearHubDispatches(dispatchIds, options = {}) {
|
|
|
1659
1662
|
// ── Book-scoped sweep (no explicit dispatch ids) ──────────────────────
|
|
1660
1663
|
// Exactly one request; the hub filters by record.book_id so only this
|
|
1661
1664
|
// book's queue/running/result entries are removed.
|
|
1665
|
+
//
|
|
1666
|
+
// 0.3.3 FAIL-CLOSED (reports/2026-11-04/regenerate-sweep-fix.md): a
|
|
1667
|
+
// book-wide sweep removes EVERY in-flight job of the book — incident
|
|
1668
|
+
// 2026-11-04: a one-scene audio /regenerate swept a running image job
|
|
1669
|
+
// of another scene (result rejected 409, lost) and a queued one (back
|
|
1670
|
+
// only after 7 min of stale-lease recovery). It is issued ONLY when the
|
|
1671
|
+
// caller states it explicitly (`allowBookSweep: true` — an explicit
|
|
1672
|
+
// cancellation / full restart of the whole book / book deletion).
|
|
1673
|
+
// Otherwise: no request at all, a warning, `refused: 1`.
|
|
1674
|
+
if (ids.length === 0 && bookId && options.allowBookSweep !== true) {
|
|
1675
|
+
reportWarning(`${context}: book-wide GPU hub sweep for ${bookId} REFUSED (no dispatch ids, allowBookSweep not set) — nothing swept`);
|
|
1676
|
+
return { requested: 0, cleared: 0, failed: 0, refused: 1 };
|
|
1677
|
+
}
|
|
1662
1678
|
if (ids.length === 0 && bookId) {
|
|
1663
1679
|
try {
|
|
1664
1680
|
const response = await fetchImpl(
|