arcane-os 0.2.2 → 0.3.0

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 (117) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +8 -8
  3. package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +29 -22
  4. package/browser-runtime/ai/ARCANE_AI_BROWSER_SPEECH_COMPONENTS.json +203 -0
  5. package/browser-runtime/ai/browser-kokoro-worker.mjs +11 -2
  6. package/browser-runtime/ai/browser-speech-artifacts.mjs +3230 -397
  7. package/browser-runtime/ai/browser-speech-providers.mjs +1141 -157
  8. package/browser-runtime/ai/browser-speech.mjs +2 -0
  9. package/browser-runtime/ai/browser-whisper-worker.mjs +11 -2
  10. package/browser-runtime/ai/model-controller.mjs +285 -95
  11. package/browser-runtime/ai/speech-worker-client.mjs +247 -32
  12. package/browser-runtime/ai/speech-worker-runtime.mjs +2310 -167
  13. package/browser-runtime/event-manager.mjs +1097 -1
  14. package/docs/architecture.md +2 -2
  15. package/docs/event-manager.md +155 -27
  16. package/docs/reference/README.md +27 -27
  17. package/docs/reference/ai/browser-speech-package-authority.json +835 -0
  18. package/docs/reference/ai/browser-speech.md +1162 -246
  19. package/docs/reference/ai/browser-wasm.md +18 -7
  20. package/docs/reference/availability-and-normalization.md +6 -3
  21. package/docs/reference/behavioral-testing.md +29 -6
  22. package/docs/reference/cli.md +117 -9
  23. package/docs/reference/core/arcane-ai-contracts.md +1 -1
  24. package/docs/reference/event-manager.md +577 -32
  25. package/docs/reference/inventory/package-api.json +478 -2
  26. package/docs/reference/inventory/runtime-components.json +108 -44
  27. package/docs/reference/inventory/runtime-modules.json +131 -53
  28. package/docs/reference/mail.md +316 -0
  29. package/docs/reference/protocols.md +157 -43
  30. package/docs/reference/runtime-components.md +258 -83
  31. package/docs/reference/runtime-modules.md +613 -77
  32. package/docs/reference/sdk-api.md +1014 -25
  33. package/package.json +5 -4
  34. package/runtime/ARCANE_RUNTIME_RELEASE.json +145 -140
  35. package/runtime/arcane/components/app-bar.html +34 -13
  36. package/runtime/arcane/components/assistant-panel.html +110 -57
  37. package/runtime/arcane/components/calculator.html +7 -4
  38. package/runtime/arcane/components/chart.html +58 -17
  39. package/runtime/arcane/components/chat.html +606 -136
  40. package/runtime/arcane/components/conversation-view.html +13 -6
  41. package/runtime/arcane/components/dashboard-config.html +96 -59
  42. package/runtime/arcane/components/data-maintenance.html +69 -14
  43. package/runtime/arcane/components/data-view.html +53 -7
  44. package/runtime/arcane/components/directory-picker.html +118 -32
  45. package/runtime/arcane/components/document-inspector.html +47 -10
  46. package/runtime/arcane/components/file-drop.html +81 -35
  47. package/runtime/arcane/components/file-inspector.html +72 -22
  48. package/runtime/arcane/components/file-manager.html +374 -79
  49. package/runtime/arcane/components/integration-settings.html +12 -5
  50. package/runtime/arcane/components/local-ai-status.html +48 -19
  51. package/runtime/arcane/components/markdown-document.html +161 -68
  52. package/runtime/arcane/components/markdown-editor.html +110 -33
  53. package/runtime/arcane/components/media-embed.html +8 -5
  54. package/runtime/arcane/components/modal.html +15 -5
  55. package/runtime/arcane/components/output-panel.html +28 -23
  56. package/runtime/arcane/components/preferences-form.html +22 -4
  57. package/runtime/arcane/components/record-timeline.html +18 -2
  58. package/runtime/arcane/components/relationship-board.html +23 -3
  59. package/runtime/arcane/components/screen-capture.html +10 -4
  60. package/runtime/arcane/components/source-code-viewer.html +76 -9
  61. package/runtime/arcane/components/source-explanation.html +23 -3
  62. package/runtime/arcane/components/speech.html +462 -384
  63. package/runtime/arcane/components/summary-strip.html +22 -11
  64. package/runtime/arcane/components/table.html +39 -21
  65. package/runtime/arcane/components/task-progress.html +79 -21
  66. package/runtime/arcane/components/terminal-workspace.html +7 -4
  67. package/runtime/arcane/components/theme-editor.html +7 -3
  68. package/runtime/arcane/components/unified-inbox.html +9 -4
  69. package/runtime/arcane/components/voice-transcription.html +639 -98
  70. package/runtime/arcane/components/weather-widget.html +5 -3
  71. package/runtime/arcane/components/web-navigator.html +48 -8
  72. package/runtime/arcane/entities/Chat.js +1 -1
  73. package/runtime/arcane/entities/User.js +110 -23
  74. package/runtime/arcane/modules/AI.js +2109 -130
  75. package/runtime/arcane/modules/AIProviderRuntime.js +720 -13
  76. package/runtime/arcane/modules/AIRuntimeState.js +109 -52
  77. package/runtime/arcane/modules/ApiModelDatabase.js +390 -17
  78. package/runtime/arcane/modules/BrowserTestSuite.js +205 -28
  79. package/runtime/arcane/modules/CalculatorEngine.js +63 -3
  80. package/runtime/arcane/modules/CommunicationAppController.js +588 -28
  81. package/runtime/arcane/modules/CommunicationHub.js +590 -10
  82. package/runtime/arcane/modules/ComponentContracts.js +470 -0
  83. package/runtime/arcane/modules/ConversationTimebox.js +152 -33
  84. package/runtime/arcane/modules/DBLS.js +40 -7
  85. package/runtime/arcane/modules/DBOPFS.js +35 -11
  86. package/runtime/arcane/modules/DataMaintenance.js +12 -2
  87. package/runtime/arcane/modules/Errors.js +65 -7
  88. package/runtime/arcane/modules/HTMLImport.js +198 -14
  89. package/runtime/arcane/modules/LocalAIReadinessController.js +208 -29
  90. package/runtime/arcane/modules/Mail.js +738 -115
  91. package/runtime/arcane/modules/MailOutbox.mjs +1395 -0
  92. package/runtime/arcane/modules/MailTransport.mjs +197 -39
  93. package/runtime/arcane/modules/Ollama.js +36 -1
  94. package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +583 -7
  95. package/runtime/arcane/modules/PreferenceStore.js +367 -33
  96. package/runtime/arcane/modules/RecordReviewStore.js +322 -23
  97. package/runtime/arcane/modules/ScreenCapture.js +1397 -15
  98. package/runtime/arcane/modules/SpeechPlayback.js +438 -41
  99. package/runtime/arcane/modules/TerminalClient.js +277 -12
  100. package/runtime/arcane/modules/ThemeBootstrap.js +80 -6
  101. package/runtime/arcane/modules/ThemeManager.js +39 -7
  102. package/runtime/arcane/modules/TimeGuard.js +131 -20
  103. package/runtime/arcane/modules/WaitForComponent.js +386 -33
  104. package/schemas/arcane-lock.schema.json +2 -2
  105. package/src/cli/main.mjs +435 -9
  106. package/src/event-manager.mjs +1097 -1
  107. package/src/import-map.mjs +21 -3
  108. package/src/index.mjs +13 -0
  109. package/src/installed-sdk-runtime.mjs +112 -0
  110. package/src/mail-api.mjs +22 -0
  111. package/src/mail-credentials.mjs +667 -0
  112. package/src/mail-server.mjs +1769 -0
  113. package/src/mail.mjs +261 -0
  114. package/src/sdk-browser-runtime.mjs +85 -41
  115. package/src/testing-loader.mjs +7 -0
  116. package/src/toolchain.mjs +3 -0
  117. package/src/workspace.mjs +1 -1
@@ -1,11 +1,457 @@
1
1
  export const SPEECH_WORKER_PROTOCOL = "arcane-ai-speech-worker/1";
2
2
 
3
+ const SPEECH_WORKER_ERROR_PROTOCOL = "arcane-ai-speech-worker-error/1";
4
+
5
+ const ARTIFACT_GRAPH_MODULE_GRAPH =
6
+ "browser-speech-authenticated-artifact-graph";
7
+ const GRAPH_GUARD_NAME = "__arcaneBrowserSpeechArtifactGraphGuardsV1";
8
+ const NESTED_WORKER_PROTOCOL =
9
+ "arcane-ai-browser-speech-artifact-module-worker/1";
10
+ const STRICT_GRAPH_ADMISSIONS = new Set([
11
+ "artifact-graph-network-dbopfs-verified",
12
+ "artifact-graph-dbopfs-cache-verified",
13
+ "artifact-graph-offline-dbopfs-cache-verified",
14
+ "artifact-graph-network-dbopfs-partially-checked",
15
+ "artifact-graph-dbopfs-cache-partially-checked",
16
+ "artifact-graph-offline-dbopfs-cache-partially-checked",
17
+ "artifact-graph-network-dbopfs-unchecked",
18
+ "artifact-graph-dbopfs-cache-unchecked",
19
+ "artifact-graph-offline-dbopfs-cache-unchecked",
20
+ ]);
3
21
  const ADAPTERS = Object.freeze({
4
22
  stt: "transformers-whisper",
5
23
  tts: "kokoro-js",
6
24
  });
25
+ const ONNX_NAMESPACES = Object.freeze({
26
+ stt: "transformers-env-backends-onnx-wasm",
27
+ tts: "kokoro-env-wasm-paths",
28
+ });
29
+ const ARTIFACT_GRAPH_TRANSFORM_KINDS = new Set([
30
+ "function-return-this-to-global-this",
31
+ "typed-array-constructor",
32
+ ]);
33
+ const PUBLIC_WORKER_OPERATIONS = Object.freeze([
34
+ "load",
35
+ "use",
36
+ "status",
37
+ "unload",
38
+ "dispose",
39
+ ]);
40
+ const TRANSPORT_WORKER_OPERATION_SET = new Set([
41
+ ...PUBLIC_WORKER_OPERATIONS,
42
+ "cancel",
43
+ ]);
44
+ const BOTH_WORKER_ROLES = Object.freeze(["stt", "tts"]);
45
+ const LOAD_OPERATION = Object.freeze(["load"]);
46
+ const USE_OPERATION = Object.freeze(["use"]);
47
+ const STATUS_OPERATION = Object.freeze(["status"]);
48
+ const UNLOAD_OPERATION = Object.freeze(["unload"]);
49
+ const DISPOSE_OPERATION = Object.freeze(["dispose"]);
50
+ const UNLOAD_OR_DISPOSE_OPERATIONS = Object.freeze(["unload", "dispose"]);
51
+ const LOAD_OR_USE_OPERATIONS = Object.freeze(["load", "use"]);
52
+ const SDK_WORKER_ERRORS = new WeakSet();
53
+ const WORKER_ERROR_MESSAGES = Object.freeze({
54
+ ARCANE_AI_REQUEST_ABORTED: "The speech worker operation was cancelled.",
55
+ ARCANE_AI_NOT_READY: "The speech worker is not loaded.",
56
+ ARCANE_AI_INVALID_REQUEST: "The speech worker request was rejected.",
57
+ ARCANE_AI_INVALID_PROVIDER_RESULT: "The speech engine result was rejected.",
58
+ ARCANE_AI_UNDECLARED_ARTIFACT: "The speech engine requested an undeclared artifact.",
59
+ ARCANE_AI_PROVIDER_UNAVAILABLE: "The selected speech engine is unavailable.",
60
+ ARCANE_AI_PROVIDER_DISPOSED: "The speech worker is disposed.",
61
+ ARCANE_AI_PROVIDER_REQUEST_FAILED: "The speech engine operation was rejected.",
62
+ ARCANE_AI_WORKER_CRASHED: "The speech Worker stopped unexpectedly.",
63
+ ARCANE_AI_WORKER_MESSAGE_ERROR: "The speech Worker message was rejected.",
64
+ ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID:
65
+ "The authenticated artifact graph Worker configuration was rejected.",
66
+ ARCANE_AI_ARTIFACT_GRAPH_FETCH_EDGE_UNDECLARED:
67
+ "The runtime used an undeclared artifact graph fetch edge.",
68
+ ARCANE_AI_ARTIFACT_GRAPH_IMPORT_EDGE_UNDECLARED:
69
+ "The runtime used an undeclared artifact graph import edge.",
70
+ ARCANE_AI_ARTIFACT_GRAPH_CACHE_EDGE_UNDECLARED:
71
+ "The runtime used an undeclared artifact graph cache edge.",
72
+ ARCANE_AI_ARTIFACT_GRAPH_WORKER_EDGE_UNDECLARED:
73
+ "The runtime used an undeclared artifact graph module Worker edge.",
74
+ ARCANE_AI_ARTIFACT_GRAPH_ISOLATION_UNAVAILABLE:
75
+ "The Worker cannot install the authenticated artifact graph isolation boundary.",
76
+ });
77
+ const WORKER_ERROR_REASON_ADMISSIONS = new Map();
78
+
79
+ function registerWorkerErrorReasons(code, roles, operations, reasons) {
80
+ const admission = Object.freeze({
81
+ code,
82
+ roles: Object.freeze([...roles]),
83
+ operations: Object.freeze([...operations]),
84
+ });
85
+ for (const reason of reasons) WORKER_ERROR_REASON_ADMISSIONS.set(reason, admission);
86
+ }
87
+
88
+ registerWorkerErrorReasons("ARCANE_AI_REQUEST_ABORTED", ["stt"], LOAD_OPERATION, [
89
+ "stt-load-cancelled",
90
+ ]);
91
+ registerWorkerErrorReasons("ARCANE_AI_REQUEST_ABORTED", ["stt"], STATUS_OPERATION, [
92
+ "stt-status-cancelled",
93
+ ]);
94
+ registerWorkerErrorReasons("ARCANE_AI_REQUEST_ABORTED", ["stt"], UNLOAD_OPERATION, [
95
+ "stt-unload-cancelled",
96
+ ]);
97
+ registerWorkerErrorReasons("ARCANE_AI_REQUEST_ABORTED", ["stt"], DISPOSE_OPERATION, [
98
+ "stt-dispose-cancelled",
99
+ ]);
100
+ registerWorkerErrorReasons("ARCANE_AI_REQUEST_ABORTED", ["stt"], USE_OPERATION, [
101
+ "stt-transcription-cancelled",
102
+ ]);
103
+ registerWorkerErrorReasons("ARCANE_AI_REQUEST_ABORTED", ["tts"], LOAD_OPERATION, [
104
+ "tts-load-cancelled",
105
+ ]);
106
+ registerWorkerErrorReasons("ARCANE_AI_REQUEST_ABORTED", ["tts"], STATUS_OPERATION, [
107
+ "tts-status-cancelled",
108
+ ]);
109
+ registerWorkerErrorReasons("ARCANE_AI_REQUEST_ABORTED", ["tts"], UNLOAD_OPERATION, [
110
+ "tts-unload-cancelled",
111
+ ]);
112
+ registerWorkerErrorReasons("ARCANE_AI_REQUEST_ABORTED", ["tts"], DISPOSE_OPERATION, [
113
+ "tts-dispose-cancelled",
114
+ ]);
115
+ registerWorkerErrorReasons("ARCANE_AI_REQUEST_ABORTED", ["tts"], USE_OPERATION, [
116
+ "tts-synthesis-cancelled",
117
+ ]);
118
+ registerWorkerErrorReasons("ARCANE_AI_PROVIDER_REQUEST_FAILED", ["stt"], LOAD_OPERATION, [
119
+ "stt-worker-runtime-configuration-rejected",
120
+ "stt-worker-runtime-import-rejected",
121
+ "stt-worker-model-load-rejected",
122
+ ]);
123
+ registerWorkerErrorReasons("ARCANE_AI_PROVIDER_REQUEST_FAILED", ["tts"], LOAD_OPERATION, [
124
+ "tts-worker-runtime-configuration-rejected",
125
+ "tts-worker-runtime-import-rejected",
126
+ "tts-worker-model-load-rejected",
127
+ ]);
128
+ registerWorkerErrorReasons("ARCANE_AI_PROVIDER_REQUEST_FAILED", ["stt"], USE_OPERATION, [
129
+ "stt-transcription-engine-operation-rejected",
130
+ ]);
131
+ registerWorkerErrorReasons("ARCANE_AI_PROVIDER_REQUEST_FAILED", ["tts"], USE_OPERATION, [
132
+ "tts-synthesis-engine-operation-rejected",
133
+ ]);
134
+ registerWorkerErrorReasons(
135
+ "ARCANE_AI_PROVIDER_REQUEST_FAILED",
136
+ ["stt"],
137
+ UNLOAD_OR_DISPOSE_OPERATIONS,
138
+ ["stt-worker-engine-dispose-rejected"],
139
+ );
140
+ registerWorkerErrorReasons(
141
+ "ARCANE_AI_PROVIDER_REQUEST_FAILED",
142
+ ["tts"],
143
+ UNLOAD_OR_DISPOSE_OPERATIONS,
144
+ ["tts-worker-engine-dispose-rejected"],
145
+ );
146
+ registerWorkerErrorReasons("ARCANE_AI_PROVIDER_REQUEST_FAILED", ["stt"], DISPOSE_OPERATION, [
147
+ "stt-worker-dispose-rejected",
148
+ ]);
149
+ registerWorkerErrorReasons("ARCANE_AI_PROVIDER_REQUEST_FAILED", ["tts"], DISPOSE_OPERATION, [
150
+ "tts-worker-dispose-rejected",
151
+ ]);
152
+ registerWorkerErrorReasons("ARCANE_AI_PROVIDER_REQUEST_FAILED", ["stt"], STATUS_OPERATION, [
153
+ "stt-worker-status-rejected",
154
+ ]);
155
+ registerWorkerErrorReasons("ARCANE_AI_PROVIDER_REQUEST_FAILED", ["tts"], STATUS_OPERATION, [
156
+ "tts-worker-status-rejected",
157
+ ]);
158
+ registerWorkerErrorReasons("ARCANE_AI_INVALID_REQUEST", BOTH_WORKER_ROLES, LOAD_OPERATION, [
159
+ "artifact-graph-entrypoint-not-materialized",
160
+ "artifact-graph-materialized-file-not-object",
161
+ "artifact-graph-materialized-media-type-empty",
162
+ "artifact-graph-materialized-module-url-empty",
163
+ "artifact-graph-materialized-path-ambiguous",
164
+ "artifact-graph-materialized-path-empty",
165
+ "artifact-graph-materialized-source-url-empty",
166
+ "artifact-graph-runtime-request-routes-not-array",
167
+ "speech-model-sample-rate-not-positive-safe-integer",
168
+ "speech-runtime-entrypoint-not-materialized",
169
+ "speech-worker-configuration-missing",
170
+ "speech-worker-materialized-files-missing",
171
+ "speech-worker-model-dtype-empty",
172
+ "speech-worker-model-id-empty",
173
+ "speech-worker-model-repository-empty",
174
+ "speech-worker-model-revision-empty",
175
+ "speech-worker-runtime-entry-empty",
176
+ "speech-worker-runtime-selection-mismatch",
177
+ ]);
178
+ registerWorkerErrorReasons("ARCANE_AI_INVALID_REQUEST", ["stt"], USE_OPERATION, [
179
+ "stt-transcription-audio-not-float32array",
180
+ "stt-transcription-sample-rate-mismatch",
181
+ ]);
182
+ registerWorkerErrorReasons("ARCANE_AI_INVALID_REQUEST", ["tts"], LOAD_OPERATION, [
183
+ "tts-default-voice-empty",
184
+ ]);
185
+ registerWorkerErrorReasons("ARCANE_AI_INVALID_REQUEST", ["tts"], USE_OPERATION, [
186
+ "tts-synthesis-speed-out-of-range",
187
+ "tts-synthesis-text-empty",
188
+ "tts-synthesis-voice-empty",
189
+ "tts-synthesis-voice-not-declared",
190
+ ]);
191
+ registerWorkerErrorReasons("ARCANE_AI_INVALID_PROVIDER_RESULT", ["stt"], USE_OPERATION, [
192
+ "stt-transcription-result-text-not-string",
193
+ ]);
194
+ registerWorkerErrorReasons("ARCANE_AI_INVALID_PROVIDER_RESULT", ["tts"], USE_OPERATION, [
195
+ "tts-synthesis-pcm-result-not-float32array",
196
+ "tts-synthesis-pcm-sample-non-finite",
197
+ "tts-synthesis-result-sample-rate-mismatch",
198
+ ]);
199
+ registerWorkerErrorReasons(
200
+ "ARCANE_AI_UNDECLARED_ARTIFACT",
201
+ BOTH_WORKER_ROLES,
202
+ LOAD_OR_USE_OPERATIONS,
203
+ [
204
+ "artifact-graph-cache-write-rejected",
205
+ "artifact-graph-runtime-request-url-malformed",
206
+ "speech-worker-artifact-request-method-rejected",
207
+ "speech-worker-artifact-request-undeclared",
208
+ "speech-worker-cache-match-rejected",
209
+ "speech-worker-cache-open-rejected",
210
+ ],
211
+ );
212
+ registerWorkerErrorReasons("ARCANE_AI_PROVIDER_DISPOSED", ["stt"], LOAD_OPERATION, [
213
+ "stt-load-rejected-after-dispose",
214
+ ]);
215
+ registerWorkerErrorReasons("ARCANE_AI_PROVIDER_DISPOSED", ["tts"], LOAD_OPERATION, [
216
+ "tts-load-rejected-after-dispose",
217
+ ]);
218
+ registerWorkerErrorReasons("ARCANE_AI_NOT_READY", ["stt"], USE_OPERATION, [
219
+ "stt-transcription-rejected-before-load",
220
+ ]);
221
+ registerWorkerErrorReasons("ARCANE_AI_NOT_READY", ["tts"], USE_OPERATION, [
222
+ "tts-synthesis-rejected-before-load",
223
+ ]);
224
+ registerWorkerErrorReasons("ARCANE_AI_PROVIDER_UNAVAILABLE", BOTH_WORKER_ROLES, LOAD_OPERATION, [
225
+ "artifact-graph-fetch-constructor-unavailable",
226
+ "artifact-graph-onnx-wasm-pair-not-materialized",
227
+ "speech-worker-fetch-unavailable",
228
+ ]);
229
+ registerWorkerErrorReasons(
230
+ "ARCANE_AI_PROVIDER_UNAVAILABLE",
231
+ BOTH_WORKER_ROLES,
232
+ LOAD_OR_USE_OPERATIONS,
233
+ [
234
+ "artifact-graph-module-worker-constructor-unavailable",
235
+ "artifact-graph-negative-response-constructor-unavailable",
236
+ ],
237
+ );
238
+ registerWorkerErrorReasons("ARCANE_AI_PROVIDER_UNAVAILABLE", ["tts"], LOAD_OPERATION, [
239
+ "kokoro-env-wasm-paths-assignment-rejected",
240
+ "kokoro-env-wasm-paths-unavailable",
241
+ "kokoro-tts-constructor-export-missing",
242
+ ]);
243
+ registerWorkerErrorReasons("ARCANE_AI_PROVIDER_UNAVAILABLE", ["stt"], LOAD_OPERATION, [
244
+ "transformers-env-allow-local-models-assignment-rejected",
245
+ "transformers-env-allow-local-models-unavailable",
246
+ "transformers-env-allow-remote-models-assignment-rejected",
247
+ "transformers-env-allow-remote-models-unavailable",
248
+ "transformers-env-backends-onnx-wasm-unavailable",
249
+ "transformers-env-browser-cache-assignment-rejected",
250
+ "transformers-env-browser-cache-unavailable",
251
+ "transformers-env-custom-cache-assignment-rejected",
252
+ "transformers-env-custom-cache-toggle-unavailable",
253
+ "transformers-env-custom-cache-toggle-assignment-rejected",
254
+ "transformers-env-custom-cache-unavailable",
255
+ "transformers-env-fs-cache-assignment-rejected",
256
+ "transformers-env-fs-cache-unavailable",
257
+ "transformers-env-num-threads-assignment-rejected",
258
+ "transformers-env-num-threads-unavailable",
259
+ "transformers-env-wasm-paths-assignment-rejected",
260
+ "transformers-env-wasm-paths-unavailable",
261
+ "transformers-whisper-pipeline-export-missing",
262
+ ]);
263
+ registerWorkerErrorReasons("ARCANE_AI_PROVIDER_UNAVAILABLE", ["stt"], USE_OPERATION, [
264
+ "stt-transcription-method-unavailable",
265
+ ]);
266
+ registerWorkerErrorReasons("ARCANE_AI_PROVIDER_UNAVAILABLE", ["tts"], USE_OPERATION, [
267
+ "tts-synthesis-method-unavailable",
268
+ ]);
269
+ registerWorkerErrorReasons(
270
+ "ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
271
+ BOTH_WORKER_ROLES,
272
+ LOAD_OPERATION,
273
+ [
274
+ "artifact-graph-kokoro-voice-inventory-missing",
275
+ "artifact-graph-module-worker-target-not-materialized",
276
+ "artifact-graph-negative-request-routes-not-array",
277
+ "artifact-graph-onnx-wasm-configuration-mismatch",
278
+ "artifact-graph-runtime-request-route-ambiguous",
279
+ "artifact-graph-transform-identity-ambiguous",
280
+ "artifact-graph-transform-kind-not-admitted",
281
+ "artifact-graph-transform-module-path-empty",
282
+ "artifact-graph-transform-occurrence-not-positive-safe-integer",
283
+ "artifact-graph-transformers-cache-edge-ambiguous",
284
+ "artifact-graph-worker-configuration-incomplete",
285
+ ],
286
+ );
287
+ registerWorkerErrorReasons(
288
+ "ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
289
+ BOTH_WORKER_ROLES,
290
+ LOAD_OR_USE_OPERATIONS,
291
+ ["artifact-graph-typed-array-constructor-transform-undeclared"],
292
+ );
293
+ registerWorkerErrorReasons(
294
+ "ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
295
+ ["tts"],
296
+ LOAD_OPERATION,
297
+ [
298
+ "kokoro-env-num-threads-field-not-exposed",
299
+ ],
300
+ );
301
+ registerWorkerErrorReasons(
302
+ "ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
303
+ ["stt"],
304
+ LOAD_OPERATION,
305
+ [
306
+ "transformers-env-num-threads-not-positive-safe-integer",
307
+ ],
308
+ );
309
+ for (const label of ["cache-open", "dynamic-import", "fetch", "module-worker"]) {
310
+ registerWorkerErrorReasons(
311
+ "ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
312
+ BOTH_WORKER_ROLES,
313
+ LOAD_OPERATION,
314
+ [
315
+ `artifact-graph-${label}-edges-not-array`,
316
+ `artifact-graph-${label}-edge-not-object`,
317
+ `artifact-graph-${label}-edge-module-path-empty`,
318
+ `artifact-graph-${label}-edge-occurrence-not-positive-safe-integer`,
319
+ `artifact-graph-${label}-edge-policy-not-admitted`,
320
+ `artifact-graph-${label}-edge-identity-ambiguous`,
321
+ ],
322
+ );
323
+ }
324
+ registerWorkerErrorReasons(
325
+ "ARCANE_AI_ARTIFACT_GRAPH_IMPORT_EDGE_UNDECLARED",
326
+ BOTH_WORKER_ROLES,
327
+ LOAD_OR_USE_OPERATIONS,
328
+ [
329
+ "artifact-graph-dynamic-import-edge-undeclared",
330
+ "artifact-graph-dynamic-import-edge-undeclared-inactive-runtime-branch-entered",
331
+ "artifact-graph-dynamic-import-target-undeclared",
332
+ ],
333
+ );
334
+ registerWorkerErrorReasons(
335
+ "ARCANE_AI_ARTIFACT_GRAPH_FETCH_EDGE_UNDECLARED",
336
+ BOTH_WORKER_ROLES,
337
+ LOAD_OR_USE_OPERATIONS,
338
+ [
339
+ "artifact-graph-fetch-edge-undeclared",
340
+ "artifact-graph-fetch-edge-undeclared-inactive-runtime-branch-entered",
341
+ "artifact-graph-fetch-guard-bypassed",
342
+ "artifact-graph-fetch-method-undeclared",
343
+ "artifact-graph-fetch-target-undeclared",
344
+ ],
345
+ );
346
+ registerWorkerErrorReasons(
347
+ "ARCANE_AI_ARTIFACT_GRAPH_CACHE_EDGE_UNDECLARED",
348
+ BOTH_WORKER_ROLES,
349
+ LOAD_OR_USE_OPERATIONS,
350
+ [
351
+ "artifact-graph-cache-match-guard-bypassed",
352
+ "artifact-graph-cache-name-mismatch",
353
+ "artifact-graph-cache-open-edge-undeclared",
354
+ "artifact-graph-cache-open-edge-undeclared-inactive-runtime-branch-entered",
355
+ "artifact-graph-cache-open-guard-bypassed",
356
+ "artifact-graph-cache-read-target-undeclared",
357
+ ],
358
+ );
359
+ registerWorkerErrorReasons(
360
+ "ARCANE_AI_ARTIFACT_GRAPH_WORKER_EDGE_UNDECLARED",
361
+ BOTH_WORKER_ROLES,
362
+ LOAD_OR_USE_OPERATIONS,
363
+ [
364
+ "artifact-graph-module-worker-edge-undeclared",
365
+ "artifact-graph-module-worker-edge-undeclared-inactive-runtime-branch-entered",
366
+ "artifact-graph-module-worker-target-undeclared",
367
+ "artifact-graph-module-worker-type-mismatch",
368
+ ],
369
+ );
370
+ registerWorkerErrorReasons(
371
+ "ARCANE_AI_ARTIFACT_GRAPH_ISOLATION_UNAVAILABLE",
372
+ BOTH_WORKER_ROLES,
373
+ LOAD_OPERATION,
374
+ [
375
+ "artifact-graph-cache-isolation-unavailable",
376
+ "artifact-graph-dynamic-code-constructor-isolation-unavailable",
377
+ "artifact-graph-fetch-isolation-unavailable",
378
+ "artifact-graph-guard-global-collision",
379
+ "artifact-graph-guard-global-definition-rejected",
380
+ "artifact-graph-indexeddb-isolation-unavailable",
381
+ "artifact-graph-opfs-isolation-unavailable",
382
+ "artifact-graph-private-message-port-missing",
383
+ "artifact-graph-setinterval-isolation-unavailable",
384
+ "artifact-graph-settimeout-isolation-unavailable",
385
+ "artifact-graph-typed-array-validation-unavailable",
386
+ "speech-worker-cache-isolation-unavailable",
387
+ "speech-worker-fetch-isolation-unavailable",
388
+ ],
389
+ );
390
+ registerWorkerErrorReasons(
391
+ "ARCANE_AI_ARTIFACT_GRAPH_ISOLATION_UNAVAILABLE",
392
+ BOTH_WORKER_ROLES,
393
+ LOAD_OR_USE_OPERATIONS,
394
+ [
395
+ "artifact-graph-dynamic-code-constructor-rejected",
396
+ "artifact-graph-guard-capability-mismatch",
397
+ "artifact-graph-setinterval-string-callback-rejected",
398
+ "artifact-graph-settimeout-string-callback-rejected",
399
+ "artifact-graph-typed-array-constructor-intrinsic-mismatch",
400
+ "artifact-graph-typed-array-constructor-receiver-not-typed-array",
401
+ ],
402
+ );
403
+ for (const name of [
404
+ "broadcastchannel",
405
+ "eventsource",
406
+ "function",
407
+ "rtcpeerconnection",
408
+ "shadowrealm",
409
+ "sharedworker",
410
+ "websocket",
411
+ "websocketstream",
412
+ "webtransport",
413
+ "worker",
414
+ "xmlhttprequest",
415
+ "eval",
416
+ "importscripts",
417
+ ]) {
418
+ registerWorkerErrorReasons(
419
+ "ARCANE_AI_ARTIFACT_GRAPH_ISOLATION_UNAVAILABLE",
420
+ BOTH_WORKER_ROLES,
421
+ LOAD_OR_USE_OPERATIONS,
422
+ [`artifact-graph-${name}-capability-undeclared`],
423
+ );
424
+ registerWorkerErrorReasons(
425
+ "ARCANE_AI_ARTIFACT_GRAPH_ISOLATION_UNAVAILABLE",
426
+ BOTH_WORKER_ROLES,
427
+ LOAD_OPERATION,
428
+ [`artifact-graph-${name}-isolation-unavailable`],
429
+ );
430
+ }
431
+ registerWorkerErrorReasons(
432
+ "ARCANE_AI_WORKER_MESSAGE_ERROR",
433
+ BOTH_WORKER_ROLES,
434
+ LOAD_OR_USE_OPERATIONS,
435
+ [
436
+ "artifact-graph-module-worker-error-envelope-rejected",
437
+ "artifact-graph-module-worker-initialization-message-rejected",
438
+ ],
439
+ );
440
+
441
+ function operationFailureReason(role, op) {
442
+ if (op === "load") return `${role}-worker-model-load-rejected`;
443
+ if (op === "use") {
444
+ return role === "stt"
445
+ ? "stt-transcription-engine-operation-rejected"
446
+ : "tts-synthesis-engine-operation-rejected";
447
+ }
448
+ if (op === "unload") return `${role}-worker-engine-dispose-rejected`;
449
+ if (op === "dispose") return `${role}-worker-dispose-rejected`;
450
+ if (op === "status") return `${role}-worker-status-rejected`;
451
+ return `${role}-worker-operation-unknown`;
452
+ }
7
453
 
8
- function workerError(code, message, cause) {
454
+ function workerError(code, message, cause, reason) {
9
455
  const error = cause === undefined
10
456
  ? new Error(message)
11
457
  : new Error(message, { cause });
@@ -13,15 +459,35 @@ function workerError(code, message, cause) {
13
459
  ? "AbortError"
14
460
  : "ArcaneSpeechWorkerError";
15
461
  error.code = code;
462
+ if (typeof reason === "string" && reason) error.reason = reason;
463
+ SDK_WORKER_ERRORS.add(error);
16
464
  return error;
17
465
  }
18
466
 
19
- function throwIfAborted(signal) {
467
+ function isSdkWorkerError(value) {
468
+ return value instanceof Error && SDK_WORKER_ERRORS.has(value);
469
+ }
470
+
471
+ function admitWorkerFailure(error, code, message, reason) {
472
+ return isSdkWorkerError(error)
473
+ ? error
474
+ : workerError(code, message, error, reason);
475
+ }
476
+
477
+ function operationReason(role, op, suffix) {
478
+ const subject = op === "use"
479
+ ? role === "stt" ? "stt-transcription" : "tts-synthesis"
480
+ : `${role}-${op}`;
481
+ return `${subject}-${suffix}`;
482
+ }
483
+
484
+ function throwIfAborted(signal, reason) {
20
485
  if (!signal?.aborted) return;
21
486
  throw workerError(
22
487
  "ARCANE_AI_REQUEST_ABORTED",
23
488
  "The speech worker operation was cancelled.",
24
489
  signal.reason,
490
+ reason,
25
491
  );
26
492
  }
27
493
 
@@ -51,119 +517,559 @@ export function collectSpeechTransferables(value) {
51
517
  return transfers;
52
518
  }
53
519
 
54
- function serializedError(error) {
55
- const code = typeof error?.code === "string" && /^ARCANE_AI_[A-Z0-9_]+$/u.test(error.code)
56
- ? error.code
57
- : "ARCANE_AI_PROVIDER_REQUEST_FAILED";
58
- const messages = Object.freeze({
59
- ARCANE_AI_REQUEST_ABORTED: "The speech worker operation was cancelled.",
60
- ARCANE_AI_NOT_READY: "The speech worker is not loaded.",
61
- ARCANE_AI_INVALID_REQUEST: "The speech worker request is invalid.",
62
- ARCANE_AI_INVALID_PROVIDER_RESULT: "The speech engine returned an invalid result.",
63
- ARCANE_AI_UNDECLARED_ARTIFACT: "The speech engine requested an undeclared artifact.",
64
- ARCANE_AI_PROVIDER_UNAVAILABLE: "The selected speech engine is unavailable.",
520
+ function serializedError(error, role, op) {
521
+ const admission = isSdkWorkerError(error)
522
+ ? workerErrorReasonAdmission(error.reason, role, op)
523
+ : null;
524
+ const admittedCode = isSdkWorkerError(error)
525
+ && Object.hasOwn(WORKER_ERROR_MESSAGES, error.code)
526
+ && admission?.code === error.code;
527
+ const code = admittedCode ? error.code : "ARCANE_AI_PROVIDER_REQUEST_FAILED";
528
+ const reason = admittedCode ? error.reason : operationFailureReason(role, op);
529
+ return Object.freeze({
530
+ protocol: SPEECH_WORKER_ERROR_PROTOCOL,
531
+ code,
532
+ message: WORKER_ERROR_MESSAGES[code],
533
+ reason,
65
534
  });
535
+ }
536
+
537
+ function workerErrorReasonAdmission(reason, role, op) {
538
+ const admission = WORKER_ERROR_REASON_ADMISSIONS.get(reason);
539
+ return admission
540
+ && admission.roles.includes(role)
541
+ && admission.operations.includes(op)
542
+ ? admission
543
+ : null;
544
+ }
545
+
546
+ export function normalizeSpeechWorkerErrorEnvelope(value, role, op) {
547
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
548
+ let descriptors;
549
+ try {
550
+ const keys = Reflect.ownKeys(value);
551
+ if (keys.some((key) => typeof key !== "string")
552
+ || keys.sort().join(",") !== "code,message,protocol,reason") return null;
553
+ descriptors = Object.getOwnPropertyDescriptors(value);
554
+ } catch {
555
+ return null;
556
+ }
557
+ for (const key of ["code", "message", "protocol", "reason"]) {
558
+ if (!Object.hasOwn(descriptors[key], "value")) return null;
559
+ }
560
+ const protocol = descriptors.protocol.value;
561
+ const code = descriptors.code.value;
562
+ const message = descriptors.message.value;
563
+ const reason = descriptors.reason.value;
564
+ if (protocol !== SPEECH_WORKER_ERROR_PROTOCOL) return null;
565
+ if (!Object.hasOwn(WORKER_ERROR_MESSAGES, code)) return null;
566
+ if (message !== WORKER_ERROR_MESSAGES[code]) return null;
567
+ const admission = workerErrorReasonAdmission(reason, role, op);
568
+ if (admission?.code !== code) return null;
66
569
  return Object.freeze({
67
570
  code,
68
- message: messages[code] ?? "The speech worker operation failed.",
571
+ message,
572
+ reason,
69
573
  });
70
574
  }
71
575
 
72
- function requiredText(value, label) {
576
+ function requiredText(
577
+ value,
578
+ label,
579
+ reason,
580
+ ) {
73
581
  if (typeof value !== "string" || !value.trim()) {
74
- throw workerError("ARCANE_AI_INVALID_REQUEST", `${label} is required.`);
582
+ throw workerError(
583
+ "ARCANE_AI_INVALID_REQUEST",
584
+ `${label} is required.`,
585
+ undefined,
586
+ reason,
587
+ );
75
588
  }
76
589
  return value.trim();
77
590
  }
78
591
 
592
+ function requiredSampleRate(value, label, fallback) {
593
+ const candidate = value ?? fallback;
594
+ if (!Number.isSafeInteger(candidate) || candidate < 1) {
595
+ throw workerError(
596
+ "ARCANE_AI_INVALID_REQUEST",
597
+ `${label} must be a positive safe integer.`,
598
+ undefined,
599
+ "speech-model-sample-rate-not-positive-safe-integer",
600
+ );
601
+ }
602
+ return candidate;
603
+ }
604
+
605
+ function graphConfiguration(configuration) {
606
+ return configuration?.runtime?.moduleGraph === ARTIFACT_GRAPH_MODULE_GRAPH;
607
+ }
608
+
609
+ function validateMaterializedFile(file, label) {
610
+ if (!file || typeof file !== "object" || Array.isArray(file)) {
611
+ throw workerError(
612
+ "ARCANE_AI_INVALID_REQUEST",
613
+ `${label} must be an object.`,
614
+ undefined,
615
+ "artifact-graph-materialized-file-not-object",
616
+ );
617
+ }
618
+ requiredText(file.path, `${label} path`, "artifact-graph-materialized-path-empty");
619
+ requiredText(file.sourceUrl, `${label} sourceUrl`, "artifact-graph-materialized-source-url-empty");
620
+ requiredText(file.moduleUrl, `${label} moduleUrl`, "artifact-graph-materialized-module-url-empty");
621
+ requiredText(file.mediaType, `${label} mediaType`, "artifact-graph-materialized-media-type-empty");
622
+ if (file.runtimeRequestUrls !== undefined && !Array.isArray(file.runtimeRequestUrls)) {
623
+ throw workerError(
624
+ "ARCANE_AI_INVALID_REQUEST",
625
+ `${label} runtimeRequestUrls must be an array.`,
626
+ undefined,
627
+ "artifact-graph-runtime-request-routes-not-array",
628
+ );
629
+ }
630
+ return file;
631
+ }
632
+
79
633
  function validateConfiguration(configuration, role) {
80
634
  if (!configuration || typeof configuration !== "object" || Array.isArray(configuration)) {
81
- throw workerError("ARCANE_AI_INVALID_REQUEST", "Speech worker configuration is required.");
635
+ throw workerError(
636
+ "ARCANE_AI_INVALID_REQUEST",
637
+ "Speech worker configuration is required.",
638
+ undefined,
639
+ "speech-worker-configuration-missing",
640
+ );
82
641
  }
642
+ const moduleGraph = configuration.runtime?.moduleGraph;
83
643
  if (
84
644
  configuration.role !== role
85
645
  || configuration.runtime?.adapter !== ADAPTERS[role]
86
- || configuration.runtime?.moduleGraph !== "self-contained"
646
+ || (moduleGraph !== "self-contained" && moduleGraph !== ARTIFACT_GRAPH_MODULE_GRAPH)
87
647
  ) {
88
- throw workerError("ARCANE_AI_INVALID_REQUEST", "Speech worker role and runtime adapter do not match.");
648
+ throw workerError(
649
+ "ARCANE_AI_INVALID_REQUEST",
650
+ "Speech worker role, runtime adapter, or module graph does not match.",
651
+ undefined,
652
+ "speech-worker-runtime-selection-mismatch",
653
+ );
89
654
  }
90
- requiredText(configuration.model?.id, "Speech model id");
91
- requiredText(configuration.model?.repository, "Speech model repository");
92
- requiredText(configuration.model?.revision, "Speech model revision");
93
- requiredText(configuration.runtime?.entry, "Speech runtime entry");
655
+ requiredText(configuration.model?.id, "Speech model id", "speech-worker-model-id-empty");
656
+ requiredText(configuration.model?.repository, "Speech model repository", "speech-worker-model-repository-empty");
657
+ requiredText(configuration.model?.revision, "Speech model revision", "speech-worker-model-revision-empty");
658
+ requiredText(configuration.runtime?.entry, "Speech runtime entry", "speech-worker-runtime-entry-empty");
94
659
  if (!Array.isArray(configuration.runtime?.files) || !Array.isArray(configuration.model?.files)) {
95
- throw workerError("ARCANE_AI_INVALID_REQUEST", "Speech runtime and model files are required.");
660
+ throw workerError(
661
+ "ARCANE_AI_INVALID_REQUEST",
662
+ "Speech runtime and model files are required.",
663
+ undefined,
664
+ "speech-worker-materialized-files-missing",
665
+ );
96
666
  }
97
667
  const entry = configuration.runtime.files.find((file) =>
98
668
  file.path === configuration.runtime.entry);
99
669
  if (!entry?.moduleUrl) {
100
- throw workerError("ARCANE_AI_INVALID_REQUEST", "Speech runtime entry was not materialized.");
670
+ throw workerError(
671
+ "ARCANE_AI_INVALID_REQUEST",
672
+ "Speech runtime entry was not materialized.",
673
+ undefined,
674
+ moduleGraph === ARTIFACT_GRAPH_MODULE_GRAPH
675
+ ? "artifact-graph-entrypoint-not-materialized"
676
+ : "speech-runtime-entrypoint-not-materialized",
677
+ );
678
+ }
679
+ if (moduleGraph === ARTIFACT_GRAPH_MODULE_GRAPH) {
680
+ const allFiles = [
681
+ ...configuration.runtime.files,
682
+ ...configuration.model.files,
683
+ ];
684
+ const paths = new Set();
685
+ for (let index = 0; index < allFiles.length; index += 1) {
686
+ const file = validateMaterializedFile(allFiles[index], `Speech file ${String(index)}`);
687
+ if (paths.has(file.path)) {
688
+ throw workerError(
689
+ "ARCANE_AI_INVALID_REQUEST",
690
+ "Speech materialized file paths must be unique.",
691
+ undefined,
692
+ "artifact-graph-materialized-path-ambiguous",
693
+ );
694
+ }
695
+ paths.add(file.path);
696
+ }
697
+ const runtimePaths = new Set(configuration.runtime.files.map((file) => file.path));
698
+ if (
699
+ configuration.security?.secure !== true
700
+ || typeof configuration.runtime.artifactGraphId !== "string"
701
+ || !/^[a-f0-9]{64}$/u.test(configuration.runtime.artifactGraphId)
702
+ || (
703
+ typeof configuration.runtime.guardCapability !== "string"
704
+ || !/^[a-f0-9]{64}$/u.test(configuration.runtime.guardCapability)
705
+ || !STRICT_GRAPH_ADMISSIONS.has(configuration.runtime.artifactGraphAdmission)
706
+ || !configuration.runtime.edges
707
+ || typeof configuration.runtime.edges !== "object"
708
+ || !Array.isArray(configuration.runtime.transforms)
709
+ )
710
+ ) {
711
+ throw workerError(
712
+ "ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
713
+ "Explicit secure:true plus authenticated artifact graph identity, admission, edges, and transforms are required.",
714
+ undefined,
715
+ "artifact-graph-worker-configuration-incomplete",
716
+ );
717
+ }
718
+ const wasm = configuration.runtime.onnxWasm;
719
+ if (
720
+ !wasm
721
+ || wasm.namespace !== ONNX_NAMESPACES[role]
722
+ || !runtimePaths.has(wasm.mjsPath)
723
+ || !runtimePaths.has(wasm.wasmPath)
724
+ ) {
725
+ throw workerError(
726
+ "ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
727
+ "The authenticated ONNX Runtime Web MJS/WASM pair is incomplete or uses the wrong namespace.",
728
+ undefined,
729
+ "artifact-graph-onnx-wasm-configuration-mismatch",
730
+ );
731
+ }
732
+ if (role === "tts" && wasm.numThreads !== undefined) {
733
+ throw workerError(
734
+ "ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
735
+ "Kokoro does not expose a verified numThreads configuration field.",
736
+ undefined,
737
+ "kokoro-env-num-threads-field-not-exposed",
738
+ );
739
+ }
740
+ if (
741
+ wasm.numThreads !== undefined
742
+ && (!Number.isSafeInteger(wasm.numThreads) || wasm.numThreads < 1)
743
+ ) {
744
+ throw workerError(
745
+ "ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
746
+ "Transformers numThreads must be a positive safe integer.",
747
+ undefined,
748
+ "transformers-env-num-threads-not-positive-safe-integer",
749
+ );
750
+ }
751
+ requiredText(configuration.model?.dtype, "Speech model dtype", "speech-worker-model-dtype-empty");
752
+ if (role === "stt") {
753
+ requiredSampleRate(configuration.model.inputSampleRate, "Whisper inputSampleRate");
754
+ } else {
755
+ requiredSampleRate(configuration.model.outputSampleRate, "Kokoro outputSampleRate");
756
+ requiredText(configuration.model.defaultVoice, "Kokoro defaultVoice", "tts-default-voice-empty");
757
+ if (!Array.isArray(configuration.model.voices) || configuration.model.voices.length < 1) {
758
+ throw workerError(
759
+ "ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
760
+ "Kokoro graph configuration requires the caller-owned voice inventory.",
761
+ undefined,
762
+ "artifact-graph-kokoro-voice-inventory-missing",
763
+ );
764
+ }
765
+ }
766
+ if (
767
+ configuration.runtime.negativeRuntimeRequestUrls !== undefined
768
+ && !Array.isArray(configuration.runtime.negativeRuntimeRequestUrls)
769
+ ) {
770
+ throw workerError(
771
+ "ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
772
+ "Artifact graph negative runtime request routes must be an array.",
773
+ undefined,
774
+ "artifact-graph-negative-request-routes-not-array",
775
+ );
776
+ }
101
777
  }
102
778
  return configuration;
103
779
  }
104
780
 
105
- function artifactMap(configuration) {
106
- const map = new Map();
107
- const materialized = new Set();
108
- for (const file of [
781
+ function absoluteRequestUrl(value, scope) {
782
+ const input = typeof value === "string" || value instanceof URL
783
+ ? String(value)
784
+ : value?.url;
785
+ try {
786
+ return new URL(input, scope.location?.href).href;
787
+ } catch {
788
+ throw workerError(
789
+ "ARCANE_AI_UNDECLARED_ARTIFACT",
790
+ "The speech engine requested a malformed artifact URL.",
791
+ undefined,
792
+ "artifact-graph-runtime-request-url-malformed",
793
+ );
794
+ }
795
+ }
796
+
797
+ function createArtifactRoutes(scope, configuration) {
798
+ const positive = new Map();
799
+ const negative = new Set();
800
+ const files = [
109
801
  ...configuration.runtime.files,
110
802
  ...configuration.model.files,
111
- ]) {
112
- const sourceUrl = new URL(file.sourceUrl).href;
113
- map.set(sourceUrl, file.moduleUrl);
114
- materialized.add(file.moduleUrl);
803
+ ];
804
+
805
+ function addPositive(route, file) {
806
+ const absolute = absoluteRequestUrl(route, scope);
807
+ const existing = positive.get(absolute);
808
+ if (existing && existing.path !== file.path) {
809
+ throw workerError(
810
+ "ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
811
+ "An artifact graph runtime request route names more than one file.",
812
+ undefined,
813
+ "artifact-graph-runtime-request-route-ambiguous",
814
+ );
815
+ }
816
+ positive.set(absolute, file);
817
+ }
818
+
819
+ for (const file of files) {
820
+ addPositive(file.sourceUrl, file);
821
+ addPositive(file.moduleUrl, file);
822
+ for (const route of file.runtimeRequestUrls ?? []) addPositive(route, file);
823
+ }
824
+ for (const route of configuration.runtime.negativeRuntimeRequestUrls ?? []) {
825
+ const absolute = absoluteRequestUrl(route, scope);
826
+ if (positive.has(absolute) || negative.has(absolute)) {
827
+ throw workerError(
828
+ "ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
829
+ "Artifact graph positive and negative runtime request routes overlap.",
830
+ undefined,
831
+ "artifact-graph-runtime-request-route-ambiguous",
832
+ );
833
+ }
834
+ negative.add(absolute);
115
835
  }
116
- return Object.freeze({ map, materialized });
836
+ return Object.freeze({ files, positive, negative });
117
837
  }
118
838
 
119
- function installAuthorizedFetch(scope, configuration) {
120
- const original = scope.fetch?.bind(scope);
121
- if (typeof original !== "function") {
122
- throw workerError("ARCANE_AI_PROVIDER_UNAVAILABLE", "Browser fetch is unavailable in the speech worker.");
123
- }
124
- const allowed = artifactMap(configuration);
125
- scope.fetch = async function fetchAuthorizedSpeechArtifact(input, init) {
126
- const requested = typeof Request === "function" && input instanceof Request
127
- ? input.url
128
- : String(input);
129
- let absolute;
839
+ function requestMethod(input, init, scope) {
840
+ const RequestConstructor = scope.Request ?? globalThis.Request;
841
+ const inherited = typeof RequestConstructor === "function" && input instanceof RequestConstructor
842
+ ? input.method
843
+ : "GET";
844
+ return String(init?.method ?? inherited ?? "GET").toUpperCase();
845
+ }
846
+
847
+ function installScopeValue(scope, name, value, reason) {
848
+ const descriptor = Object.getOwnPropertyDescriptor(scope, name);
849
+ if (descriptor?.configurable === false && descriptor.writable !== true) {
850
+ throw workerError(
851
+ "ARCANE_AI_ARTIFACT_GRAPH_ISOLATION_UNAVAILABLE",
852
+ `The speech Worker cannot isolate globalThis.${name}.`,
853
+ undefined,
854
+ reason,
855
+ );
856
+ }
857
+ try {
858
+ if (descriptor?.configurable === false) {
859
+ scope[name] = value;
860
+ } else {
861
+ Object.defineProperty(scope, name, {
862
+ configurable: true,
863
+ enumerable: descriptor?.enumerable ?? true,
864
+ writable: false,
865
+ value,
866
+ });
867
+ }
868
+ } catch (error) {
869
+ throw workerError(
870
+ "ARCANE_AI_ARTIFACT_GRAPH_ISOLATION_UNAVAILABLE",
871
+ `The speech Worker could not isolate globalThis.${name}.`,
872
+ error,
873
+ reason,
874
+ );
875
+ }
876
+ return function restoreScopeValue() {
877
+ if (descriptor) Object.defineProperty(scope, name, descriptor);
878
+ else delete scope[name];
879
+ };
880
+ }
881
+
882
+ function restoreScopeValues(restores) {
883
+ for (const restore of restores.splice(0).reverse()) {
130
884
  try {
131
- absolute = new URL(requested, scope.location?.href).href;
885
+ restore();
132
886
  } catch {
133
- throw workerError("ARCANE_AI_UNDECLARED_ARTIFACT", "The speech engine requested an invalid artifact URL.");
887
+ // Terminating the owning Worker remains the final isolation boundary.
134
888
  }
135
- const replacement = allowed.map.get(absolute);
136
- if (replacement) return original(replacement, init);
137
- if (allowed.materialized.has(absolute)) return original(absolute, init);
889
+ }
890
+ }
891
+
892
+ function installObjectValue(object, name, value, label, reason) {
893
+ const descriptor = Object.getOwnPropertyDescriptor(object, name);
894
+ if (descriptor?.configurable === false && descriptor.writable !== true) {
138
895
  throw workerError(
139
- "ARCANE_AI_UNDECLARED_ARTIFACT",
140
- "The speech engine requested an artifact outside its admitted file map.",
896
+ "ARCANE_AI_ARTIFACT_GRAPH_ISOLATION_UNAVAILABLE",
897
+ `The speech Worker cannot isolate ${label}.`,
898
+ undefined,
899
+ reason,
141
900
  );
901
+ }
902
+ try {
903
+ Object.defineProperty(object, name, {
904
+ configurable: true,
905
+ enumerable: descriptor?.enumerable ?? true,
906
+ writable: false,
907
+ value,
908
+ });
909
+ } catch (error) {
910
+ throw workerError(
911
+ "ARCANE_AI_ARTIFACT_GRAPH_ISOLATION_UNAVAILABLE",
912
+ `The speech Worker could not isolate ${label}.`,
913
+ error,
914
+ reason,
915
+ );
916
+ }
917
+ return function restoreObjectValue() {
918
+ if (descriptor) Object.defineProperty(object, name, descriptor);
919
+ else delete object[name];
142
920
  };
143
- return function restoreSpeechWorkerFetch() {
144
- scope.fetch = original;
145
- };
146
921
  }
147
922
 
148
- function installCacheIsolation(scope) {
149
- const ownDescriptor = Object.getOwnPropertyDescriptor(scope, "caches");
150
- if (ownDescriptor && ownDescriptor.configurable === false) {
923
+ function createRouteReader(scope, configuration, originalFetch) {
924
+ const routes = createArtifactRoutes(scope, configuration);
925
+ const ResponseConstructor = scope.Response ?? globalThis.Response;
926
+
927
+ async function responseFor(resolution, signal) {
928
+ if (resolution.kind === "negative") {
929
+ if (typeof ResponseConstructor !== "function") {
930
+ throw workerError(
931
+ "ARCANE_AI_PROVIDER_UNAVAILABLE",
932
+ "Browser Response is unavailable in the speech Worker.",
933
+ undefined,
934
+ "artifact-graph-negative-response-constructor-unavailable",
935
+ );
936
+ }
937
+ return new ResponseConstructor(null, {
938
+ status: 404,
939
+ statusText: "Not Found",
940
+ headers: { "cache-control": "no-store" },
941
+ });
942
+ }
943
+ return originalFetch(resolution.file.moduleUrl, {
944
+ method: "GET",
945
+ credentials: "omit",
946
+ redirect: "error",
947
+ signal,
948
+ });
949
+ }
950
+
951
+ function resolve(input) {
952
+ const absolute = absoluteRequestUrl(input, scope);
953
+ const file = routes.positive.get(absolute);
954
+ if (file) return Object.freeze({ kind: "file", absolute, file });
955
+ if (routes.negative.has(absolute)) {
956
+ return Object.freeze({ kind: "negative", absolute, file: null });
957
+ }
958
+ return null;
959
+ }
960
+
961
+ function cacheForPaths(targetPaths) {
962
+ const admittedPaths = new Set(targetPaths);
963
+ return Object.freeze({
964
+ async match(input) {
965
+ const resolution = resolve(input);
966
+ if (
967
+ !resolution
968
+ || resolution.kind !== "file"
969
+ || !admittedPaths.has(resolution.file.path)
970
+ ) {
971
+ throw workerError(
972
+ "ARCANE_AI_ARTIFACT_GRAPH_CACHE_EDGE_UNDECLARED",
973
+ "The runtime cache read target is outside its admitted artifact graph edge.",
974
+ undefined,
975
+ "artifact-graph-cache-read-target-undeclared",
976
+ );
977
+ }
978
+ return responseFor(resolution);
979
+ },
980
+ async put() {
981
+ throw workerError(
982
+ "ARCANE_AI_UNDECLARED_ARTIFACT",
983
+ "Speech runtime cache writes are disabled; DBOPFS is the sole durable artifact store.",
984
+ undefined,
985
+ "artifact-graph-cache-write-rejected",
986
+ );
987
+ },
988
+ async add() {
989
+ throw workerError(
990
+ "ARCANE_AI_UNDECLARED_ARTIFACT",
991
+ "Speech runtime cache writes are disabled; DBOPFS is the sole durable artifact store.",
992
+ undefined,
993
+ "artifact-graph-cache-write-rejected",
994
+ );
995
+ },
996
+ async addAll() {
997
+ throw workerError(
998
+ "ARCANE_AI_UNDECLARED_ARTIFACT",
999
+ "Speech runtime cache writes are disabled; DBOPFS is the sole durable artifact store.",
1000
+ undefined,
1001
+ "artifact-graph-cache-write-rejected",
1002
+ );
1003
+ },
1004
+ async delete() {
1005
+ return false;
1006
+ },
1007
+ async keys() {
1008
+ return Object.freeze([]);
1009
+ },
1010
+ });
1011
+ }
1012
+
1013
+ const cache = cacheForPaths(routes.files.map((file) => file.path));
1014
+ return Object.freeze({ cache, cacheForPaths, resolve, responseFor, routes });
1015
+ }
1016
+
1017
+ function installLegacyAuthorizedFetch(scope, configuration) {
1018
+ const original = scope.fetch?.bind(scope);
1019
+ if (typeof original !== "function") {
151
1020
  throw workerError(
152
1021
  "ARCANE_AI_PROVIDER_UNAVAILABLE",
153
- "The speech worker cannot isolate the browser cache API.",
1022
+ "Browser fetch is unavailable in the speech Worker.",
1023
+ undefined,
1024
+ "speech-worker-fetch-unavailable",
154
1025
  );
155
1026
  }
1027
+ const reader = createRouteReader(scope, configuration, original);
1028
+ const authorized = async function fetchAuthorizedSpeechArtifact(input, init) {
1029
+ if (requestMethod(input, init, scope) !== "GET") {
1030
+ throw workerError(
1031
+ "ARCANE_AI_UNDECLARED_ARTIFACT",
1032
+ "Legacy speech artifacts admit only GET requests.",
1033
+ undefined,
1034
+ "speech-worker-artifact-request-method-rejected",
1035
+ );
1036
+ }
1037
+ const resolution = reader.resolve(input);
1038
+ if (!resolution) {
1039
+ throw workerError(
1040
+ "ARCANE_AI_UNDECLARED_ARTIFACT",
1041
+ "The speech engine requested an artifact outside its admitted file map.",
1042
+ undefined,
1043
+ "speech-worker-artifact-request-undeclared",
1044
+ );
1045
+ }
1046
+ return reader.responseFor(resolution, init?.signal);
1047
+ };
1048
+ const restore = installScopeValue(
1049
+ scope,
1050
+ "fetch",
1051
+ authorized,
1052
+ "speech-worker-fetch-isolation-unavailable",
1053
+ );
1054
+ return Object.freeze({ cache: null, cleanup: restore });
1055
+ }
1056
+
1057
+ function installDeniedCacheIsolation(scope) {
156
1058
  const denied = Object.freeze({
157
1059
  async open() {
158
1060
  throw workerError(
159
1061
  "ARCANE_AI_UNDECLARED_ARTIFACT",
160
1062
  "Speech runtime cache access is disabled; DBOPFS admission is the sole artifact source.",
1063
+ undefined,
1064
+ "speech-worker-cache-open-rejected",
161
1065
  );
162
1066
  },
163
1067
  async match() {
164
1068
  throw workerError(
165
1069
  "ARCANE_AI_UNDECLARED_ARTIFACT",
166
1070
  "Speech runtime cache access is disabled; DBOPFS admission is the sole artifact source.",
1071
+ undefined,
1072
+ "speech-worker-cache-match-rejected",
167
1073
  );
168
1074
  },
169
1075
  async has() {
@@ -176,54 +1082,915 @@ function installCacheIsolation(scope) {
176
1082
  return false;
177
1083
  },
178
1084
  });
179
- try {
180
- Object.defineProperty(scope, "caches", {
181
- configurable: true,
182
- enumerable: ownDescriptor?.enumerable ?? true,
183
- writable: false,
184
- value: denied,
1085
+ return installScopeValue(
1086
+ scope,
1087
+ "caches",
1088
+ denied,
1089
+ "speech-worker-cache-isolation-unavailable",
1090
+ );
1091
+ }
1092
+
1093
+ function edgeKey(modulePath, occurrence) {
1094
+ return `${modulePath}\u0000${String(occurrence)}`;
1095
+ }
1096
+
1097
+ function edgeMap(edges, label) {
1098
+ if (!Array.isArray(edges)) {
1099
+ throw workerError(
1100
+ "ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
1101
+ `Artifact graph ${label} edges must be an array.`,
1102
+ undefined,
1103
+ `artifact-graph-${label}-edges-not-array`,
1104
+ );
1105
+ }
1106
+ const map = new Map();
1107
+ for (const edge of edges) {
1108
+ if (!edge || typeof edge !== "object" || Array.isArray(edge)) {
1109
+ throw workerError(
1110
+ "ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
1111
+ `Artifact graph ${label} edge must be an object.`,
1112
+ undefined,
1113
+ `artifact-graph-${label}-edge-not-object`,
1114
+ );
1115
+ }
1116
+ if (typeof edge.modulePath !== "string" || !edge.modulePath) {
1117
+ throw workerError(
1118
+ "ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
1119
+ `Artifact graph ${label} edge modulePath must be nonempty.`,
1120
+ undefined,
1121
+ `artifact-graph-${label}-edge-module-path-empty`,
1122
+ );
1123
+ }
1124
+ if (!Number.isSafeInteger(edge.occurrence) || edge.occurrence < 1) {
1125
+ throw workerError(
1126
+ "ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
1127
+ `Artifact graph ${label} edge occurrence must be a positive safe integer.`,
1128
+ undefined,
1129
+ `artifact-graph-${label}-edge-occurrence-not-positive-safe-integer`,
1130
+ );
1131
+ }
1132
+ if (edge.edgePolicy !== "artifact-targets-admitted"
1133
+ && edge.edgePolicy !== "inactive-runtime-branch-rejected") {
1134
+ throw workerError(
1135
+ "ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
1136
+ `Artifact graph ${label} edge policy is not admitted.`,
1137
+ undefined,
1138
+ `artifact-graph-${label}-edge-policy-not-admitted`,
1139
+ );
1140
+ }
1141
+ const key = edgeKey(edge.modulePath, edge.occurrence);
1142
+ if (map.has(key)) {
1143
+ throw workerError(
1144
+ "ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
1145
+ `Artifact graph ${label} edge identity is ambiguous.`,
1146
+ undefined,
1147
+ `artifact-graph-${label}-edge-identity-ambiguous`,
1148
+ );
1149
+ }
1150
+ map.set(key, edge);
1151
+ }
1152
+ return map;
1153
+ }
1154
+
1155
+ function transformSet(transforms, kind) {
1156
+ const result = new Set();
1157
+ for (const transform of transforms) {
1158
+ if (!ARTIFACT_GRAPH_TRANSFORM_KINDS.has(transform?.kind)) {
1159
+ throw workerError(
1160
+ "ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
1161
+ "Artifact graph Worker configuration contains a transform kind that is not admitted.",
1162
+ undefined,
1163
+ "artifact-graph-transform-kind-not-admitted",
1164
+ );
1165
+ }
1166
+ if (transform?.kind !== kind) continue;
1167
+ if (typeof transform.modulePath !== "string" || !transform.modulePath) {
1168
+ throw workerError(
1169
+ "ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
1170
+ `Artifact graph ${kind} transform modulePath must be nonempty.`,
1171
+ undefined,
1172
+ "artifact-graph-transform-module-path-empty",
1173
+ );
1174
+ }
1175
+ if (!Number.isSafeInteger(transform.occurrence) || transform.occurrence < 1) {
1176
+ throw workerError(
1177
+ "ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
1178
+ `Artifact graph ${kind} transform occurrence must be a positive safe integer.`,
1179
+ undefined,
1180
+ "artifact-graph-transform-occurrence-not-positive-safe-integer",
1181
+ );
1182
+ }
1183
+ const key = edgeKey(transform.modulePath, transform.occurrence);
1184
+ if (result.has(key)) {
1185
+ throw workerError(
1186
+ "ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
1187
+ `Artifact graph ${kind} transform identity is ambiguous.`,
1188
+ undefined,
1189
+ "artifact-graph-transform-identity-ambiguous",
1190
+ );
1191
+ }
1192
+ result.add(key);
1193
+ }
1194
+ return result;
1195
+ }
1196
+
1197
+ function typedArrayIntrinsics(scope) {
1198
+ const constructors = new Map();
1199
+ for (const name of [
1200
+ "BigInt64Array",
1201
+ "BigUint64Array",
1202
+ "Float32Array",
1203
+ "Float64Array",
1204
+ "Int8Array",
1205
+ "Int16Array",
1206
+ "Int32Array",
1207
+ "Uint8Array",
1208
+ "Uint8ClampedArray",
1209
+ "Uint16Array",
1210
+ "Uint32Array",
1211
+ ]) {
1212
+ const Constructor = scope[name] ?? globalThis[name];
1213
+ if (typeof Constructor === "function" && Constructor.prototype) {
1214
+ constructors.set(Constructor.prototype, Constructor);
1215
+ }
1216
+ }
1217
+ const ArrayBufferConstructor = scope.ArrayBuffer ?? globalThis.ArrayBuffer;
1218
+ const DataViewConstructor = scope.DataView ?? globalThis.DataView;
1219
+ if (typeof ArrayBufferConstructor?.isView !== "function") {
1220
+ throw workerError(
1221
+ "ARCANE_AI_ARTIFACT_GRAPH_ISOLATION_UNAVAILABLE",
1222
+ "Authenticated artifact graph typed-array validation is unavailable.",
1223
+ undefined,
1224
+ "artifact-graph-typed-array-validation-unavailable",
1225
+ );
1226
+ }
1227
+ return Object.freeze({
1228
+ constructors,
1229
+ dataViewPrototype: DataViewConstructor?.prototype ?? null,
1230
+ getPrototypeOf: Object.getPrototypeOf,
1231
+ hasOwn: Object.hasOwn,
1232
+ isView: ArrayBufferConstructor.isView.bind(ArrayBufferConstructor),
1233
+ });
1234
+ }
1235
+
1236
+ function runtimeFileMap(configuration) {
1237
+ return new Map(configuration.runtime.files.map((file) => [file.path, file]));
1238
+ }
1239
+
1240
+ function targetRecords(edge) {
1241
+ if (Array.isArray(edge.targets)) return edge.targets;
1242
+ if (Array.isArray(edge.targetPaths)) {
1243
+ return edge.targetPaths.map((targetPath) => ({ targetPath }));
1244
+ }
1245
+ return [];
1246
+ }
1247
+
1248
+ function targetForRequest(edge, requested, files, sourceFile) {
1249
+ const value = String(requested);
1250
+ for (const target of targetRecords(edge)) {
1251
+ const file = files.get(target?.targetPath);
1252
+ if (!file) continue;
1253
+ if (target.match === "exact-runtime-specifier") {
1254
+ if (value === target.exactSpecifier) return file;
1255
+ continue;
1256
+ }
1257
+ if (target.match === "self-module-url") {
1258
+ if (file.path === sourceFile?.path && value === sourceFile.moduleUrl) return file;
1259
+ continue;
1260
+ }
1261
+ if (target.match === "materialized-module-url" && value === file.moduleUrl) {
1262
+ return file;
1263
+ }
1264
+ }
1265
+ return null;
1266
+ }
1267
+
1268
+ function nestedWorkerUrl(role) {
1269
+ const url = new URL(
1270
+ role === "stt"
1271
+ ? "./browser-whisper-worker.mjs"
1272
+ : "./browser-kokoro-worker.mjs",
1273
+ import.meta.url,
1274
+ );
1275
+ url.searchParams.set("arcaneSpeechWorkerMode", "artifact-module-worker");
1276
+ return url;
1277
+ }
1278
+
1279
+ function nestedWorkerFailureEvent(scope, error) {
1280
+ const ErrorEventConstructor = scope.ErrorEvent ?? globalThis.ErrorEvent;
1281
+ if (typeof ErrorEventConstructor === "function") {
1282
+ return new ErrorEventConstructor("error", {
1283
+ error,
1284
+ message: error.message,
1285
+ cancelable: true,
185
1286
  });
1287
+ }
1288
+ const EventConstructor = scope.Event ?? globalThis.Event;
1289
+ const event = new EventConstructor("error", { cancelable: true });
1290
+ Object.defineProperties(event, {
1291
+ error: { value: error },
1292
+ message: { value: error.message },
1293
+ });
1294
+ return event;
1295
+ }
1296
+
1297
+ function createArtifactGraphGuard(scope, configuration, role, reader, originalWorker) {
1298
+ const files = runtimeFileMap(configuration);
1299
+ const edges = configuration.runtime.edges;
1300
+ const imports = edgeMap(edges.dynamicImports ?? [], "dynamic-import");
1301
+ const fetches = edgeMap(edges.fetches ?? [], "fetch");
1302
+ const workers = edgeMap(edges.moduleWorkers ?? [], "module-worker");
1303
+ const cacheOpens = edgeMap(edges.cacheOpens ?? [], "cache-open");
1304
+ const typedArrayConstructors = transformSet(
1305
+ configuration.runtime.transforms,
1306
+ "typed-array-constructor",
1307
+ );
1308
+ transformSet(configuration.runtime.transforms, "function-return-this-to-global-this");
1309
+ const typedArrays = typedArrayIntrinsics(scope);
1310
+ const guardCapability = configuration.runtime.guardCapability;
1311
+ const nestedWorkers = new Set();
1312
+
1313
+ function assertGuardCapability(candidate) {
1314
+ if (candidate === guardCapability) return;
1315
+ throw workerError(
1316
+ "ARCANE_AI_ARTIFACT_GRAPH_ISOLATION_UNAVAILABLE",
1317
+ "Authenticated artifact graph runtime guard capability did not match.",
1318
+ undefined,
1319
+ "artifact-graph-guard-capability-mismatch",
1320
+ );
1321
+ }
1322
+
1323
+ function declaredEdge(map, modulePath, occurrence, code, reason) {
1324
+ const edge = map.get(edgeKey(modulePath, occurrence));
1325
+ if (edge?.edgePolicy === "artifact-targets-admitted") return edge;
1326
+ if (edge?.edgePolicy === "inactive-runtime-branch-rejected") {
1327
+ throw workerError(
1328
+ code,
1329
+ "The runtime entered an artifact graph branch declared inactive.",
1330
+ undefined,
1331
+ `${reason}-inactive-runtime-branch-entered`,
1332
+ );
1333
+ }
1334
+ throw workerError(
1335
+ code,
1336
+ "The runtime used an undeclared authenticated artifact graph edge.",
1337
+ undefined,
1338
+ reason,
1339
+ );
1340
+ }
1341
+
1342
+ function fetchEdgeAllows(edge, resolution) {
1343
+ if (resolution.kind === "negative") {
1344
+ return (edge.negativeRuntimeRequestUrls ?? [])
1345
+ .map((route) => absoluteRequestUrl(route, scope))
1346
+ .includes(resolution.absolute);
1347
+ }
1348
+ const paths = new Set([
1349
+ ...(edge.targetPaths ?? []),
1350
+ ...targetRecords(edge).map((target) => target?.targetPath),
1351
+ ]);
1352
+ if (!paths.has(resolution.file.path)) return false;
1353
+ if (resolution.absolute === absoluteRequestUrl(resolution.file.moduleUrl, scope)) {
1354
+ return edge.allowMaterializedUrls === true;
1355
+ }
1356
+ return true;
1357
+ }
1358
+
1359
+ const guard = Object.freeze({
1360
+ protocol: "arcane-ai-browser-speech-artifact-graph-runtime/1",
1361
+
1362
+ async dynamicImport(capability, modulePath, occurrence, specifier) {
1363
+ assertGuardCapability(capability);
1364
+ const edge = declaredEdge(
1365
+ imports,
1366
+ modulePath,
1367
+ occurrence,
1368
+ "ARCANE_AI_ARTIFACT_GRAPH_IMPORT_EDGE_UNDECLARED",
1369
+ "artifact-graph-dynamic-import-edge-undeclared",
1370
+ );
1371
+ const sourceFile = files.get(modulePath);
1372
+ const target = targetForRequest(edge, specifier, files, sourceFile);
1373
+ if (!target) {
1374
+ throw workerError(
1375
+ "ARCANE_AI_ARTIFACT_GRAPH_IMPORT_EDGE_UNDECLARED",
1376
+ "The runtime dynamic import target is outside its admitted artifact graph edge.",
1377
+ undefined,
1378
+ "artifact-graph-dynamic-import-target-undeclared",
1379
+ );
1380
+ }
1381
+ return import(target.moduleUrl);
1382
+ },
1383
+
1384
+ async fetch(capability, modulePath, occurrence, input, init) {
1385
+ assertGuardCapability(capability);
1386
+ const edge = declaredEdge(
1387
+ fetches,
1388
+ modulePath,
1389
+ occurrence,
1390
+ "ARCANE_AI_ARTIFACT_GRAPH_FETCH_EDGE_UNDECLARED",
1391
+ "artifact-graph-fetch-edge-undeclared",
1392
+ );
1393
+ const method = requestMethod(input, init, scope);
1394
+ if (!(edge.methods ?? ["GET"]).includes(method)) {
1395
+ throw workerError(
1396
+ "ARCANE_AI_ARTIFACT_GRAPH_FETCH_EDGE_UNDECLARED",
1397
+ "The runtime fetch method is outside its admitted artifact graph edge.",
1398
+ undefined,
1399
+ "artifact-graph-fetch-method-undeclared",
1400
+ );
1401
+ }
1402
+ const resolution = reader.resolve(input);
1403
+ if (!resolution || !fetchEdgeAllows(edge, resolution)) {
1404
+ throw workerError(
1405
+ "ARCANE_AI_ARTIFACT_GRAPH_FETCH_EDGE_UNDECLARED",
1406
+ "The runtime fetch target is outside its admitted artifact graph edge.",
1407
+ undefined,
1408
+ "artifact-graph-fetch-target-undeclared",
1409
+ );
1410
+ }
1411
+ return reader.responseFor(resolution, init?.signal);
1412
+ },
1413
+
1414
+ async openCache(capability, modulePath, occurrence, cacheName) {
1415
+ assertGuardCapability(capability);
1416
+ const edge = declaredEdge(
1417
+ cacheOpens,
1418
+ modulePath,
1419
+ occurrence,
1420
+ "ARCANE_AI_ARTIFACT_GRAPH_CACHE_EDGE_UNDECLARED",
1421
+ "artifact-graph-cache-open-edge-undeclared",
1422
+ );
1423
+ if (cacheName !== edge.cacheName) {
1424
+ throw workerError(
1425
+ "ARCANE_AI_ARTIFACT_GRAPH_CACHE_EDGE_UNDECLARED",
1426
+ "The runtime cache name is outside its admitted artifact graph edge.",
1427
+ undefined,
1428
+ "artifact-graph-cache-name-mismatch",
1429
+ );
1430
+ }
1431
+ return reader.cacheForPaths(edge.targetPaths);
1432
+ },
1433
+
1434
+ createWorker(capability, modulePath, occurrence, specifier, options = {}) {
1435
+ assertGuardCapability(capability);
1436
+ const edge = declaredEdge(
1437
+ workers,
1438
+ modulePath,
1439
+ occurrence,
1440
+ "ARCANE_AI_ARTIFACT_GRAPH_WORKER_EDGE_UNDECLARED",
1441
+ "artifact-graph-module-worker-edge-undeclared",
1442
+ );
1443
+ const sourceFile = files.get(modulePath);
1444
+ const target = targetForRequest(edge, specifier, files, sourceFile);
1445
+ if (!target) {
1446
+ throw workerError(
1447
+ "ARCANE_AI_ARTIFACT_GRAPH_WORKER_EDGE_UNDECLARED",
1448
+ "The runtime module Worker target is outside its admitted artifact graph edge.",
1449
+ undefined,
1450
+ "artifact-graph-module-worker-target-undeclared",
1451
+ );
1452
+ }
1453
+ if (options?.type !== "module") {
1454
+ throw workerError(
1455
+ "ARCANE_AI_ARTIFACT_GRAPH_WORKER_EDGE_UNDECLARED",
1456
+ "Authenticated artifact graph child Workers must be module Workers.",
1457
+ undefined,
1458
+ "artifact-graph-module-worker-type-mismatch",
1459
+ );
1460
+ }
1461
+ if (typeof originalWorker !== "function") {
1462
+ throw workerError(
1463
+ "ARCANE_AI_PROVIDER_UNAVAILABLE",
1464
+ "Nested browser module Workers are unavailable.",
1465
+ undefined,
1466
+ "artifact-graph-module-worker-constructor-unavailable",
1467
+ );
1468
+ }
1469
+ const worker = new originalWorker(nestedWorkerUrl(role), {
1470
+ type: "module",
1471
+ name: typeof options.name === "string" ? options.name : "arcane-speech-artifact-module",
1472
+ });
1473
+ nestedWorkers.add(worker);
1474
+ const onBootstrapMessage = (event) => {
1475
+ if (event.data?.protocol !== NESTED_WORKER_PROTOCOL
1476
+ || event.data?.event !== "artifact-module-worker-bootstrap-rejected") return;
1477
+ event.stopImmediatePropagation?.();
1478
+ const admitted = normalizeSpeechWorkerErrorEnvelope(event.data.error, role, "load");
1479
+ const failure = admitted
1480
+ ? workerError(admitted.code, admitted.message, undefined, admitted.reason)
1481
+ : workerError(
1482
+ "ARCANE_AI_WORKER_MESSAGE_ERROR",
1483
+ "The authenticated artifact module Worker error envelope was rejected.",
1484
+ undefined,
1485
+ "artifact-graph-module-worker-error-envelope-rejected",
1486
+ );
1487
+ worker.dispatchEvent?.(nestedWorkerFailureEvent(scope, failure));
1488
+ };
1489
+ worker.addEventListener?.("message", onBootstrapMessage);
1490
+ try {
1491
+ worker.postMessage({
1492
+ protocol: NESTED_WORKER_PROTOCOL,
1493
+ op: "initialize-authenticated-artifact-module-worker",
1494
+ role,
1495
+ targetPath: target.path,
1496
+ configuration,
1497
+ });
1498
+ } catch (error) {
1499
+ nestedWorkers.delete(worker);
1500
+ worker.terminate();
1501
+ throw workerError(
1502
+ "ARCANE_AI_WORKER_MESSAGE_ERROR",
1503
+ "The authenticated artifact module Worker initialization message was rejected.",
1504
+ error,
1505
+ "artifact-graph-module-worker-initialization-message-rejected",
1506
+ );
1507
+ }
1508
+ return worker;
1509
+ },
1510
+
1511
+ typedArrayConstructor(capability, modulePath, occurrence, receiver) {
1512
+ assertGuardCapability(capability);
1513
+ if (!typedArrayConstructors.has(edgeKey(modulePath, occurrence))) {
1514
+ throw workerError(
1515
+ "ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
1516
+ "The runtime used an undeclared typed-array constructor transform.",
1517
+ undefined,
1518
+ "artifact-graph-typed-array-constructor-transform-undeclared",
1519
+ );
1520
+ }
1521
+ if (
1522
+ !typedArrays.isView(receiver)
1523
+ || typedArrays.getPrototypeOf(receiver) === typedArrays.dataViewPrototype
1524
+ || typedArrays.hasOwn(receiver, "constructor")
1525
+ ) {
1526
+ throw workerError(
1527
+ "ARCANE_AI_ARTIFACT_GRAPH_ISOLATION_UNAVAILABLE",
1528
+ "The typed-array constructor transform received an unauthenticated receiver.",
1529
+ undefined,
1530
+ "artifact-graph-typed-array-constructor-receiver-not-typed-array",
1531
+ );
1532
+ }
1533
+ const Constructor = typedArrays.constructors.get(
1534
+ typedArrays.getPrototypeOf(receiver),
1535
+ );
1536
+ if (typeof Constructor !== "function") {
1537
+ throw workerError(
1538
+ "ARCANE_AI_ARTIFACT_GRAPH_ISOLATION_UNAVAILABLE",
1539
+ "The typed-array constructor transform receiver does not use an intrinsic prototype.",
1540
+ undefined,
1541
+ "artifact-graph-typed-array-constructor-intrinsic-mismatch",
1542
+ );
1543
+ }
1544
+ return Constructor;
1545
+ },
1546
+ });
1547
+
1548
+ return Object.freeze({
1549
+ guard,
1550
+ transformersCache: (() => {
1551
+ const edgesForTransformers = [...cacheOpens.values()].filter((edge) =>
1552
+ edge.edgePolicy === "artifact-targets-admitted"
1553
+ && edge.cacheName === "transformers-cache");
1554
+ if (edgesForTransformers.length === 0) return null;
1555
+ if (edgesForTransformers.length !== 1) {
1556
+ throw workerError(
1557
+ "ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
1558
+ "The Transformers custom cache edge is ambiguous.",
1559
+ undefined,
1560
+ "artifact-graph-transformers-cache-edge-ambiguous",
1561
+ );
1562
+ }
1563
+ return reader.cacheForPaths(edgesForTransformers[0].targetPaths);
1564
+ })(),
1565
+ cleanup() {
1566
+ for (const worker of nestedWorkers) {
1567
+ try {
1568
+ worker.terminate();
1569
+ } catch {
1570
+ // The owning speech Worker is already being torn down.
1571
+ }
1572
+ }
1573
+ nestedWorkers.clear();
1574
+ },
1575
+ });
1576
+ }
1577
+
1578
+ function installDynamicCodeConstructorIsolation() {
1579
+ const rejectingConstructor = function rejectArtifactGraphDynamicCodeConstructor() {
1580
+ throw workerError(
1581
+ "ARCANE_AI_ARTIFACT_GRAPH_ISOLATION_UNAVAILABLE",
1582
+ "Authenticated artifact graph runtime code cannot construct executable strings.",
1583
+ undefined,
1584
+ "artifact-graph-dynamic-code-constructor-rejected",
1585
+ );
1586
+ };
1587
+ const prototypes = new Set([
1588
+ Object.getPrototypeOf(function artifactGraphFunctionProbe() {}),
1589
+ Object.getPrototypeOf(async function artifactGraphAsyncFunctionProbe() {}),
1590
+ Object.getPrototypeOf(function* artifactGraphGeneratorFunctionProbe() {}),
1591
+ Object.getPrototypeOf(async function* artifactGraphAsyncGeneratorFunctionProbe() {}),
1592
+ ]);
1593
+ const restores = [];
1594
+ try {
1595
+ for (const prototype of prototypes) {
1596
+ const descriptor = Object.getOwnPropertyDescriptor(prototype, "constructor");
1597
+ if (!descriptor || (descriptor.configurable === false && descriptor.writable !== true)) {
1598
+ throw workerError(
1599
+ "ARCANE_AI_ARTIFACT_GRAPH_ISOLATION_UNAVAILABLE",
1600
+ "The Worker cannot isolate a dynamic-code constructor prototype.",
1601
+ undefined,
1602
+ "artifact-graph-dynamic-code-constructor-isolation-unavailable",
1603
+ );
1604
+ }
1605
+ Object.defineProperty(prototype, "constructor", {
1606
+ ...descriptor,
1607
+ value: rejectingConstructor,
1608
+ });
1609
+ restores.push(() => Object.defineProperty(prototype, "constructor", descriptor));
1610
+ }
1611
+ } catch (error) {
1612
+ restoreScopeValues(restores);
1613
+ throw error;
1614
+ }
1615
+ return () => restoreScopeValues(restores);
1616
+ }
1617
+
1618
+ function installStringTimerIsolation(scope, name) {
1619
+ const original = scope[name];
1620
+ if (typeof original !== "function") return () => undefined;
1621
+ const invoke = original.bind(scope);
1622
+ return installScopeValue(
1623
+ scope,
1624
+ name,
1625
+ function authenticatedArtifactGraphTimer(callback, ...arguments_) {
1626
+ if (typeof callback !== "function") {
1627
+ throw workerError(
1628
+ "ARCANE_AI_ARTIFACT_GRAPH_ISOLATION_UNAVAILABLE",
1629
+ `Authenticated artifact graph ${name} requires a function callback.`,
1630
+ undefined,
1631
+ `artifact-graph-${name.toLowerCase()}-string-callback-rejected`,
1632
+ );
1633
+ }
1634
+ return invoke(callback, ...arguments_);
1635
+ },
1636
+ `artifact-graph-${name.toLowerCase()}-isolation-unavailable`,
1637
+ );
1638
+ }
1639
+
1640
+ function installArtifactGraphEnvironment(scope, configuration, role) {
1641
+ const originalFetch = scope.fetch?.bind(scope);
1642
+ const originalWorker = scope.Worker;
1643
+ const strictSecurity = configuration.security?.secure === true;
1644
+ if (typeof originalFetch !== "function") {
1645
+ throw workerError(
1646
+ "ARCANE_AI_PROVIDER_UNAVAILABLE",
1647
+ "Browser fetch is unavailable in the speech Worker.",
1648
+ undefined,
1649
+ "artifact-graph-fetch-constructor-unavailable",
1650
+ );
1651
+ }
1652
+ if (GRAPH_GUARD_NAME in scope) {
1653
+ throw workerError(
1654
+ "ARCANE_AI_ARTIFACT_GRAPH_ISOLATION_UNAVAILABLE",
1655
+ "The authenticated artifact graph guard global already exists.",
1656
+ undefined,
1657
+ "artifact-graph-guard-global-collision",
1658
+ );
1659
+ }
1660
+ const reader = createRouteReader(scope, configuration, originalFetch);
1661
+ const graphGuard = createArtifactGraphGuard(
1662
+ scope,
1663
+ configuration,
1664
+ role,
1665
+ reader,
1666
+ originalWorker,
1667
+ );
1668
+ const restores = [];
1669
+ try {
1670
+ if (strictSecurity) {
1671
+ restores.push(installDynamicCodeConstructorIsolation());
1672
+ restores.push(installStringTimerIsolation(scope, "setInterval"));
1673
+ restores.push(installStringTimerIsolation(scope, "setTimeout"));
1674
+ if ("indexedDB" in scope) {
1675
+ restores.push(installScopeValue(
1676
+ scope,
1677
+ "indexedDB",
1678
+ undefined,
1679
+ "artifact-graph-indexeddb-isolation-unavailable",
1680
+ ));
1681
+ }
1682
+ if (scope.navigator && "storage" in scope.navigator) {
1683
+ restores.push(installObjectValue(
1684
+ scope.navigator,
1685
+ "storage",
1686
+ undefined,
1687
+ "WorkerNavigator.storage",
1688
+ "artifact-graph-opfs-isolation-unavailable",
1689
+ ));
1690
+ }
1691
+ }
1692
+ restores.push(installScopeValue(
1693
+ scope,
1694
+ GRAPH_GUARD_NAME,
1695
+ graphGuard.guard,
1696
+ "artifact-graph-guard-global-definition-rejected",
1697
+ ));
1698
+ if (strictSecurity) {
1699
+ restores.push(installScopeValue(
1700
+ scope,
1701
+ "fetch",
1702
+ async function rejectUntransformedArtifactGraphFetch() {
1703
+ throw workerError(
1704
+ "ARCANE_AI_ARTIFACT_GRAPH_FETCH_EDGE_UNDECLARED",
1705
+ "Artifact graph fetch must use its declared transformed edge.",
1706
+ undefined,
1707
+ "artifact-graph-fetch-guard-bypassed",
1708
+ );
1709
+ },
1710
+ "artifact-graph-fetch-isolation-unavailable",
1711
+ ));
1712
+ const cacheStorage = Object.freeze({
1713
+ async open() {
1714
+ throw workerError(
1715
+ "ARCANE_AI_ARTIFACT_GRAPH_CACHE_EDGE_UNDECLARED",
1716
+ "Artifact graph CacheStorage.open must use its declared transformed edge.",
1717
+ undefined,
1718
+ "artifact-graph-cache-open-guard-bypassed",
1719
+ );
1720
+ },
1721
+ async match() {
1722
+ throw workerError(
1723
+ "ARCANE_AI_ARTIFACT_GRAPH_CACHE_EDGE_UNDECLARED",
1724
+ "Artifact graph CacheStorage.match requires a declared cache-open edge.",
1725
+ undefined,
1726
+ "artifact-graph-cache-match-guard-bypassed",
1727
+ );
1728
+ },
1729
+ async has() {
1730
+ return true;
1731
+ },
1732
+ async keys() {
1733
+ return Object.freeze([]);
1734
+ },
1735
+ async delete() {
1736
+ return false;
1737
+ },
1738
+ });
1739
+ restores.push(installScopeValue(
1740
+ scope,
1741
+ "caches",
1742
+ cacheStorage,
1743
+ "artifact-graph-cache-isolation-unavailable",
1744
+ ));
1745
+
1746
+ const deniedCapabilities = [
1747
+ "BroadcastChannel",
1748
+ "EventSource",
1749
+ "Function",
1750
+ "RTCPeerConnection",
1751
+ "ShadowRealm",
1752
+ "SharedWorker",
1753
+ "WebSocket",
1754
+ "WebSocketStream",
1755
+ "WebTransport",
1756
+ "Worker",
1757
+ "XMLHttpRequest",
1758
+ "eval",
1759
+ "importScripts",
1760
+ ];
1761
+ for (const name of deniedCapabilities) {
1762
+ if (!(name in scope)) continue;
1763
+ restores.push(installScopeValue(
1764
+ scope,
1765
+ name,
1766
+ function rejectUndeclaredArtifactGraphCapability() {
1767
+ throw workerError(
1768
+ "ARCANE_AI_ARTIFACT_GRAPH_ISOLATION_UNAVAILABLE",
1769
+ `The authenticated artifact graph denied raw ${name} access.`,
1770
+ undefined,
1771
+ `artifact-graph-${name.toLowerCase()}-capability-undeclared`,
1772
+ );
1773
+ },
1774
+ `artifact-graph-${name.toLowerCase()}-isolation-unavailable`,
1775
+ ));
1776
+ }
1777
+ }
1778
+ } catch (error) {
1779
+ restoreScopeValues(restores);
1780
+ graphGuard.cleanup();
1781
+ throw error;
1782
+ }
1783
+ return Object.freeze({
1784
+ cache: graphGuard.transformersCache,
1785
+ cleanup() {
1786
+ graphGuard.cleanup();
1787
+ restoreScopeValues(restores);
1788
+ },
1789
+ });
1790
+ }
1791
+
1792
+ function assignSetting(object, name, value, cleanup, {
1793
+ allowCreate = false,
1794
+ assignmentRejectedReason,
1795
+ unavailableReason,
1796
+ } = {}) {
1797
+ if (!object) {
1798
+ throw workerError(
1799
+ "ARCANE_AI_PROVIDER_UNAVAILABLE",
1800
+ `The selected runtime does not expose the verified ${name} setting.`,
1801
+ undefined,
1802
+ unavailableReason,
1803
+ );
1804
+ }
1805
+ let hadOwn;
1806
+ let previous;
1807
+ let canRestore = false;
1808
+ try {
1809
+ if (!allowCreate && !(name in object)) {
1810
+ throw workerError(
1811
+ "ARCANE_AI_PROVIDER_UNAVAILABLE",
1812
+ `The selected runtime does not expose the verified ${name} setting.`,
1813
+ undefined,
1814
+ unavailableReason,
1815
+ );
1816
+ }
1817
+ hadOwn = Object.hasOwn(object, name);
1818
+ previous = object[name];
1819
+ canRestore = true;
1820
+ object[name] = value;
1821
+ if (object[name] !== value) {
1822
+ throw new TypeError(`The ${name} assignment was not retained.`);
1823
+ }
186
1824
  } catch (error) {
1825
+ if (isSdkWorkerError(error)) throw error;
1826
+ if (canRestore) {
1827
+ try {
1828
+ if (hadOwn) object[name] = previous;
1829
+ else delete object[name];
1830
+ } catch {
1831
+ // Preserve the exact configuration failure as the public boundary.
1832
+ }
1833
+ }
187
1834
  throw workerError(
188
1835
  "ARCANE_AI_PROVIDER_UNAVAILABLE",
189
- "The speech worker could not isolate the browser cache API.",
1836
+ `The selected runtime rejected the verified ${name} setting.`,
190
1837
  error,
1838
+ assignmentRejectedReason,
191
1839
  );
192
1840
  }
193
- return function restoreSpeechWorkerCaches() {
194
- if (ownDescriptor) Object.defineProperty(scope, "caches", ownDescriptor);
195
- else delete scope.caches;
196
- };
1841
+ cleanup.push(() => {
1842
+ if (hadOwn) object[name] = previous;
1843
+ else delete object[name];
1844
+ });
197
1845
  }
198
1846
 
199
- function configureWasmPaths(namespace, configuration) {
200
- if (namespace?.env) {
201
- namespace.env.allowLocalModels = false;
202
- namespace.env.allowRemoteModels = true;
203
- namespace.env.useBrowserCache = false;
204
- namespace.env.useFSCache = false;
205
- namespace.env.useCustomCache = false;
206
- namespace.env.customCache = null;
207
- }
208
- const wasm = namespace?.env?.backends?.onnx?.wasm;
209
- if (!wasm) return;
210
- const paths = {};
211
- for (const file of configuration.runtime.files) {
212
- if (/\.(?:m?js|wasm)$/iu.test(file.path) && file.path !== configuration.runtime.entry) {
213
- paths[file.path.split("/").pop()] = file.moduleUrl;
1847
+ function configuredWasmPaths(configuration) {
1848
+ if (configuration.runtime.wasmPaths !== undefined) {
1849
+ if (configuration.security?.secure === true) {
1850
+ throw workerError(
1851
+ "ARCANE_AI_INVALID_REQUEST",
1852
+ "Secure browser speech cannot use remote wasmPaths.",
1853
+ undefined,
1854
+ "speech-worker-secure-remote-wasm-paths-rejected",
1855
+ );
214
1856
  }
1857
+ return configuration.runtime.wasmPaths;
1858
+ }
1859
+ const byPath = new Map(configuration.runtime.files.map((file) => [file.path, file]));
1860
+ const declared = configuration.runtime.onnxWasm;
1861
+ let mjs = declared ? byPath.get(declared.mjsPath) : null;
1862
+ let wasm = declared ? byPath.get(declared.wasmPath) : null;
1863
+ if (!declared) {
1864
+ mjs = configuration.runtime.files.find((file) =>
1865
+ file.path !== configuration.runtime.entry && /\.mjs$/iu.test(file.path));
1866
+ wasm = configuration.runtime.files.find((file) => /\.wasm$/iu.test(file.path));
215
1867
  }
216
- if (Object.keys(paths).length > 0) wasm.wasmPaths = Object.freeze(paths);
217
- if (scopeIsCrossOriginIsolated() && Number.isSafeInteger(globalThis.navigator?.hardwareConcurrency)) {
218
- wasm.numThreads = Math.max(1, Math.min(8, globalThis.navigator.hardwareConcurrency));
1868
+ if (!mjs && !wasm) return null;
1869
+ if (!mjs?.moduleUrl || !wasm?.moduleUrl) {
1870
+ throw workerError(
1871
+ "ARCANE_AI_PROVIDER_UNAVAILABLE",
1872
+ "The selected runtime did not materialize its complete ONNX MJS/WASM pair.",
1873
+ undefined,
1874
+ "artifact-graph-onnx-wasm-pair-not-materialized",
1875
+ );
219
1876
  }
1877
+ return Object.freeze({ mjs: mjs.moduleUrl, wasm: wasm.moduleUrl });
220
1878
  }
221
1879
 
222
- function scopeIsCrossOriginIsolated() {
223
- return globalThis.crossOriginIsolated === true;
1880
+ function configureRuntimeNamespace(namespace, configuration, role, cache) {
1881
+ const cleanup = [];
1882
+ const strictSecurity = configuration.security?.secure === true;
1883
+ const restoreSettings = () => {
1884
+ for (const restore of cleanup.splice(0).reverse()) {
1885
+ try {
1886
+ restore();
1887
+ } catch {
1888
+ // Worker isolation is still released by terminating the owning Worker.
1889
+ }
1890
+ }
1891
+ };
1892
+ const paths = configuredWasmPaths(configuration);
1893
+ if (role === "tts") {
1894
+ const env = namespace?.env;
1895
+ if (!env || !("wasmPaths" in env)) {
1896
+ throw workerError(
1897
+ "ARCANE_AI_PROVIDER_UNAVAILABLE",
1898
+ "Kokoro does not expose the verified env.wasmPaths configuration field.",
1899
+ undefined,
1900
+ "kokoro-env-wasm-paths-unavailable",
1901
+ );
1902
+ }
1903
+ try {
1904
+ if (paths) {
1905
+ assignSetting(
1906
+ env,
1907
+ "wasmPaths",
1908
+ paths,
1909
+ cleanup,
1910
+ {
1911
+ assignmentRejectedReason: "kokoro-env-wasm-paths-assignment-rejected",
1912
+ unavailableReason: "kokoro-env-wasm-paths-unavailable",
1913
+ },
1914
+ );
1915
+ }
1916
+ } catch (error) {
1917
+ restoreSettings();
1918
+ throw error;
1919
+ }
1920
+ return restoreSettings;
1921
+ }
1922
+
1923
+ const env = namespace?.env;
1924
+ const wasm = env?.backends?.onnx?.wasm;
1925
+ if (!env || !wasm) {
1926
+ throw workerError(
1927
+ "ARCANE_AI_PROVIDER_UNAVAILABLE",
1928
+ "Transformers does not expose env.backends.onnx.wasm.",
1929
+ undefined,
1930
+ "transformers-env-backends-onnx-wasm-unavailable",
1931
+ );
1932
+ }
1933
+ try {
1934
+ assignSetting(env, "allowLocalModels", false, cleanup, {
1935
+ assignmentRejectedReason: "transformers-env-allow-local-models-assignment-rejected",
1936
+ unavailableReason: "transformers-env-allow-local-models-unavailable",
1937
+ });
1938
+ assignSetting(env, "allowRemoteModels", true, cleanup, {
1939
+ assignmentRejectedReason: "transformers-env-allow-remote-models-assignment-rejected",
1940
+ unavailableReason: "transformers-env-allow-remote-models-unavailable",
1941
+ });
1942
+ if (strictSecurity) {
1943
+ assignSetting(env, "useBrowserCache", false, cleanup, {
1944
+ assignmentRejectedReason: "transformers-env-browser-cache-assignment-rejected",
1945
+ unavailableReason: "transformers-env-browser-cache-unavailable",
1946
+ });
1947
+ assignSetting(env, "useFSCache", false, cleanup, {
1948
+ assignmentRejectedReason: "transformers-env-fs-cache-assignment-rejected",
1949
+ unavailableReason: "transformers-env-fs-cache-unavailable",
1950
+ });
1951
+ assignSetting(env, "useCustomCache", cache !== null, cleanup, {
1952
+ assignmentRejectedReason: "transformers-env-custom-cache-toggle-assignment-rejected",
1953
+ unavailableReason: "transformers-env-custom-cache-toggle-unavailable",
1954
+ });
1955
+ assignSetting(env, "customCache", cache, cleanup, {
1956
+ assignmentRejectedReason: "transformers-env-custom-cache-assignment-rejected",
1957
+ unavailableReason: "transformers-env-custom-cache-unavailable",
1958
+ });
1959
+ }
1960
+ if (paths) {
1961
+ assignSetting(
1962
+ wasm,
1963
+ "wasmPaths",
1964
+ paths,
1965
+ cleanup,
1966
+ {
1967
+ allowCreate: true,
1968
+ assignmentRejectedReason: "transformers-env-wasm-paths-assignment-rejected",
1969
+ unavailableReason: "transformers-env-wasm-paths-unavailable",
1970
+ },
1971
+ );
1972
+ }
1973
+ if (configuration.runtime.onnxWasm?.numThreads !== undefined) {
1974
+ assignSetting(
1975
+ wasm,
1976
+ "numThreads",
1977
+ configuration.runtime.onnxWasm.numThreads,
1978
+ cleanup,
1979
+ {
1980
+ allowCreate: true,
1981
+ assignmentRejectedReason: "transformers-env-num-threads-assignment-rejected",
1982
+ unavailableReason: "transformers-env-num-threads-unavailable",
1983
+ },
1984
+ );
1985
+ }
1986
+ } catch (error) {
1987
+ restoreSettings();
1988
+ throw error;
1989
+ }
1990
+ return restoreSettings;
224
1991
  }
225
1992
 
226
- function workerProgress(send, requestId, phase, completed = 0, total = null, unit = "items") {
1993
+ function workerProgress(send, role, requestId, phase, completed = 0, total = null, unit = "items") {
227
1994
  send({
228
1995
  protocol: SPEECH_WORKER_PROTOCOL,
229
1996
  event: "progress",
@@ -238,6 +2005,24 @@ function workerProgress(send, requestId, phase, completed = 0, total = null, uni
238
2005
  }, []);
239
2006
  }
240
2007
 
2008
+ function upstreamProgress(send, role, requestId) {
2009
+ return function reportSpeechModelProgress(update = {}) {
2010
+ const completed = Number.isFinite(update.loaded)
2011
+ ? update.loaded
2012
+ : Number.isFinite(update.progress) ? update.progress : 0;
2013
+ const total = Number.isFinite(update.total) ? update.total : null;
2014
+ workerProgress(
2015
+ send,
2016
+ role,
2017
+ requestId,
2018
+ `${role}-model-load-progress`,
2019
+ completed,
2020
+ total,
2021
+ Number.isFinite(update.loaded) ? "bytes" : "items",
2022
+ );
2023
+ };
2024
+ }
2025
+
241
2026
  async function disposeEngine(engine) {
242
2027
  if (!engine) return;
243
2028
  if (typeof engine.dispose === "function") {
@@ -254,26 +2039,39 @@ async function disposeEngine(engine) {
254
2039
 
255
2040
  async function createWhisperEngine(namespace, configuration, signal, report) {
256
2041
  if (typeof namespace?.pipeline !== "function") {
257
- throw workerError("ARCANE_AI_PROVIDER_UNAVAILABLE", "The Whisper runtime does not export pipeline().");
2042
+ throw workerError(
2043
+ "ARCANE_AI_PROVIDER_UNAVAILABLE",
2044
+ "The Whisper runtime does not export pipeline().",
2045
+ undefined,
2046
+ "transformers-whisper-pipeline-export-missing",
2047
+ );
258
2048
  }
259
- configureWasmPaths(namespace, configuration);
260
- throwIfAborted(signal);
2049
+ throwIfAborted(signal, "stt-load-cancelled");
261
2050
  const transcriber = await namespace.pipeline(
262
2051
  "automatic-speech-recognition",
263
2052
  configuration.model.repository,
264
2053
  {
265
2054
  device: "wasm",
266
- dtype: "fp32",
2055
+ dtype: configuration.model.dtype ?? "fp32",
267
2056
  revision: configuration.model.revision,
268
2057
  progress_callback: report,
269
2058
  },
270
2059
  );
271
- throwIfAborted(signal);
2060
+ try {
2061
+ throwIfAborted(signal, "stt-load-cancelled");
2062
+ } catch (error) {
2063
+ try {
2064
+ await disposeEngine(transcriber);
2065
+ } catch {
2066
+ // Preserve the cancellation boundary while the owning Worker terminates.
2067
+ }
2068
+ throw error;
2069
+ }
272
2070
  return Object.freeze({
273
2071
  async transcribe(input, { signal: requestSignal } = {}) {
274
- throwIfAborted(requestSignal);
2072
+ throwIfAborted(requestSignal, "stt-transcription-cancelled");
275
2073
  const output = await transcriber(input.audio, { signal: requestSignal });
276
- throwIfAborted(requestSignal);
2074
+ throwIfAborted(requestSignal, "stt-transcription-cancelled");
277
2075
  return Object.freeze({ text: String(output?.text ?? "").trim() });
278
2076
  },
279
2077
  dispose: () => disposeEngine(transcriber),
@@ -282,29 +2080,42 @@ async function createWhisperEngine(namespace, configuration, signal, report) {
282
2080
 
283
2081
  async function createKokoroEngine(namespace, configuration, signal, report) {
284
2082
  if (typeof namespace?.KokoroTTS?.from_pretrained !== "function") {
285
- throw workerError("ARCANE_AI_PROVIDER_UNAVAILABLE", "The Kokoro runtime does not export KokoroTTS.");
2083
+ throw workerError(
2084
+ "ARCANE_AI_PROVIDER_UNAVAILABLE",
2085
+ "The Kokoro runtime does not export KokoroTTS.",
2086
+ undefined,
2087
+ "kokoro-tts-constructor-export-missing",
2088
+ );
286
2089
  }
287
- configureWasmPaths(namespace, configuration);
288
- throwIfAborted(signal);
2090
+ throwIfAborted(signal, "tts-load-cancelled");
289
2091
  const synthesizer = await namespace.KokoroTTS.from_pretrained(
290
2092
  configuration.model.repository,
291
2093
  {
292
2094
  device: "wasm",
293
- dtype: "q8",
2095
+ dtype: configuration.model.dtype ?? "q8",
294
2096
  revision: configuration.model.revision,
295
2097
  progress_callback: report,
296
2098
  },
297
2099
  );
298
- throwIfAborted(signal);
2100
+ try {
2101
+ throwIfAborted(signal, "tts-load-cancelled");
2102
+ } catch (error) {
2103
+ try {
2104
+ await disposeEngine(synthesizer);
2105
+ } catch {
2106
+ // Preserve the cancellation boundary while the owning Worker terminates.
2107
+ }
2108
+ throw error;
2109
+ }
299
2110
  return Object.freeze({
300
2111
  async synthesize(input, { signal: requestSignal } = {}) {
301
- throwIfAborted(requestSignal);
2112
+ throwIfAborted(requestSignal, "tts-synthesis-cancelled");
302
2113
  const output = await synthesizer.generate(input.text, {
303
2114
  voice: input.voice,
304
2115
  speed: input.speed,
305
2116
  signal: requestSignal,
306
2117
  });
307
- throwIfAborted(requestSignal);
2118
+ throwIfAborted(requestSignal, "tts-synthesis-cancelled");
308
2119
  const audio = output?.audio instanceof Float32Array
309
2120
  ? output.audio
310
2121
  : new Float32Array(output?.audio ?? []);
@@ -318,49 +2129,109 @@ async function createKokoroEngine(namespace, configuration, signal, report) {
318
2129
  });
319
2130
  }
320
2131
 
2132
+ function callerVoiceIds(configuration) {
2133
+ return new Set((configuration.model.voices ?? []).map((voice) =>
2134
+ typeof voice === "string" ? voice : voice?.id));
2135
+ }
2136
+
321
2137
  function validateInput(role, payload, configuration) {
322
2138
  if (role === "stt") {
323
- if (!(payload?.audio instanceof Float32Array) || payload.sampleRate !== 16_000) {
2139
+ const inputSampleRate = requiredSampleRate(
2140
+ configuration.model.inputSampleRate,
2141
+ "Whisper inputSampleRate",
2142
+ 16_000,
2143
+ );
2144
+ if (!(payload?.audio instanceof Float32Array)) {
2145
+ throw workerError(
2146
+ "ARCANE_AI_INVALID_REQUEST",
2147
+ "Whisper requires Float32Array audio.",
2148
+ undefined,
2149
+ "stt-transcription-audio-not-float32array",
2150
+ );
2151
+ }
2152
+ if (payload.sampleRate !== inputSampleRate) {
324
2153
  throw workerError(
325
2154
  "ARCANE_AI_INVALID_REQUEST",
326
- "Whisper requires Float32Array audio sampled at exactly 16000 Hz.",
2155
+ `Whisper requires audio sampled at exactly ${String(inputSampleRate)} Hz.`,
2156
+ undefined,
2157
+ "stt-transcription-sample-rate-mismatch",
327
2158
  );
328
2159
  }
329
- return Object.freeze({ audio: payload.audio, sampleRate: 16_000 });
2160
+ return Object.freeze({ audio: payload.audio, sampleRate: inputSampleRate });
330
2161
  }
331
- const text = requiredText(payload?.text, "Kokoro text");
2162
+ const text = requiredText(payload?.text, "Kokoro text", "tts-synthesis-text-empty");
332
2163
  const voice = requiredText(
333
2164
  payload?.voice ?? configuration.model.defaultVoice,
334
2165
  "Kokoro voice",
2166
+ "tts-synthesis-voice-empty",
335
2167
  );
2168
+ const voices = callerVoiceIds(configuration);
2169
+ if (voices.size > 0 && !voices.has(voice)) {
2170
+ throw workerError(
2171
+ "ARCANE_AI_INVALID_REQUEST",
2172
+ "Kokoro voice is outside the caller-owned voice inventory.",
2173
+ undefined,
2174
+ "tts-synthesis-voice-not-declared",
2175
+ );
2176
+ }
336
2177
  const speed = payload?.speed ?? 1;
337
2178
  if (!Number.isFinite(speed) || speed <= 0 || speed > 4) {
338
- throw workerError("ARCANE_AI_INVALID_REQUEST", "Kokoro speed must be greater than 0 and at most 4.");
2179
+ throw workerError(
2180
+ "ARCANE_AI_INVALID_REQUEST",
2181
+ "Kokoro speed must be greater than 0 and at most 4.",
2182
+ undefined,
2183
+ "tts-synthesis-speed-out-of-range",
2184
+ );
339
2185
  }
340
2186
  return Object.freeze({ text, voice, speed });
341
2187
  }
342
2188
 
343
- function validateResult(role, result) {
2189
+ function validateResult(role, result, configuration) {
344
2190
  if (role === "stt") {
345
2191
  if (!result || typeof result.text !== "string") {
346
- throw workerError("ARCANE_AI_INVALID_PROVIDER_RESULT", "Whisper did not return text.");
2192
+ throw workerError(
2193
+ "ARCANE_AI_INVALID_PROVIDER_RESULT",
2194
+ "Whisper did not return text.",
2195
+ undefined,
2196
+ "stt-transcription-result-text-not-string",
2197
+ );
347
2198
  }
348
2199
  return Object.freeze({ text: result.text.trim() });
349
2200
  }
350
- if (!(result?.audio instanceof Float32Array) || result.sampleRate !== 24_000) {
2201
+ const outputSampleRate = requiredSampleRate(
2202
+ configuration.model.outputSampleRate,
2203
+ "Kokoro outputSampleRate",
2204
+ 24_000,
2205
+ );
2206
+ if (!(result?.audio instanceof Float32Array)) {
351
2207
  throw workerError(
352
2208
  "ARCANE_AI_INVALID_PROVIDER_RESULT",
353
- "Kokoro must return 24000 Hz Float32 PCM.",
2209
+ "Kokoro must return Float32 PCM.",
2210
+ undefined,
2211
+ "tts-synthesis-pcm-result-not-float32array",
2212
+ );
2213
+ }
2214
+ if (result.sampleRate !== outputSampleRate) {
2215
+ throw workerError(
2216
+ "ARCANE_AI_INVALID_PROVIDER_RESULT",
2217
+ `Kokoro must return ${String(outputSampleRate)} Hz PCM.`,
2218
+ undefined,
2219
+ "tts-synthesis-result-sample-rate-mismatch",
354
2220
  );
355
2221
  }
356
2222
  for (const sample of result.audio) {
357
2223
  if (!Number.isFinite(sample)) {
358
- throw workerError("ARCANE_AI_INVALID_PROVIDER_RESULT", "Kokoro returned non-finite PCM.");
2224
+ throw workerError(
2225
+ "ARCANE_AI_INVALID_PROVIDER_RESULT",
2226
+ "Kokoro returned non-finite PCM.",
2227
+ undefined,
2228
+ "tts-synthesis-pcm-sample-non-finite",
2229
+ );
359
2230
  }
360
2231
  }
361
2232
  return Object.freeze({
362
2233
  audio: result.audio,
363
- sampleRate: 24_000,
2234
+ sampleRate: outputSampleRate,
364
2235
  voice: result.voice,
365
2236
  });
366
2237
  }
@@ -374,92 +2245,232 @@ export function createSpeechWorkerRuntime({ role, scope = globalThis, send } = {
374
2245
  }
375
2246
  let configuration = null;
376
2247
  let engine = null;
377
- let restoreFetch = null;
378
- let restoreCaches = null;
2248
+ let environment = null;
2249
+ let restoreNamespace = null;
379
2250
  let disposed = false;
380
2251
  let tail = Promise.resolve();
2252
+ let lifecycleReason = `${role}-worker-created`;
381
2253
  const operations = new Map();
382
2254
 
383
2255
  function status() {
2256
+ const state = disposed ? "disposed" : engine ? "ready" : "unloaded";
2257
+ const security = configuration?.security
2258
+ ? Object.freeze({
2259
+ secure: configuration.security.secure === true,
2260
+ checks: Object.freeze({
2261
+ byteLength: configuration.security.checks?.byteLength === true,
2262
+ sha256: configuration.security.checks?.sha256 === true,
2263
+ }),
2264
+ })
2265
+ : null;
384
2266
  return Object.freeze({
385
- state: disposed ? "disposed" : engine ? "ready" : "unloaded",
2267
+ state,
2268
+ lifecycleStatus: `${role}-worker-${state}`,
2269
+ lifecycleReason,
2270
+ role,
386
2271
  loaded: engine !== null,
387
2272
  busy: operations.size > 0,
2273
+ activeOperation: operations.values().next().value?.publicOperation ?? null,
2274
+ security,
2275
+ artifactGraphId: configuration?.runtime?.artifactGraphId ?? null,
2276
+ artifactGraphAdmission: configuration?.runtime?.artifactGraphAdmission ?? null,
388
2277
  });
389
2278
  }
390
2279
 
391
2280
  async function load(request, signal) {
392
2281
  if (disposed) {
393
- throw workerError("ARCANE_AI_PROVIDER_DISPOSED", "The speech worker is disposed.");
2282
+ throw workerError(
2283
+ "ARCANE_AI_PROVIDER_DISPOSED",
2284
+ "The speech worker is disposed.",
2285
+ undefined,
2286
+ `${role}-load-rejected-after-dispose`,
2287
+ );
394
2288
  }
395
2289
  if (engine) return status();
396
- configuration = validateConfiguration(request.payload?.configuration, role);
397
- const entry = configuration.runtime.files.find((file) =>
398
- file.path === configuration.runtime.entry);
399
- restoreFetch = installAuthorizedFetch(scope, configuration);
2290
+ lifecycleReason = `${role}-load-started`;
2291
+ let loadFailureReason = `${role}-worker-runtime-configuration-rejected`;
400
2292
  try {
401
- restoreCaches = installCacheIsolation(scope);
402
- workerProgress(send, request.id, "runtime-import");
2293
+ configuration = validateConfiguration(request.payload?.configuration, role);
2294
+ const entry = configuration.runtime.files.find((file) =>
2295
+ file.path === configuration.runtime.entry);
2296
+ if (graphConfiguration(configuration) && configuration.security?.secure === true) {
2297
+ environment = installArtifactGraphEnvironment(scope, configuration, role);
2298
+ } else if (configuration.security?.secure === true) {
2299
+ const legacyFetch = installLegacyAuthorizedFetch(scope, configuration);
2300
+ try {
2301
+ const restoreCaches = installDeniedCacheIsolation(scope);
2302
+ environment = Object.freeze({
2303
+ cache: null,
2304
+ cleanup() {
2305
+ try {
2306
+ restoreCaches();
2307
+ } finally {
2308
+ legacyFetch.cleanup();
2309
+ }
2310
+ },
2311
+ });
2312
+ } catch (error) {
2313
+ legacyFetch.cleanup();
2314
+ throw error;
2315
+ }
2316
+ } else {
2317
+ environment = Object.freeze({
2318
+ cache: null,
2319
+ cleanup() {},
2320
+ });
2321
+ }
2322
+ workerProgress(send, role, request.id, `${role}-runtime-import-started`);
2323
+ loadFailureReason = `${role}-worker-runtime-import-rejected`;
403
2324
  const namespace = await import(entry.moduleUrl);
404
- throwIfAborted(signal);
405
- const report = () => workerProgress(send, request.id, "model-load");
2325
+ throwIfAborted(signal, `${role}-load-cancelled`);
2326
+ restoreNamespace = configureRuntimeNamespace(
2327
+ namespace,
2328
+ configuration,
2329
+ role,
2330
+ environment.cache,
2331
+ );
2332
+ workerProgress(send, role, request.id, `${role}-model-load-started`);
2333
+ loadFailureReason = `${role}-worker-model-load-rejected`;
2334
+ const report = upstreamProgress(send, role, request.id);
406
2335
  engine = role === "stt"
407
2336
  ? await createWhisperEngine(namespace, configuration, signal, report)
408
2337
  : await createKokoroEngine(namespace, configuration, signal, report);
409
- workerProgress(send, request.id, "ready", 1, 1);
2338
+ lifecycleReason = `${role}-load-completed`;
2339
+ workerProgress(send, role, request.id, `${role}-provider-ready`, 1, 1);
410
2340
  return status();
411
2341
  } catch (error) {
412
- restoreFetch?.();
413
- restoreFetch = null;
414
- restoreCaches?.();
415
- restoreCaches = null;
2342
+ const failure = admitWorkerFailure(
2343
+ error,
2344
+ "ARCANE_AI_PROVIDER_REQUEST_FAILED",
2345
+ "The speech Worker load operation was rejected.",
2346
+ loadFailureReason,
2347
+ );
2348
+ const rejectedEngine = engine;
2349
+ engine = null;
2350
+ try {
2351
+ await disposeEngine(rejectedEngine);
2352
+ } catch {
2353
+ // Preserve the load boundary error that caused teardown.
2354
+ }
2355
+ restoreNamespace?.();
2356
+ restoreNamespace = null;
2357
+ try {
2358
+ environment?.cleanup();
2359
+ } catch {
2360
+ // A rejected load is followed by provider-owned Worker termination.
2361
+ }
2362
+ environment = null;
416
2363
  configuration = null;
417
- throw error;
2364
+ lifecycleReason = failure.reason;
2365
+ throw failure;
418
2366
  }
419
2367
  }
420
2368
 
421
2369
  async function use(request, signal) {
422
2370
  if (!engine || !configuration) {
423
- throw workerError("ARCANE_AI_NOT_READY", "The speech worker is not loaded.");
2371
+ throw workerError(
2372
+ "ARCANE_AI_NOT_READY",
2373
+ "The speech worker is not loaded.",
2374
+ undefined,
2375
+ role === "stt"
2376
+ ? "stt-transcription-rejected-before-load"
2377
+ : "tts-synthesis-rejected-before-load",
2378
+ );
424
2379
  }
425
2380
  const input = validateInput(role, request.payload, configuration);
426
2381
  const method = role === "stt" ? engine.transcribe : engine.synthesize;
427
2382
  if (typeof method !== "function") {
428
- throw workerError("ARCANE_AI_PROVIDER_UNAVAILABLE", "The speech engine operation is unavailable.");
2383
+ throw workerError(
2384
+ "ARCANE_AI_PROVIDER_UNAVAILABLE",
2385
+ "The speech engine operation is unavailable.",
2386
+ undefined,
2387
+ role === "stt"
2388
+ ? "stt-transcription-method-unavailable"
2389
+ : "tts-synthesis-method-unavailable",
2390
+ );
2391
+ }
2392
+ lifecycleReason = role === "stt"
2393
+ ? "stt-transcription-started"
2394
+ : "tts-synthesis-started";
2395
+ try {
2396
+ const result = validateResult(
2397
+ role,
2398
+ await method(input, { signal }),
2399
+ configuration,
2400
+ );
2401
+ lifecycleReason = role === "stt"
2402
+ ? "stt-transcription-completed"
2403
+ : "tts-synthesis-completed";
2404
+ return result;
2405
+ } catch (error) {
2406
+ const failure = admitWorkerFailure(
2407
+ error,
2408
+ "ARCANE_AI_PROVIDER_REQUEST_FAILED",
2409
+ "The speech engine operation was rejected.",
2410
+ role === "stt"
2411
+ ? "stt-transcription-engine-operation-rejected"
2412
+ : "tts-synthesis-engine-operation-rejected",
2413
+ );
2414
+ lifecycleReason = failure.reason;
2415
+ throw failure;
429
2416
  }
430
- return validateResult(role, await method(input, { signal }));
431
2417
  }
432
2418
 
433
- async function unload() {
434
- for (const controller of operations.values()) controller.abort();
2419
+ async function unload(reason = `${role}-unload-completed`) {
2420
+ for (const operation of operations.values()) {
2421
+ if (operation.op === "unload" || operation.op === "dispose") continue;
2422
+ operation.controller.abort(operationReason(role, operation.op, "superseded-by-unload"));
2423
+ }
435
2424
  const current = engine;
436
2425
  engine = null;
437
- configuration = null;
2426
+ let disposeFailure = null;
438
2427
  try {
439
2428
  await disposeEngine(current);
2429
+ } catch (error) {
2430
+ const failure = admitWorkerFailure(
2431
+ error,
2432
+ "ARCANE_AI_PROVIDER_REQUEST_FAILED",
2433
+ "The speech engine dispose operation was rejected.",
2434
+ `${role}-worker-engine-dispose-rejected`,
2435
+ );
2436
+ disposeFailure = failure;
2437
+ lifecycleReason = failure.reason;
2438
+ throw failure;
440
2439
  } finally {
441
- restoreFetch?.();
442
- restoreFetch = null;
443
- restoreCaches?.();
444
- restoreCaches = null;
2440
+ restoreNamespace?.();
2441
+ restoreNamespace = null;
2442
+ try {
2443
+ environment?.cleanup();
2444
+ } catch {
2445
+ // Worker termination remains the final cleanup boundary.
2446
+ }
2447
+ environment = null;
2448
+ configuration = null;
2449
+ if (!disposeFailure) lifecycleReason = reason;
445
2450
  }
446
2451
  return status();
447
2452
  }
448
2453
 
449
- async function dispatch(request, signal) {
450
- if (request.op === "load") return load(request, signal);
451
- if (request.op === "use") return use(request, signal);
452
- if (request.op === "status") return status();
453
- if (request.op === "unload") return unload();
454
- if (request.op === "dispose") {
455
- await unload();
2454
+ async function dispatch(request, signal, op) {
2455
+ if (op === "load") return load(request, signal);
2456
+ if (op === "use") return use(request, signal);
2457
+ if (op === "status") return status();
2458
+ if (op === "unload") return unload(`${role}-unload-completed`);
2459
+ if (op === "dispose") {
2460
+ await unload(`${role}-dispose-unloaded-worker`);
456
2461
  disposed = true;
2462
+ lifecycleReason = `${role}-dispose-completed`;
457
2463
  return status();
458
2464
  }
459
- throw workerError("ARCANE_AI_INVALID_REQUEST", "The speech worker operation is unsupported.");
2465
+ throw workerError(
2466
+ "ARCANE_AI_INVALID_REQUEST",
2467
+ "The speech worker operation is not part of its protocol.",
2468
+ undefined,
2469
+ `${role}-worker-operation-unknown`,
2470
+ );
460
2471
  }
461
2472
 
462
- function respond(request, operation) {
2473
+ function respond(request, operation, op) {
463
2474
  operation.then((result) => send({
464
2475
  protocol: SPEECH_WORKER_PROTOCOL,
465
2476
  id: request.id,
@@ -469,7 +2480,7 @@ export function createSpeechWorkerRuntime({ role, scope = globalThis, send } = {
469
2480
  protocol: SPEECH_WORKER_PROTOCOL,
470
2481
  id: request.id,
471
2482
  ok: false,
472
- error: serializedError(error),
2483
+ error: serializedError(error, role, op),
473
2484
  }, []));
474
2485
  }
475
2486
 
@@ -477,40 +2488,172 @@ export function createSpeechWorkerRuntime({ role, scope = globalThis, send } = {
477
2488
  if (request?.protocol !== SPEECH_WORKER_PROTOCOL
478
2489
  || !Number.isSafeInteger(request.id)
479
2490
  || request.id < 1) {
480
- return Promise.reject(workerError("ARCANE_AI_INVALID_REQUEST", "The speech worker envelope is invalid."));
2491
+ return Promise.reject(workerError(
2492
+ "ARCANE_AI_INVALID_REQUEST",
2493
+ "The speech worker envelope shape was rejected.",
2494
+ undefined,
2495
+ `${role}-worker-message-envelope-shape-rejected`,
2496
+ ));
2497
+ }
2498
+ const op = request.op;
2499
+ if (!TRANSPORT_WORKER_OPERATION_SET.has(op)) {
2500
+ return Promise.reject(workerError(
2501
+ "ARCANE_AI_INVALID_REQUEST",
2502
+ "The speech worker operation is not part of its protocol.",
2503
+ undefined,
2504
+ `${role}-worker-operation-unknown`,
2505
+ ));
481
2506
  }
482
- if (request.op === "cancel") {
2507
+ if (op === "cancel") {
483
2508
  const target = operations.get(request.payload?.targetId);
484
- target?.abort(request.payload?.reason);
485
- const result = Promise.resolve(Object.freeze({ cancelled: Boolean(target) }));
486
- respond(request, result);
2509
+ target?.controller.abort(operationReason(role, target.op, "cancelled"));
2510
+ const result = Promise.resolve(Object.freeze({
2511
+ cancelled: Boolean(target),
2512
+ reason: target
2513
+ ? operationReason(role, target.op, "cancelled")
2514
+ : `${role}-cancel-target-not-active`,
2515
+ }));
2516
+ respond(request, result, op);
487
2517
  return result;
488
2518
  }
489
2519
  const controller = new AbortController();
2520
+ const publicOperation = op === "use"
2521
+ ? role === "stt" ? "stt-transcription" : "tts-synthesis"
2522
+ : `${role}-${op}`;
2523
+ const record = { controller, op, publicOperation };
490
2524
  const execute = tail.catch(() => undefined).then(() => {
491
- throwIfAborted(controller.signal);
492
- return dispatch(request, controller.signal);
2525
+ throwIfAborted(controller.signal, operationReason(role, op, "cancelled"));
2526
+ return dispatch(request, controller.signal, op);
493
2527
  });
494
- operations.set(request.id, controller);
2528
+ operations.set(request.id, record);
495
2529
  const operation = execute.finally(() => operations.delete(request.id));
496
2530
  tail = operation.catch(() => undefined);
497
- respond(request, operation);
2531
+ respond(request, operation, op);
498
2532
  return operation;
499
2533
  }
500
2534
 
501
2535
  return Object.freeze({ handleMessage, status });
502
2536
  }
503
2537
 
2538
+ function privatePort(value) {
2539
+ return value
2540
+ && typeof value.postMessage === "function"
2541
+ && typeof value.addEventListener === "function"
2542
+ ? value
2543
+ : null;
2544
+ }
2545
+
504
2546
  export function installBrowserSpeechWorker(role, scope = globalThis) {
2547
+ let transport = scope;
2548
+ let transportMode = null;
505
2549
  const runtime = createSpeechWorkerRuntime({
506
2550
  role,
507
2551
  scope,
508
- send: (message, transfers) => scope.postMessage(message, transfers),
2552
+ send: (message, transfers) => transport.postMessage(message, transfers),
509
2553
  });
2554
+
2555
+ function receive(request) {
2556
+ void runtime.handleMessage(request).catch(() => undefined);
2557
+ }
2558
+
510
2559
  scope.addEventListener("message", (event) => {
511
- // Valid envelopes own their one response inside handleMessage(). Invalid
512
- // envelopes have no trustworthy request id and are intentionally ignored.
513
- void runtime.handleMessage(event.data).catch(() => undefined);
2560
+ if (transportMode === "private-message-port") return;
2561
+ const requestedPort = privatePort(event.data?.privatePort);
2562
+ const isGraphLoad = event.data?.op === "load"
2563
+ && graphConfiguration(event.data?.payload?.configuration);
2564
+ if (requestedPort) {
2565
+ if (!isGraphLoad || transportMode !== null) return;
2566
+ transportMode = "private-message-port";
2567
+ transport = requestedPort;
2568
+ requestedPort.addEventListener("message", (portEvent) => receive(portEvent.data));
2569
+ requestedPort.start?.();
2570
+ const { privatePort: ignored, ...request } = event.data;
2571
+ void ignored;
2572
+ receive(request);
2573
+ return;
2574
+ }
2575
+ if (isGraphLoad) {
2576
+ scope.postMessage({
2577
+ protocol: SPEECH_WORKER_PROTOCOL,
2578
+ id: event.data.id,
2579
+ ok: false,
2580
+ error: serializedError(workerError(
2581
+ "ARCANE_AI_ARTIFACT_GRAPH_ISOLATION_UNAVAILABLE",
2582
+ "Authenticated artifact graph loading requires a private MessagePort.",
2583
+ undefined,
2584
+ "artifact-graph-private-message-port-missing",
2585
+ ), role, "load"),
2586
+ });
2587
+ return;
2588
+ }
2589
+ transportMode ??= "worker-global-message";
2590
+ receive(event.data);
514
2591
  });
515
2592
  return runtime;
516
2593
  }
2594
+
2595
+ function replayWorkerMessage(scope, event) {
2596
+ if (typeof scope.dispatchEvent === "function") {
2597
+ scope.dispatchEvent(event);
2598
+ return;
2599
+ }
2600
+ scope.onmessage?.(event);
2601
+ }
2602
+
2603
+ export function installBrowserSpeechArtifactModuleWorker(role, scope = globalThis) {
2604
+ if (role !== "stt" && role !== "tts") {
2605
+ throw new TypeError('Speech artifact module Worker role must be "stt" or "tts".');
2606
+ }
2607
+ let initializing = false;
2608
+ const queued = [];
2609
+ const bootstrap = (event) => {
2610
+ if (initializing) {
2611
+ queued.push(event);
2612
+ return;
2613
+ }
2614
+ const request = event.data;
2615
+ if (request?.protocol !== NESTED_WORKER_PROTOCOL
2616
+ || request.op !== "initialize-authenticated-artifact-module-worker"
2617
+ || request.role !== role) return;
2618
+ initializing = true;
2619
+ void (async () => {
2620
+ let environment = null;
2621
+ try {
2622
+ const configuration = validateConfiguration(request.configuration, role);
2623
+ const target = configuration.runtime.files.find((file) =>
2624
+ file.path === request.targetPath);
2625
+ if (!target?.moduleUrl) {
2626
+ throw workerError(
2627
+ "ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
2628
+ "The nested module Worker target was not materialized.",
2629
+ undefined,
2630
+ "artifact-graph-module-worker-target-not-materialized",
2631
+ );
2632
+ }
2633
+ environment = installArtifactGraphEnvironment(scope, configuration, role);
2634
+ await import(target.moduleUrl);
2635
+ scope.removeEventListener("message", bootstrap);
2636
+ await new Promise((resolve) => queueMicrotask(resolve));
2637
+ for (const queuedEvent of queued.splice(0)) replayWorkerMessage(scope, queuedEvent);
2638
+ } catch (error) {
2639
+ try {
2640
+ environment?.cleanup();
2641
+ } catch {
2642
+ // Preserve the exact nested Worker bootstrap failure.
2643
+ }
2644
+ scope.postMessage({
2645
+ protocol: NESTED_WORKER_PROTOCOL,
2646
+ event: "artifact-module-worker-bootstrap-rejected",
2647
+ error: serializedError(error, role, "load"),
2648
+ });
2649
+ scope.close?.();
2650
+ }
2651
+ })();
2652
+ };
2653
+ scope.addEventListener("message", bootstrap);
2654
+ return Object.freeze({
2655
+ protocol: NESTED_WORKER_PROTOCOL,
2656
+ role,
2657
+ lifecycleStatus: `${role}-artifact-module-worker-awaiting-initialization`,
2658
+ });
2659
+ }