arcane-os 0.2.3 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/CHANGELOG.md +30 -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 +3242 -391
  7. package/browser-runtime/ai/browser-speech-providers.mjs +1209 -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 +249 -32
  12. package/browser-runtime/ai/speech-worker-runtime.mjs +2322 -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 +18 -20
  17. package/docs/reference/ai/browser-speech-package-authority.json +835 -0
  18. package/docs/reference/ai/browser-speech.md +1205 -246
  19. package/docs/reference/ai/browser-wasm.md +18 -7
  20. package/docs/reference/availability-and-normalization.md +2 -2
  21. package/docs/reference/behavioral-testing.md +28 -5
  22. package/docs/reference/cli.md +115 -7
  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 +86 -37
  27. package/docs/reference/inventory/runtime-modules.json +126 -49
  28. package/docs/reference/mail.md +316 -0
  29. package/docs/reference/protocols.md +185 -33
  30. package/docs/reference/runtime-components.md +192 -80
  31. package/docs/reference/runtime-modules.md +589 -70
  32. package/docs/reference/sdk-api.md +1017 -24
  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 +453 -131
  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 +244 -72
  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 +2117 -130
  75. package/runtime/arcane/modules/AIProviderRuntime.js +756 -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 +235 -37
  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 +130 -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-runtime.mjs +804 -22
  118. package/src/workspace.mjs +1 -1
@@ -1,9 +1,21 @@
1
1
  import {
2
2
  collectSpeechTransferables,
3
+ normalizeSpeechWorkerErrorEnvelope,
3
4
  SPEECH_WORKER_PROTOCOL,
4
5
  } from "./speech-worker-runtime.mjs";
5
6
 
6
- function clientError(code, message, cause) {
7
+ const ARTIFACT_GRAPH_MODULE_GRAPH =
8
+ "browser-speech-authenticated-artifact-graph";
9
+ const PUBLIC_WORKER_OPERATIONS = new Set([
10
+ "load",
11
+ "use",
12
+ "status",
13
+ "unload",
14
+ "dispose",
15
+ ]);
16
+ const WORKER_CLIENT_ERRORS = new WeakSet();
17
+
18
+ function clientError(code, message, cause, reason) {
7
19
  const error = cause === undefined
8
20
  ? new Error(message)
9
21
  : new Error(message, { cause });
@@ -11,17 +23,43 @@ function clientError(code, message, cause) {
11
23
  ? "AbortError"
12
24
  : "ArcaneSpeechWorkerError";
13
25
  error.code = code;
26
+ if (typeof reason === "string" && reason) error.reason = reason;
27
+ WORKER_CLIENT_ERRORS.add(error);
14
28
  return error;
15
29
  }
16
30
 
17
- function abortError(signal) {
31
+ function operationSubject(role, op) {
32
+ if (op === "use") return role === "stt" ? "stt-transcription" : "tts-synthesis";
33
+ return `${role}-${op}`;
34
+ }
35
+
36
+ function abortError(signal, role, op) {
18
37
  return clientError(
19
38
  "ARCANE_AI_REQUEST_ABORTED",
20
39
  "The speech worker operation was cancelled.",
21
40
  signal?.reason,
41
+ `${operationSubject(role, op)}-cancelled`,
22
42
  );
23
43
  }
24
44
 
45
+ function validProgress(role, progress) {
46
+ if (!progress || typeof progress !== "object" || Array.isArray(progress)) return false;
47
+ const keys = Object.keys(progress).sort().join(",");
48
+ if (keys !== "completed,heartbeat,phase,total,unit") return false;
49
+ const phases = new Set([
50
+ `${role}-runtime-import-started`,
51
+ `${role}-model-load-started`,
52
+ `${role}-model-load-progress`,
53
+ `${role}-provider-ready`,
54
+ ]);
55
+ return phases.has(progress.phase)
56
+ && Number.isFinite(progress.completed)
57
+ && progress.completed >= 0
58
+ && (progress.total === null || (Number.isFinite(progress.total) && progress.total >= 0))
59
+ && (progress.unit === "bytes" || progress.unit === "items")
60
+ && progress.heartbeat === true;
61
+ }
62
+
25
63
  function validateWorker(worker) {
26
64
  if (!worker || typeof worker.postMessage !== "function" || typeof worker.terminate !== "function") {
27
65
  throw new TypeError("The packaged speech worker did not create a Worker.");
@@ -29,6 +67,23 @@ function validateWorker(worker) {
29
67
  return worker;
30
68
  }
31
69
 
70
+ function validateMessagePort(port) {
71
+ if (
72
+ !port
73
+ || typeof port.postMessage !== "function"
74
+ || typeof port.addEventListener !== "function"
75
+ || typeof port.close !== "function"
76
+ ) {
77
+ throw clientError(
78
+ "ARCANE_AI_WORKER_MESSAGE_ERROR",
79
+ "The browser did not create a usable private speech Worker MessagePort.",
80
+ undefined,
81
+ "artifact-graph-private-message-port-unavailable",
82
+ );
83
+ }
84
+ return port;
85
+ }
86
+
32
87
  const WORKER_CLIENTS = new WeakSet();
33
88
 
34
89
  /**
@@ -36,8 +91,12 @@ const WORKER_CLIENTS = new WeakSet();
36
91
  * the only reliable preemption boundary for the third-party WASM engines.
37
92
  */
38
93
  class SpeechWorkerClient {
94
+ #role;
39
95
  #createWorker;
40
96
  #worker = null;
97
+ #transport = null;
98
+ #transportMode = null;
99
+ #privatePorts = null;
41
100
  #pending = new Map();
42
101
  #nextId = 1;
43
102
  #listeners = [];
@@ -54,6 +113,7 @@ class SpeechWorkerClient {
54
113
  const workerUrl = role === "stt"
55
114
  ? new URL("./browser-whisper-worker.mjs", import.meta.url)
56
115
  : new URL("./browser-kokoro-worker.mjs", import.meta.url);
116
+ this.#role = role;
57
117
  this.#createWorker = () => new Worker(workerUrl, {
58
118
  type: "module",
59
119
  name: role === "stt" ? "arcane-whisper-stt" : "arcane-kokoro-tts",
@@ -62,46 +122,125 @@ class SpeechWorkerClient {
62
122
  WORKER_CLIENTS.add(this);
63
123
  }
64
124
 
65
- #listen(worker, type, listener) {
66
- worker.addEventListener(type, listener);
67
- this.#listeners.push(() => worker.removeEventListener(type, listener));
125
+ #listen(target, type, listener) {
126
+ target.addEventListener(type, listener);
127
+ this.#listeners.push(() => target.removeEventListener(type, listener));
68
128
  }
69
129
 
70
130
  #start() {
71
131
  if (this.#worker) return this.#worker;
72
132
  if (this.#terminated) {
73
- throw clientError("ARCANE_AI_OPERATION_SUPERSEDED", "The speech Worker was already terminated.");
133
+ throw clientError(
134
+ "ARCANE_AI_OPERATION_SUPERSEDED",
135
+ "The speech Worker was already terminated.",
136
+ undefined,
137
+ `${this.#role}-worker-already-terminated`,
138
+ );
74
139
  }
75
140
  const worker = validateWorker(this.#createWorker());
76
141
  this.#worker = worker;
77
- this.#listen(worker, "message", (event) => this.#handleMessage(event.data));
142
+ this.#listen(worker, "message", (event) => {
143
+ if (this.#transportMode === "private-message-port") return;
144
+ this.#handleMessage(event.data);
145
+ });
78
146
  this.#listen(worker, "messageerror", (event) => {
79
147
  void this.terminate(clientError(
80
148
  "ARCANE_AI_WORKER_MESSAGE_ERROR",
81
149
  "The speech Worker returned an unreadable message.",
82
150
  event,
83
- ), { intentional: false });
151
+ `${this.#role}-worker-message-rejected`,
152
+ ), { intentional: false }).catch(() => undefined);
84
153
  });
85
154
  this.#listen(worker, "error", (event) => {
86
155
  void this.terminate(clientError(
87
156
  "ARCANE_AI_WORKER_CRASHED",
88
157
  "The speech Worker crashed.",
89
158
  event,
90
- ), { intentional: false });
159
+ `${this.#role}-worker-crashed`,
160
+ ), { intentional: false }).catch(() => undefined);
91
161
  });
92
162
  return worker;
93
163
  }
94
164
 
165
+ #establishPrivateTransport() {
166
+ if (this.#transportMode === "private-message-port") return null;
167
+ if (this.#transportMode !== null) {
168
+ throw clientError(
169
+ "ARCANE_AI_ADAPTER_PROTOCOL_MISMATCH",
170
+ "The speech Worker already uses the legacy global message transport.",
171
+ undefined,
172
+ "artifact-graph-private-message-port-established-too-late",
173
+ );
174
+ }
175
+ const MessageChannelConstructor = globalThis.MessageChannel;
176
+ if (typeof MessageChannelConstructor !== "function") {
177
+ throw clientError(
178
+ "ARCANE_AI_WORKER_MESSAGE_ERROR",
179
+ "Strict artifact graph loading requires MessageChannel.",
180
+ undefined,
181
+ "artifact-graph-private-message-channel-unavailable",
182
+ );
183
+ }
184
+ const channel = new MessageChannelConstructor();
185
+ const clientPort = validateMessagePort(channel.port1);
186
+ const workerPort = validateMessagePort(channel.port2);
187
+ this.#privatePorts = Object.freeze({ clientPort, workerPort });
188
+ this.#transport = clientPort;
189
+ this.#transportMode = "private-message-port";
190
+ this.#listen(clientPort, "message", (event) => this.#handleMessage(event.data));
191
+ this.#listen(clientPort, "messageerror", (event) => {
192
+ void this.terminate(clientError(
193
+ "ARCANE_AI_WORKER_MESSAGE_ERROR",
194
+ "The private speech Worker MessagePort returned an unreadable message.",
195
+ event,
196
+ `${this.#role}-worker-private-message-rejected`,
197
+ ), { intentional: false }).catch(() => undefined);
198
+ });
199
+ clientPort.start?.();
200
+ return workerPort;
201
+ }
202
+
95
203
  #handleMessage(message) {
96
204
  if (message?.protocol !== SPEECH_WORKER_PROTOCOL) {
97
205
  void this.terminate(clientError(
98
206
  "ARCANE_AI_ADAPTER_PROTOCOL_MISMATCH",
99
207
  "The speech Worker protocol did not match the SDK.",
100
- ), { intentional: false });
208
+ undefined,
209
+ `${this.#role}-worker-protocol-mismatch`,
210
+ ), { intentional: false }).catch(() => undefined);
101
211
  return;
102
212
  }
103
213
  if (message.event === "progress") {
104
- this.#pending.get(message.requestId)?.progress(message.progress);
214
+ const pending = this.#pending.get(message.requestId);
215
+ if (!pending) return;
216
+ if (!validProgress(this.#role, message.progress)) {
217
+ void this.terminate(clientError(
218
+ "ARCANE_AI_WORKER_MESSAGE_ERROR",
219
+ "The speech Worker progress envelope was rejected.",
220
+ undefined,
221
+ `${this.#role}-worker-progress-envelope-rejected`,
222
+ ), { intentional: false }).catch(() => undefined);
223
+ return;
224
+ }
225
+ try {
226
+ pending.progress(message.progress);
227
+ } catch (error) {
228
+ void this.terminate(clientError(
229
+ "ARCANE_AI_PROGRESS_CALLBACK_THREW",
230
+ "The speech load progress callback threw an exception.",
231
+ error,
232
+ `${this.#role}-load-progress-callback-threw`,
233
+ ), { intentional: false }).catch(() => undefined);
234
+ }
235
+ return;
236
+ }
237
+ if (!Number.isSafeInteger(message.id) || typeof message.ok !== "boolean") {
238
+ void this.terminate(clientError(
239
+ "ARCANE_AI_WORKER_MESSAGE_ERROR",
240
+ "The speech Worker response envelope shape was rejected.",
241
+ undefined,
242
+ `${this.#role}-worker-response-envelope-shape-rejected`,
243
+ ), { intentional: false }).catch(() => undefined);
105
244
  return;
106
245
  }
107
246
  const pending = this.#pending.get(message.id);
@@ -112,20 +251,67 @@ class SpeechWorkerClient {
112
251
  pending.resolve(message.result);
113
252
  return;
114
253
  }
254
+ const admitted = normalizeSpeechWorkerErrorEnvelope(
255
+ message.error,
256
+ this.#role,
257
+ pending.op,
258
+ );
259
+ if (!admitted) {
260
+ const failure = clientError(
261
+ "ARCANE_AI_WORKER_MESSAGE_ERROR",
262
+ "The speech Worker error envelope was rejected.",
263
+ undefined,
264
+ `${this.#role}-worker-error-envelope-rejected`,
265
+ );
266
+ pending.reject(failure);
267
+ void this.terminate(failure, { intentional: false }).catch(() => undefined);
268
+ return;
269
+ }
115
270
  pending.reject(clientError(
116
- message.error?.code ?? "ARCANE_AI_PROVIDER_REQUEST_FAILED",
117
- message.error?.message ?? "The speech Worker operation failed.",
271
+ admitted.code,
272
+ admitted.message,
273
+ undefined,
274
+ admitted.reason,
118
275
  ));
119
276
  }
120
277
 
121
278
  request(op, payload, { signal = null, progress = () => undefined } = {}) {
122
- if (signal?.aborted) return Promise.reject(abortError(signal));
279
+ if (!PUBLIC_WORKER_OPERATIONS.has(op)) {
280
+ return Promise.reject(clientError(
281
+ "ARCANE_AI_INVALID_REQUEST",
282
+ "The speech worker operation is not part of its protocol.",
283
+ undefined,
284
+ `${this.#role}-worker-operation-unknown`,
285
+ ));
286
+ }
287
+ if (signal?.aborted) return Promise.reject(abortError(signal, this.#role, op));
123
288
  if (typeof progress !== "function") {
124
289
  return Promise.reject(new TypeError("Speech Worker progress must be a function."));
125
290
  }
126
291
  let worker;
292
+ let initialPrivatePort = null;
127
293
  try {
128
294
  worker = this.#start();
295
+ const graphLoad = op === "load"
296
+ && payload?.configuration?.runtime?.moduleGraph === ARTIFACT_GRAPH_MODULE_GRAPH;
297
+ const strictGraphLoad = graphLoad
298
+ && payload?.configuration?.security?.secure === true;
299
+ if (strictGraphLoad) {
300
+ initialPrivatePort = this.#establishPrivateTransport();
301
+ } else if (this.#transportMode === null) {
302
+ this.#transportMode = "worker-global-message";
303
+ this.#transport = worker;
304
+ }
305
+ if (op === "load"
306
+ && !strictGraphLoad
307
+ && this.#transportMode === "private-message-port") {
308
+ throw clientError(
309
+ "ARCANE_AI_ADAPTER_PROTOCOL_MISMATCH",
310
+ "A private strict artifact graph Worker cannot load an ordinary runtime descriptor.",
311
+ undefined,
312
+ `${this.#role}-worker-runtime-transport-mode-mismatch`,
313
+ );
314
+ }
129
315
  } catch (error) {
130
316
  return Promise.reject(error);
131
317
  }
@@ -140,17 +326,28 @@ class SpeechWorkerClient {
140
326
  };
141
327
  const onAbort = () => {
142
328
  if (!this.#pending.has(id)) return;
143
- void this.terminate(abortError(signal), { intentional: true });
329
+ void this.terminate(
330
+ abortError(signal, this.#role, op),
331
+ { intentional: true },
332
+ ).catch(() => undefined);
144
333
  };
145
334
  signal?.addEventListener?.("abort", onAbort, { once: true });
146
- this.#pending.set(id, { resolve, reject, progress, cleanup });
335
+ this.#pending.set(id, { resolve, reject, progress, cleanup, op });
336
+ const message = {
337
+ protocol: SPEECH_WORKER_PROTOCOL,
338
+ id,
339
+ op,
340
+ payload,
341
+ };
342
+ const transfers = collectSpeechTransferables(payload);
343
+ let destination = this.#transport;
344
+ if (initialPrivatePort) {
345
+ message.privatePort = initialPrivatePort;
346
+ transfers.push(initialPrivatePort);
347
+ destination = worker;
348
+ }
147
349
  try {
148
- worker.postMessage({
149
- protocol: SPEECH_WORKER_PROTOCOL,
150
- id,
151
- op,
152
- payload,
153
- }, collectSpeechTransferables(payload));
350
+ destination.postMessage(message, transfers);
154
351
  } catch (error) {
155
352
  this.#pending.delete(id);
156
353
  cleanup();
@@ -158,21 +355,27 @@ class SpeechWorkerClient {
158
355
  "ARCANE_AI_WORKER_MESSAGE_ERROR",
159
356
  "Unable to send an operation to the speech Worker.",
160
357
  error,
358
+ `${operationSubject(this.#role, op)}-message-rejected`,
161
359
  );
162
360
  reject(failure);
163
- void this.terminate(failure, { intentional: false });
361
+ void this.terminate(failure, { intentional: false }).catch(() => undefined);
164
362
  }
165
363
  });
166
364
  }
167
365
 
168
- async terminate(reason = clientError(
169
- "ARCANE_AI_OPERATION_SUPERSEDED",
170
- "The speech Worker was terminated.",
171
- ), { intentional = true } = {}) {
366
+ async terminate(reason = null, { intentional = true } = {}) {
172
367
  if (typeof intentional !== "boolean") {
173
368
  throw new TypeError("Speech Worker termination intent must be a boolean.");
174
369
  }
175
370
  if (this.#terminated) return;
371
+ const terminationReason = reason instanceof Error && WORKER_CLIENT_ERRORS.has(reason)
372
+ ? reason
373
+ : clientError(
374
+ "ARCANE_AI_OPERATION_SUPERSEDED",
375
+ "The speech Worker was terminated.",
376
+ reason instanceof Error ? reason : undefined,
377
+ `${this.#role}-worker-terminated`,
378
+ );
176
379
  this.#terminated = true;
177
380
  const worker = this.#worker;
178
381
  this.#worker = null;
@@ -184,16 +387,26 @@ class SpeechWorkerClient {
184
387
  }
185
388
  }
186
389
  const pendingOperations = [...this.#pending.values()];
187
- for (const pending of pendingOperations) {
188
- pending.cleanup();
189
- }
390
+ for (const pending of pendingOperations) pending.cleanup();
190
391
  this.#pending.clear();
191
392
  try {
393
+ for (const port of [
394
+ this.#privatePorts?.clientPort,
395
+ this.#privatePorts?.workerPort,
396
+ ]) {
397
+ try {
398
+ port?.close();
399
+ } catch {
400
+ // Worker termination remains the authoritative cancellation boundary.
401
+ }
402
+ }
403
+ this.#privatePorts = null;
404
+ this.#transport = null;
192
405
  const termination = worker?.terminate();
193
406
  if (termination && typeof termination.then === "function") await termination;
194
407
  } finally {
195
- for (const pending of pendingOperations) pending.reject(reason);
196
- this.#onTermination(Object.freeze({ reason, intentional }));
408
+ for (const pending of pendingOperations) pending.reject(terminationReason);
409
+ this.#onTermination(Object.freeze({ reason: terminationReason, intentional }));
197
410
  }
198
411
  }
199
412
  }
@@ -205,3 +418,7 @@ export function createSpeechWorkerClient(options) {
205
418
  export function isSpeechWorkerClient(value) {
206
419
  return WORKER_CLIENTS.has(value);
207
420
  }
421
+
422
+ export function isSpeechWorkerClientError(value) {
423
+ return value instanceof Error && WORKER_CLIENT_ERRORS.has(value);
424
+ }