@animastor/orchestration 0.3.0 → 0.3.1

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,29 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.1 (2026-11-01) — результат GPU обрабатывается один раз
4
+
5
+ Основание: `reports/2026-11-01/gpu-audio-image-run-analysis.md`, P1 — каждый
6
+ результат обрабатывался дважды (HTTP-push hub → backend, затем Phase A
7
+ reconcile подбирал оставшийся `animastor:result:*` мимо
8
+ `animastor:result-processed:*`). Описание: animastor-docs
9
+ `architecture/GPU_HUB_CONTRACT.md` §«Result ACK».
10
+
11
+ ### Changed (поведение)
12
+
13
+ - `reconciliation-engine.recoverResultKeys` (Phase A): перед
14
+ `handleTaskResult` читает маркер `animastor:result-processed:{job}:{build}`.
15
+ Значение `done:<dispatch_id>` того же dispatch, что в result-ключе, —
16
+ ключ удаляется молча (без лога), handler не вызывается.
17
+ Маркер `'1'` (в работе / старый формат), маркер другого dispatch
18
+ (перегенерация audio/video в пределах TTL) или его отсутствие — как
19
+ раньше, через fenced handler.
20
+ - Терминальный отказ (`no_active_dispatch` и др.) для дубля доставленного
21
+ результата (маркер есть) — уровень info; для сироты (маркера нет) — warn.
22
+ - `orchestrator.resetScenes` вместе с `iu-progress` удаляет
23
+ `animastor:iu-progress-ids:*` (множество IU×fence, хост task-handler).
24
+
25
+ Fence, dedup-ключ маршрута и command-gate (Stage 155) не меняются.
26
+
3
27
  ## 0.3.0 (2026-10-31) — генерация только по явной команде
4
28
 
5
29
  Правило владельца: GPU-генерация запускается только кнопками «аудио» /
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@animastor/orchestration",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
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",
@@ -575,6 +575,8 @@ async function resetScenes(redis, bookId, buildId, scenes, layerCfg, options = {
575
575
 
576
576
  const progKey = `animastor:iu-progress:${bookId}:${ds.chapter_id}:${ds.scene_id}:image`;
577
577
  try { await redis.del(progKey); } catch (_) {}
578
+ // SYNC: backend task-handler — the IU×fence set behind the counter.
579
+ try { await redis.del(`animastor:iu-progress-ids:${bookId}:${ds.chapter_id}:${ds.scene_id}:image`); } catch (_) {}
578
580
 
579
581
  try {
580
582
  let cursor = '0';
@@ -63,6 +63,25 @@ function error(msg) {
63
63
  console.error(`${logPrefix} ❌ ${msg}`);
64
64
  }
65
65
 
66
+ // SYNC: backend/src/helpers/queue-idempotency.cjs — resultDedupKey /
67
+ // readResultDedup (marker `'1'` = in flight, `done:<dispatch_id>` = settled).
68
+ // Read-only here: the result route is the only writer.
69
+ const RESULT_DEDUP_DONE_PREFIX = 'done:';
70
+ async function readResultDedup(redis, jobId, buildId) {
71
+ let raw = null;
72
+ try {
73
+ raw = await redis.get(`animastor:result-processed:${jobId}:${buildId}`);
74
+ } catch (_) {
75
+ return { state: 'unknown', dispatchId: null };
76
+ }
77
+ if (raw == null || raw === false) return { state: 'none', dispatchId: null };
78
+ const v = String(raw);
79
+ if (v.startsWith(RESULT_DEDUP_DONE_PREFIX)) {
80
+ return { state: 'settled', dispatchId: v.slice(RESULT_DEDUP_DONE_PREFIX.length) || null };
81
+ }
82
+ return { state: 'in_flight', dispatchId: null };
83
+ }
84
+
66
85
  // ======================================================
67
86
  // RECONCILIATION REPORT
68
87
  // ======================================================
@@ -1947,11 +1966,34 @@ async function recoverResultKeys(redis, deps, scope) {
1947
1966
  warn(`Skipping legacy result without build/dispatch identity: ${job_id}`);
1948
1967
  continue;
1949
1968
  }
1969
+
1970
+ // P1 (2026-11-01): the hub's fallback copy of a delivery the
1971
+ // result route already SETTLED is a duplicate — drop it
1972
+ // silently, never replay it (the replay re-ran the ready chain
1973
+ // and logged false no_active_dispatch warnings). Only a marker
1974
+ // carrying THIS dispatch counts: an in-flight / legacy '1'
1975
+ // marker or one from an older dispatch of the same job_id
1976
+ // (audio/video regeneration within the TTL) is no proof — such
1977
+ // a result goes through the fenced handler exactly as before.
1978
+ const dedup = await readResultDedup(redis, job_id, buildId);
1979
+ if (dedup.state === 'settled' && dedup.dispatchId === String(dispatchId)) {
1980
+ await redis.del(key);
1981
+ continue;
1982
+ }
1950
1983
  try {
1951
1984
  await taskHandler.handleTaskResult(job_id, result_base64, buildId, dispatchId);
1952
1985
  } catch (rejectErr) {
1953
1986
  if (rejectErr && TERMINAL_REJECTIONS.has(rejectErr.code)) {
1954
- warn(`Result ${job_id} terminally rejected (${rejectErr.code}) — dropping result key, no infinite retry`);
1987
+ // A replay of a delivery the route has seen (a marker
1988
+ // exists) is a duplicate: info. No marker (an orphan
1989
+ // that never reached the route) or an unreadable
1990
+ // marker: warn, as before.
1991
+ const msg = `Result ${job_id} terminally rejected (${rejectErr.code}) — dropping result key, no infinite retry`;
1992
+ if (dedup.state === 'in_flight' || dedup.state === 'settled') {
1993
+ log(`${msg} (duplicate of a delivered result)`);
1994
+ } else {
1995
+ warn(msg);
1996
+ }
1955
1997
  await redis.del(key);
1956
1998
  } else {
1957
1999
  throw rejectErr;