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,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
+ "Authenticated 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,65 @@ 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
+ if (graphLoad) {
298
+ initialPrivatePort = this.#establishPrivateTransport();
299
+ } else if (this.#transportMode === null) {
300
+ this.#transportMode = "worker-global-message";
301
+ this.#transport = worker;
302
+ }
303
+ if (op === "load"
304
+ && !graphLoad
305
+ && this.#transportMode === "private-message-port") {
306
+ throw clientError(
307
+ "ARCANE_AI_ADAPTER_PROTOCOL_MISMATCH",
308
+ "A private artifact graph Worker cannot load a legacy runtime descriptor.",
309
+ undefined,
310
+ `${this.#role}-worker-runtime-transport-mode-mismatch`,
311
+ );
312
+ }
129
313
  } catch (error) {
130
314
  return Promise.reject(error);
131
315
  }
@@ -140,17 +324,28 @@ class SpeechWorkerClient {
140
324
  };
141
325
  const onAbort = () => {
142
326
  if (!this.#pending.has(id)) return;
143
- void this.terminate(abortError(signal), { intentional: true });
327
+ void this.terminate(
328
+ abortError(signal, this.#role, op),
329
+ { intentional: true },
330
+ ).catch(() => undefined);
144
331
  };
145
332
  signal?.addEventListener?.("abort", onAbort, { once: true });
146
- this.#pending.set(id, { resolve, reject, progress, cleanup });
333
+ this.#pending.set(id, { resolve, reject, progress, cleanup, op });
334
+ const message = {
335
+ protocol: SPEECH_WORKER_PROTOCOL,
336
+ id,
337
+ op,
338
+ payload,
339
+ };
340
+ const transfers = collectSpeechTransferables(payload);
341
+ let destination = this.#transport;
342
+ if (initialPrivatePort) {
343
+ message.privatePort = initialPrivatePort;
344
+ transfers.push(initialPrivatePort);
345
+ destination = worker;
346
+ }
147
347
  try {
148
- worker.postMessage({
149
- protocol: SPEECH_WORKER_PROTOCOL,
150
- id,
151
- op,
152
- payload,
153
- }, collectSpeechTransferables(payload));
348
+ destination.postMessage(message, transfers);
154
349
  } catch (error) {
155
350
  this.#pending.delete(id);
156
351
  cleanup();
@@ -158,21 +353,27 @@ class SpeechWorkerClient {
158
353
  "ARCANE_AI_WORKER_MESSAGE_ERROR",
159
354
  "Unable to send an operation to the speech Worker.",
160
355
  error,
356
+ `${operationSubject(this.#role, op)}-message-rejected`,
161
357
  );
162
358
  reject(failure);
163
- void this.terminate(failure, { intentional: false });
359
+ void this.terminate(failure, { intentional: false }).catch(() => undefined);
164
360
  }
165
361
  });
166
362
  }
167
363
 
168
- async terminate(reason = clientError(
169
- "ARCANE_AI_OPERATION_SUPERSEDED",
170
- "The speech Worker was terminated.",
171
- ), { intentional = true } = {}) {
364
+ async terminate(reason = null, { intentional = true } = {}) {
172
365
  if (typeof intentional !== "boolean") {
173
366
  throw new TypeError("Speech Worker termination intent must be a boolean.");
174
367
  }
175
368
  if (this.#terminated) return;
369
+ const terminationReason = reason instanceof Error && WORKER_CLIENT_ERRORS.has(reason)
370
+ ? reason
371
+ : clientError(
372
+ "ARCANE_AI_OPERATION_SUPERSEDED",
373
+ "The speech Worker was terminated.",
374
+ reason instanceof Error ? reason : undefined,
375
+ `${this.#role}-worker-terminated`,
376
+ );
176
377
  this.#terminated = true;
177
378
  const worker = this.#worker;
178
379
  this.#worker = null;
@@ -184,16 +385,26 @@ class SpeechWorkerClient {
184
385
  }
185
386
  }
186
387
  const pendingOperations = [...this.#pending.values()];
187
- for (const pending of pendingOperations) {
188
- pending.cleanup();
189
- }
388
+ for (const pending of pendingOperations) pending.cleanup();
190
389
  this.#pending.clear();
191
390
  try {
391
+ for (const port of [
392
+ this.#privatePorts?.clientPort,
393
+ this.#privatePorts?.workerPort,
394
+ ]) {
395
+ try {
396
+ port?.close();
397
+ } catch {
398
+ // Worker termination remains the authoritative cancellation boundary.
399
+ }
400
+ }
401
+ this.#privatePorts = null;
402
+ this.#transport = null;
192
403
  const termination = worker?.terminate();
193
404
  if (termination && typeof termination.then === "function") await termination;
194
405
  } finally {
195
- for (const pending of pendingOperations) pending.reject(reason);
196
- this.#onTermination(Object.freeze({ reason, intentional }));
406
+ for (const pending of pendingOperations) pending.reject(terminationReason);
407
+ this.#onTermination(Object.freeze({ reason: terminationReason, intentional }));
197
408
  }
198
409
  }
199
410
  }
@@ -205,3 +416,7 @@ export function createSpeechWorkerClient(options) {
205
416
  export function isSpeechWorkerClient(value) {
206
417
  return WORKER_CLIENTS.has(value);
207
418
  }
419
+
420
+ export function isSpeechWorkerClientError(value) {
421
+ return value instanceof Error && WORKER_CLIENT_ERRORS.has(value);
422
+ }