@animastor/orchestration 0.1.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.
Files changed (36) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +75 -0
  3. package/package.json +51 -0
  4. package/src/host/host-bindings.js +138 -0
  5. package/src/index.js +99 -0
  6. package/src/orchestration/index.js +40 -0
  7. package/src/orchestration/orchestrator.js +630 -0
  8. package/src/orchestration/runtime-result-consumer.js +54 -0
  9. package/src/orchestration/scene-callbacks.js +542 -0
  10. package/src/orchestration/scene-orchestrator.js +601 -0
  11. package/src/orchestration/scene-restoration.js +129 -0
  12. package/src/orchestration/scene-utils.js +36 -0
  13. package/src/runtime/active-scenes-index.js +164 -0
  14. package/src/runtime/audio-fsm-port.js +187 -0
  15. package/src/runtime/circuit-breaker.js +588 -0
  16. package/src/runtime/counter-reconciliation.js +308 -0
  17. package/src/runtime/dispatch-engine.js +1768 -0
  18. package/src/runtime/failure-taxonomy.js +125 -0
  19. package/src/runtime/hub-cancel-port.js +122 -0
  20. package/src/runtime/layer-config-port.js +136 -0
  21. package/src/runtime/lease-manager.js +607 -0
  22. package/src/runtime/persistence-port.js +162 -0
  23. package/src/runtime/placeholder-audio-port.js +117 -0
  24. package/src/runtime/progress-events-port.js +121 -0
  25. package/src/runtime/reconciliation-engine.js +2405 -0
  26. package/src/runtime/retry-budget-manager.js +229 -0
  27. package/src/runtime/runtime-metrics.js +427 -0
  28. package/src/runtime/runtime-result-emitter.js +123 -0
  29. package/src/runtime/runtime-scheduler.js +629 -0
  30. package/src/runtime/scene-data-port.js +114 -0
  31. package/src/runtime/scene-window.js +827 -0
  32. package/src/runtime/video-fsm-port.js +189 -0
  33. package/src/runtime/worker-health.js +270 -0
  34. package/test/counter-reconciliation.test.js +164 -0
  35. package/test/mocks/redis-mock.js +221 -0
  36. package/test/test-bindings.js +60 -0
@@ -0,0 +1,125 @@
1
+ // ======================================================
2
+ // FAILURE TAXONOMY - MINIMAL CLASSIFIER
3
+ // ======================================================
4
+ // S1 (2026-07-19): сокращён с 424 строк до ~80.
5
+ // Избыточные pattern-matching, severity levels, retry policies,
6
+ // retry metadata, error wrappers и metrics counters удалены —
7
+ // они не использовались в production-пути.
8
+ //
9
+ // Использование:
10
+ // - orchestrator.failStage(): classifyFailure() → failureType для journal
11
+ // - dispatch-engine.finalizeDispatch('failure'): failureType для retry budget
12
+ //
13
+ // Контракт:
14
+ // classifyFailure(err) → { type: 'transient'|'permanent'|'infrastructure'|'orchestration',
15
+ // retryable: boolean }
16
+ //
17
+ // Классификация: trans|orchestration → transient (retry), permanent/infrastructure → нет.
18
+ // Отброшенные патче HTTP-коды и stack-trace парсинг мёртвого retry-manager не нужны.
19
+
20
+ const logPrefix = '[FAILURE]';
21
+ function log(msg) { console.log(`${logPrefix} ${msg}`); }
22
+
23
+ const FailureType = {
24
+ TRANSIENT: 'transient',
25
+ PERMANENT: 'permanent',
26
+ INFRASTRUCTURE: 'infrastructure',
27
+ ORCHESTRATION: 'orchestration'
28
+ };
29
+
30
+ const FailureSeverity = {
31
+ LOW: 'low',
32
+ MEDIUM: 'medium',
33
+ HIGH: 'high',
34
+ CRITICAL: 'critical'
35
+ };
36
+
37
+ // Минимальный набор паттернов для отличия permanent (не retry) от остального.
38
+ const PERMANENT_PATTERNS = [
39
+ /invalid.*workflow/i,
40
+ /invalid.*asset/i,
41
+ /corrupted/i,
42
+ /invalid.*scene.*data/i,
43
+ /missing.*required.*field/i,
44
+ /unsupported.*format/i,
45
+ /(.*)not.*found$/i
46
+ ];
47
+
48
+ const INFRASTRUCTURE_PATTERNS = [
49
+ /redis.*unavailable/i,
50
+ /redis.*connection/i,
51
+ /filesystem.*error/i,
52
+ /ffmpeg.*missing/i,
53
+ /ffmpeg.*not.*found/i,
54
+ /disk.*full/i,
55
+ /out.*of.*memory/i
56
+ ];
57
+
58
+ function matchPattern(message, patterns) {
59
+ if (!message) return false;
60
+ for (const p of patterns) {
61
+ if (p.test(message)) return true;
62
+ }
63
+ return false;
64
+ }
65
+
66
+ /**
67
+ * Classify an error into a failure category.
68
+ * Returns: { type, retryable, severity, message, code }
69
+ *
70
+ * Priority: infrastructure → permanent → orchestration → transient (default).
71
+ * For 1-5 concurrent users этого достаточно — нет нужды в HTTP-кодах и severity logic.
72
+ */
73
+ function classifyFailure(err) {
74
+ if (!err) {
75
+ return {
76
+ type: FailureType.TRANSIENT,
77
+ retryable: true,
78
+ severity: FailureSeverity.LOW,
79
+ message: 'Unknown error'
80
+ };
81
+ }
82
+
83
+ const message = (err.message || err.toString() || '');
84
+ const code = err.code || null;
85
+ const lower = message.toLowerCase();
86
+
87
+ let type = FailureType.TRANSIENT;
88
+ let retryable = true;
89
+ let severity = FailureSeverity.MEDIUM;
90
+
91
+ if (matchPattern(lower, INFRASTRUCTURE_PATTERNS)) {
92
+ type = FailureType.INFRASTRUCTURE;
93
+ retryable = true;
94
+ severity = FailureSeverity.HIGH;
95
+ } else if (matchPattern(lower, PERMANENT_PATTERNS)) {
96
+ type = FailureType.PERMANENT;
97
+ retryable = false;
98
+ severity = FailureSeverity.HIGH;
99
+ } else if (/invalid.*transition|duplicate.*dispatch|stale.*lease|counter.*drift|token.*mismatch/i.test(lower)) {
100
+ type = FailureType.ORCHESTRATION;
101
+ retryable = false;
102
+ severity = FailureSeverity.MEDIUM;
103
+ } else if (code === 'EPERM' || code === 'EACCES' || code === 'ENOENT') {
104
+ type = FailureType.PERMANENT;
105
+ retryable = false;
106
+ severity = FailureSeverity.HIGH;
107
+ } else {
108
+ type = FailureType.TRANSIENT;
109
+ retryable = true;
110
+ severity = FailureSeverity.LOW;
111
+ }
112
+
113
+ return { type, retryable, severity, message, code, classifiedAt: Date.now() };
114
+ }
115
+
116
+ function getFailureTypeKeys() {
117
+ return ['transient', 'permanent', 'infrastructure', 'orchestration'];
118
+ }
119
+
120
+ module.exports = {
121
+ FailureType,
122
+ FailureSeverity,
123
+ classifyFailure,
124
+ getFailureTypeKeys
125
+ };
@@ -0,0 +1,122 @@
1
+ // ======================================================
2
+ // O-9 PORT: HubCancelPort — runtime/orchestration
3
+ // hub-queue cleanup contract
4
+ // ======================================================
5
+ // The runtime and orchestration tiers must not own GPU Hub HTTP cleanup.
6
+ // The hub URL/API-key resolution, the DELETE /queue/clear endpoint shape,
7
+ // per-dispatch retry/warn behavior and the { requested, cleared, failed }
8
+ // result accounting stay HOST-side (backend/src/runtime/dispatch-engine.js
9
+ // clearHubDispatches — the same auth and error-handling contract every
10
+ // caller uses); the tiers purge cancelled dispatches from the hub queue
11
+ // ONLY through this port, wired by the composition root (backend.cjs) at
12
+ // startup.
13
+ //
14
+ // runtime/** + orchestration/** (tier consumers)
15
+ // ↓ consume hub-cancel ops through
16
+ // runtime/hub-cancel-port.js ← THIS PORT (tier-owned contract)
17
+ // ↑ wired by the host
18
+ // storage/hub-cancel-adapter (host adapter → dispatch-engine)
19
+ //
20
+ // This is the O-2/O-3/O-4/O-5/O-7/O-8 port convention (runtime/persistence-
21
+ // port.js, runtime/scene-data-port.js, runtime/placeholder-audio-port.js,
22
+ // runtime/progress-events-port.js, runtime/audio-fsm-port.js,
23
+ // runtime/video-fsm-port.js): a zero-require contract module, a fail-fast
24
+ // call-time resolver, one set/wired/reset surface. No hub URL, no API key,
25
+ // no fetch, no HTTP method/endpoint knowledge and no "future ops" cross
26
+ // the boundary — the contract below is EXACTLY the set of operations the
27
+ // two tiers use today (measured at O-9 reconnaissance, §32.26):
28
+ //
29
+ // clearHubDispatches — best-effort removal of the queue/running copies
30
+ // a cancelled dispatch still owns on the GPU Hub
31
+ // (orchestrator resetScenes cancel leg,
32
+ // reconciliation RELEASE_STALE_LEASE recovery leg).
33
+ // Returns { requested, cleared, failed }; hub
34
+ // unavailability is non-fatal per-id (warn + count),
35
+ // never throws for a single failed id.
36
+ //
37
+ // Optional-load/error semantics are the CALLER's concern, preserved
38
+ // exactly through the port: the reconciliation RELEASE_STALE_LEASE leg
39
+ // keeps its pre-O-9 `try { … } catch (hubErr) { warn }` best-effort
40
+ // wrapper around the port call (hub cleanup failure must not block state
41
+ // recovery); the orchestrator resetScenes leg keeps its plain await.
42
+ //
43
+ // Deliberately NOT in the port: lease cancellation itself (cancelActive-
44
+ // Dispatch / clearLeasesForScenes / clearLeasesForBookByStage / clearAll-
45
+ // LeasesForBook stay direct dispatch-engine calls — they are Redis-key
46
+ // domain operations, not hub HTTP), job dispatch (the S-3/S-6 Dispatch-
47
+ // Transport seam owns it), any combined "cancel everything" surface and
48
+ // any CRUD "for later".
49
+ //
50
+ // Docs: docs/architecture/generation-module-extraction-reconnaissance.md §32.26 (O-9)
51
+ // ======================================================
52
+
53
+ const OPS = [
54
+ 'clearHubDispatches',
55
+ ];
56
+
57
+ let impl = null;
58
+
59
+ /**
60
+ * Wire the host hub-cancel adapter. Called once by the composition root
61
+ * (backend.cjs) BEFORE any runtime/orchestration hub-cancel consumer runs.
62
+ * The adapter must expose every op in OPS as a function; lazy resolution
63
+ * of the host implementation happens at CALL time (the O-2..O-8 adapter
64
+ * discipline), so wiring never forces a dispatch-engine load.
65
+ * @param {object} adapter — map with every op in OPS as a function
66
+ */
67
+ function setHubCancelPort(adapter) {
68
+ if (!adapter) {
69
+ throw new Error('hub-cancel-port: adapter is required');
70
+ }
71
+ for (const op of OPS) {
72
+ if (typeof adapter[op] !== 'function') {
73
+ throw new Error(`hub-cancel-port: adapter must implement ${op}()`);
74
+ }
75
+ }
76
+ impl = adapter;
77
+ }
78
+
79
+ /** Resolve the port at CALL time — fail-fast when unwired. */
80
+ function hubCancel() {
81
+ if (!impl) {
82
+ throw new Error(
83
+ 'hub-cancel-port: not wired — the composition root must call ' +
84
+ 'setHubCancelPort(adapter) (storage/hub-cancel-adapter) ' +
85
+ 'before runtime/orchestration hub-cancel consumers run (O-9)'
86
+ );
87
+ }
88
+ return impl;
89
+ }
90
+
91
+ /**
92
+ * Shorthand op resolver: `hubCancelOp('clearHubDispatches')(...)`.
93
+ * Fails fast when the port or the single op is missing — a missing hub
94
+ * cleanup silently degrading into a no-op would accumulate duplicate job
95
+ * copies a worker would later drain (audit c8b79f6).
96
+ * @param {string} op — op name from OPS
97
+ */
98
+ function hubCancelOp(op) {
99
+ const fn = impl?.[op];
100
+ if (typeof fn !== 'function') {
101
+ throw new Error(`hub-cancel-port: op '${op}' is not wired (O-9)`);
102
+ }
103
+ return fn;
104
+ }
105
+
106
+ function isHubCancelPortWired() {
107
+ return impl !== null;
108
+ }
109
+
110
+ /** Test hygiene: drop the wiring (mocha hooks). */
111
+ function _resetHubCancelPort() {
112
+ impl = null;
113
+ }
114
+
115
+ module.exports = {
116
+ OPS,
117
+ setHubCancelPort,
118
+ hubCancel,
119
+ hubCancelOp,
120
+ isHubCancelPortWired,
121
+ _resetHubCancelPort,
122
+ };
@@ -0,0 +1,136 @@
1
+ // ======================================================
2
+ // O-10 PORT: LayerConfigPort — runtime/orchestration
3
+ // per-book layer-config contract
4
+ // ======================================================
5
+ // The runtime and orchestration tiers must not know the layer-config host
6
+ // service. The Redis key grammar (`animastor:layer-config:*`), the
7
+ // normalize/clamp pipeline, the durable book.json copy (Кирпич №2) and
8
+ // the fs scanning the service performs stay HOST-side
9
+ // (backend/src/services/layer-config.js); the tiers consume the
10
+ // per-book layer-config operations they actually use ONLY through this
11
+ // port, wired by the composition root (backend.cjs) at startup.
12
+ //
13
+ // runtime/** + orchestration/** (tier consumers)
14
+ // ↓ consume layer-config ops through
15
+ // runtime/layer-config-port.js ← THIS PORT (tier-owned contract)
16
+ // ↑ wired by the host
17
+ // storage/layer-config-adapter (host service adapter)
18
+ //
19
+ // This is the O-2..O-9 port convention (runtime/persistence-port.js,
20
+ // runtime/scene-data-port.js, runtime/placeholder-audio-port.js,
21
+ // runtime/progress-events-port.js, runtime/audio-fsm-port.js,
22
+ // runtime/video-fsm-port.js, runtime/hub-cancel-port.js): a zero-require
23
+ // contract module, a fail-fast call-time resolver, one set/wired/reset
24
+ // surface. No Redis key, no fs path, no BOOKS_DIR, no normalize/clamp
25
+ // rules and no "future ops" cross the boundary — the contract below is
26
+ // EXACTLY the set of operations the two tiers use today (measured at
27
+ // O-10 reconnaissance, §32.27):
28
+ //
29
+ // get — read the normalized per-book layer config
30
+ // (scene-orchestrator executeVideoDispatch
31
+ // per-type timeout leg; reconciliation-engine
32
+ // rebuildWorkList enabled-layers read)
33
+ // restoreFromBooks — heal missing Redis keys from the durable book.json
34
+ // copy (reconciliation-engine PHASE C6)
35
+ //
36
+ // The tier-local RAW Redis reads of `animastor:layer-config:*`
37
+ // (reconciliation-engine video-stall threshold, scene-window
38
+ // isWindowComplete/isSceneWindowDone, runtime-scheduler getLayerConfig)
39
+ // are NOT part of this port: they read the key in place with their own
40
+ // bespoke fallback semantics (partial-parse, per-field defaults) that a
41
+ // normalize() call would change — moving them is its own measured
42
+ // decision, not a rider on this seam. The raw Redis channel itself is
43
+ // not a host-service edge (redis is an injected parameter).
44
+ //
45
+ // Deliberately NOT in the port: set/persistToBook (routes/import
46
+ // workflow), getChunkSize (routes + services/agent consumers),
47
+ // SCOPES/ANALYSIS_MODES/DEFAULTS vocabulary (routes/services constants),
48
+ // key/KEY_PREFIX grammar, normalize (host-internal), and any CRUD "for
49
+ // later". The host adapter owns every Redis read, the durable book.json
50
+ // scan and every clamp rule.
51
+ //
52
+ // Optional-load/error semantics are the CALLER's concern, preserved
53
+ // exactly through the port: the reconciliation PHASE C6 leg keeps its
54
+ // pre-O-10 `try { … } catch (err) { warn; summary.errors }` wrapper with
55
+ // the missing-module degrade (a require failure of the host service
56
+ // previously left `layerConfig = null` and skipped the phase silently —
57
+ // the adapter's lazy call-time resolver throws at the same point the
58
+ // pre-O-10 require did, and the caller's catch handles it identically);
59
+ // the scene-orchestrator timeout leg keeps its `try { … } catch (_) {}`
60
+ // tolerance; the rebuildWorkList read stays a plain mandatory await.
61
+ //
62
+ // Docs: docs/architecture/generation-module-extraction-reconnaissance.md §32.27 (O-10)
63
+ // ======================================================
64
+
65
+ const OPS = [
66
+ 'get',
67
+ 'restoreFromBooks',
68
+ ];
69
+
70
+ let impl = null;
71
+
72
+ /**
73
+ * Wire the host layer-config adapter. Called once by the composition root
74
+ * (backend.cjs) BEFORE any runtime/orchestration layer-config consumer
75
+ * runs. The adapter must expose every op in OPS as a function; lazy
76
+ * resolution of the host implementation happens at CALL time (the
77
+ * O-2..O-9 adapter discipline), so wiring never forces a
78
+ * services/layer-config load.
79
+ * @param {object} adapter — flat map with every op in OPS as a function.
80
+ */
81
+ function setLayerConfigPort(adapter) {
82
+ if (!adapter) {
83
+ throw new Error('layer-config-port: adapter is required');
84
+ }
85
+ for (const op of OPS) {
86
+ if (typeof adapter[op] !== 'function') {
87
+ throw new Error(`layer-config-port: adapter must implement ${op}()`);
88
+ }
89
+ }
90
+ impl = adapter;
91
+ }
92
+
93
+ /** Resolve the port at CALL time — fail-fast when unwired. */
94
+ function layerConfig() {
95
+ if (!impl) {
96
+ throw new Error(
97
+ 'layer-config-port: not wired — the composition root must call ' +
98
+ 'setLayerConfigPort(adapter) (storage/layer-config-adapter) ' +
99
+ 'before runtime/orchestration layer-config consumers run (O-10)'
100
+ );
101
+ }
102
+ return impl;
103
+ }
104
+
105
+ /**
106
+ * Shorthand op resolver: `layerConfigOp('get')(...)`.
107
+ * Fails fast when the port or the single op is missing — callers that
108
+ * tolerate failure keep their own try/catch around the call (the
109
+ * caller-owned optional-load discipline of O-7/O-9).
110
+ * @param {string} op — op name from OPS
111
+ */
112
+ function layerConfigOp(op) {
113
+ const fn = impl?.[op];
114
+ if (typeof fn !== 'function') {
115
+ throw new Error(`layer-config-port: op '${op}' is not wired (O-10)`);
116
+ }
117
+ return fn;
118
+ }
119
+
120
+ function isLayerConfigPortWired() {
121
+ return impl !== null;
122
+ }
123
+
124
+ /** Test hygiene: drop the wiring (mocha hooks). */
125
+ function _resetLayerConfigPort() {
126
+ impl = null;
127
+ }
128
+
129
+ module.exports = {
130
+ OPS,
131
+ setLayerConfigPort,
132
+ layerConfig,
133
+ layerConfigOp,
134
+ isLayerConfigPortWired,
135
+ _resetLayerConfigPort,
136
+ };