arcane-os 0.5.7 → 0.5.9

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 (46) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +39 -27
  3. package/browser-runtime/ai/browser-speech-artifacts.mjs +23 -1683
  4. package/browser-runtime/ai/browser-speech-providers.mjs +353 -143
  5. package/browser-runtime/ai/browser-wasm-llm-provider.mjs +32 -324
  6. package/browser-runtime/ai/speech-worker-client.mjs +51 -14
  7. package/browser-runtime/ai/speech-worker-runtime.mjs +45 -1072
  8. package/browser-runtime/dependencies/strong-type/package.json +22 -23
  9. package/browser-runtime/event-manager.mjs +27 -41
  10. package/package.json +2 -3
  11. package/runtime/arcane/components/chat.html +1 -1
  12. package/runtime/arcane/components/speech.html +8 -38
  13. package/runtime/arcane/components/voice-transcription.html +1 -1
  14. package/runtime/arcane/css/theme.css +1 -1
  15. package/runtime/arcane/entities/Chat.js +12 -7
  16. package/runtime/arcane/modules/AI.js +569 -402
  17. package/runtime/arcane/modules/AIPreferenceTuple.js +1 -1
  18. package/runtime/arcane/modules/AIProviderRuntime.js +129 -68
  19. package/runtime/arcane/modules/AIRuntimeState.js +2 -18
  20. package/runtime/arcane/modules/BrowserTestSuite.js +2 -5
  21. package/runtime/arcane/modules/CalculatorEngine.js +0 -4
  22. package/runtime/arcane/modules/ChatRecords.js +169 -1
  23. package/runtime/arcane/modules/CommunicationHub.js +0 -19
  24. package/runtime/arcane/modules/ConfiguredAIChatSession.js +28 -25
  25. package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +4 -5
  26. package/runtime/arcane/modules/DocumentLexicalSearch.js +1 -1
  27. package/runtime/arcane/modules/Errors.js +6 -19
  28. package/runtime/arcane/modules/Mail.js +1 -2
  29. package/runtime/arcane/modules/MailTransport.mjs +1 -3
  30. package/runtime/arcane/modules/OllamaModelIdentifier.js +1 -1
  31. package/runtime/arcane/modules/PersistentAIChatSession.js +5 -5
  32. package/runtime/arcane/modules/ScreenCapture.js +2 -4
  33. package/runtime/arcane/modules/SpeechPlayback.js +0 -27
  34. package/runtime/arcane/modules/WaitForComponent.js +0 -1
  35. package/src/app-descriptor.mjs +1 -1
  36. package/src/doctor.mjs +1 -3
  37. package/src/event-manager.mjs +27 -41
  38. package/src/import-map.mjs +1 -1
  39. package/src/index.mjs +2 -9
  40. package/src/installed-sdk-runtime.mjs +1 -11
  41. package/src/mail-api.mjs +0 -1
  42. package/src/native-provider-loader.mjs +0 -9
  43. package/src/scaffold.mjs +9 -21
  44. package/src/toolchain.mjs +5 -28
  45. package/src/workspace-runtime.mjs +0 -4
  46. package/src/workspace.mjs +2 -23
@@ -19,6 +19,10 @@ const ROLE_OPERATION = completeValue({ stt: "transcribe", tts: "synthesize" });
19
19
  const STT_SAMPLE_RATE = 16_000;
20
20
  const TTS_SAMPLE_RATE = 24_000;
21
21
  const TTS_RESPONSE_FORMAT = "wav";
22
+ const TTS_EXECUTION_DEVICES = new Set(["auto", "webgpu", "wasm"]);
23
+ const DEFAULT_TTS_EXECUTION_DEVICE = "auto";
24
+ const DEFAULT_TTS_MAX_CONCURRENT_REQUESTS = 2;
25
+ const MAX_TTS_CONCURRENT_REQUESTS = 4;
22
26
  const ROLE_REQUEST_REASON = completeValue({
23
27
  stt: "stt-transcription-cancelled",
24
28
  tts: "tts-synthesis-cancelled",
@@ -141,6 +145,62 @@ function requiredIdentifier(value, label) {
141
145
  return value.trim();
142
146
  }
143
147
 
148
+ function normalizeSpeechExecution(role, execution) {
149
+ if (role === "stt") {
150
+ if (execution !== undefined) {
151
+ throw new TypeError("Browser Whisper does not accept an execution option.");
152
+ }
153
+ return completeValue({ device: "wasm", maxConcurrentRequests: 1 });
154
+ }
155
+ if (execution === undefined) {
156
+ return completeValue({
157
+ device: DEFAULT_TTS_EXECUTION_DEVICE,
158
+ maxConcurrentRequests: DEFAULT_TTS_MAX_CONCURRENT_REQUESTS,
159
+ });
160
+ }
161
+ if (!execution || typeof execution !== "object" || Array.isArray(execution)) {
162
+ throw new TypeError("Browser Kokoro execution must be a plain data record.");
163
+ }
164
+ const prototype = Object.getPrototypeOf(execution);
165
+ if (prototype !== Object.prototype && prototype !== null) {
166
+ throw new TypeError("Browser Kokoro execution must be a plain data record.");
167
+ }
168
+ const descriptors = Object.getOwnPropertyDescriptors(execution);
169
+ for (const key of Reflect.ownKeys(descriptors)) {
170
+ if (
171
+ (key !== "device" && key !== "maxConcurrentRequests")
172
+ || !Object.hasOwn(descriptors[key], "value")
173
+ ) {
174
+ throw new TypeError("Browser Kokoro execution contains an unsupported or accessor field.");
175
+ }
176
+ }
177
+ const device = Object.hasOwn(descriptors, "device")
178
+ ? descriptors.device.value
179
+ : DEFAULT_TTS_EXECUTION_DEVICE;
180
+ const maxConcurrentRequests = Object.hasOwn(descriptors, "maxConcurrentRequests")
181
+ ? descriptors.maxConcurrentRequests.value
182
+ : DEFAULT_TTS_MAX_CONCURRENT_REQUESTS;
183
+ if (!TTS_EXECUTION_DEVICES.has(device)) {
184
+ throw new TypeError('Browser Kokoro execution.device must be "auto", "webgpu", or "wasm".');
185
+ }
186
+ if (
187
+ !Number.isSafeInteger(maxConcurrentRequests)
188
+ || maxConcurrentRequests < 1
189
+ || maxConcurrentRequests > MAX_TTS_CONCURRENT_REQUESTS
190
+ ) {
191
+ throw new TypeError("Browser Kokoro execution.maxConcurrentRequests must be a safe integer from 1 through 4.");
192
+ }
193
+ return completeValue({ device, maxConcurrentRequests });
194
+ }
195
+
196
+ function navigatorHasWebGpu() {
197
+ try {
198
+ return Boolean(globalThis.navigator?.gpu);
199
+ } catch {
200
+ return false;
201
+ }
202
+ }
203
+
144
204
  function createProviderAuthority({
145
205
  providerId,
146
206
  role,
@@ -150,7 +210,7 @@ function createProviderAuthority({
150
210
  }) {
151
211
  if (graph !== undefined) {
152
212
  if (model !== undefined || runtime !== undefined) {
153
- throw new TypeError("Browser speech graph is mutually exclusive with legacy model and runtime descriptors.");
213
+ throw new TypeError("Browser speech graph is mutually exclusive with model and runtime descriptors.");
154
214
  }
155
215
  if (!isBrowserSpeechArtifactGraph(graph)) {
156
216
  throw new TypeError("Browser speech graph must be created by createBrowserSpeechArtifactGraph().");
@@ -324,6 +384,7 @@ function publicStatus({
324
384
  cache,
325
385
  lifecycleReason,
326
386
  activeOperation,
387
+ execution,
327
388
  secureIntent,
328
389
  warnings,
329
390
  }) {
@@ -340,6 +401,7 @@ function publicStatus({
340
401
  generation,
341
402
  errorCode,
342
403
  cache,
404
+ ...(execution ? { execution } : {}),
343
405
  ...(secureIntent ? { security: secureIntent } : {}),
344
406
  warnings,
345
407
  });
@@ -1040,6 +1102,7 @@ function createBrowserSpeechProvider({
1040
1102
  graph,
1041
1103
  model,
1042
1104
  runtime,
1105
+ execution,
1043
1106
  appSecurity,
1044
1107
  security,
1045
1108
  store,
@@ -1062,6 +1125,7 @@ function createBrowserSpeechProvider({
1062
1125
  model,
1063
1126
  runtime,
1064
1127
  });
1128
+ const speechExecution = normalizeSpeechExecution(role, execution);
1065
1129
  const defaultSecureIntent = reportedSecureIntent(appSecurity, security);
1066
1130
  const operation = ROLE_OPERATION[role];
1067
1131
  const speech = role === "stt"
@@ -1097,7 +1161,8 @@ function createBrowserSpeechProvider({
1097
1161
  let loadOperation = null;
1098
1162
  let unloadOperation = null;
1099
1163
  let disposeOperation = null;
1100
- let requestOperation = null;
1164
+ const requestOperations = new Set();
1165
+ let selectedDevice = null;
1101
1166
  let lastWarnings = NO_PROVIDER_WARNINGS;
1102
1167
  let secureIntent = defaultSecureIntent;
1103
1168
 
@@ -1110,45 +1175,186 @@ function createBrowserSpeechProvider({
1110
1175
  id: providerId,
1111
1176
  authority,
1112
1177
  state,
1113
- busy: requestOperation !== null,
1178
+ busy: requestOperations.size > 0,
1114
1179
  generation,
1115
1180
  errorCode,
1116
1181
  cache,
1117
1182
  lifecycleReason,
1118
1183
  activeOperation,
1184
+ execution: role === "tts"
1185
+ ? completeValue({
1186
+ requestedDevice: speechExecution.device,
1187
+ selectedDevice,
1188
+ maxConcurrentRequests: speechExecution.maxConcurrentRequests,
1189
+ activeRequestCount: requestOperations.size,
1190
+ })
1191
+ : null,
1119
1192
  secureIntent,
1120
1193
  warnings: providerWarnings(runtimeWarnings),
1121
1194
  });
1122
1195
  }
1123
1196
 
1124
- function releaseSlot(slot) {
1125
- if (!slot || slot.released) return;
1126
- slot.prepared.release();
1127
- slot.released = true;
1197
+ function releasePreparation(preparation) {
1198
+ if (!preparation || preparation.released) return;
1199
+ preparation.released = true;
1200
+ preparation.prepared.release();
1128
1201
  }
1129
1202
 
1130
- async function terminateSlot(slot, reason, { intentional = true } = {}) {
1131
- if (!slot) return;
1132
- if (!isSpeechWorkerClient(slot.client)) {
1133
- throw providerError(
1134
- "ARCANE_AI_ADAPTER_PROTOCOL_MISMATCH",
1135
- "The browser speech Worker client is not SDK-owned.",
1136
- undefined,
1137
- `${role}-worker-client-authority-mismatch`,
1138
- );
1139
- }
1203
+ async function terminatePool(
1204
+ pool,
1205
+ reason,
1206
+ { intentional = true, releasePrepared = true } = {},
1207
+ ) {
1208
+ if (!pool) return;
1140
1209
  const trustedReason = trustedWorkerFailure(reason, role);
1210
+ let terminationFailure = null;
1141
1211
  try {
1142
- await slot.client.terminate(trustedReason, { intentional });
1212
+ if (!pool.terminationPromise) {
1213
+ pool.terminating = true;
1214
+ pool.terminationPromise = (async function terminateSpeechWorkerPool() {
1215
+ const terminations = [];
1216
+ for (const slot of pool.slots) {
1217
+ if (!isSpeechWorkerClient(slot.client)) {
1218
+ terminations.push(Promise.reject(providerError(
1219
+ "ARCANE_AI_ADAPTER_PROTOCOL_MISMATCH",
1220
+ "The browser speech Worker client is not SDK-owned.",
1221
+ undefined,
1222
+ `${role}-worker-client-authority-mismatch`,
1223
+ )));
1224
+ continue;
1225
+ }
1226
+ terminations.push(slot.client.terminate(trustedReason, { intentional }));
1227
+ }
1228
+ const settlements = await Promise.allSettled(terminations);
1229
+ for (const settlement of settlements) {
1230
+ if (settlement.status === "rejected") throw settlement.reason;
1231
+ }
1232
+ })();
1233
+ }
1234
+ await pool.terminationPromise;
1143
1235
  } catch (error) {
1144
- throw trustedWorkerFailure(error, role);
1236
+ terminationFailure = trustedWorkerFailure(error, role);
1145
1237
  } finally {
1238
+ if (releasePrepared) {
1239
+ try {
1240
+ releasePreparation(pool.preparation);
1241
+ } catch (error) {
1242
+ terminationFailure = trustedWorkerFailure(error, role);
1243
+ }
1244
+ }
1245
+ }
1246
+ if (terminationFailure) throw terminationFailure;
1247
+ }
1248
+
1249
+ function handlePoolTermination(pool, slot, { reason, intentional }) {
1250
+ slot.terminated = true;
1251
+ if (pool.terminating) return;
1252
+ const trustedReason = trustedWorkerFailure(reason, role);
1253
+ pool.failure = trustedReason;
1254
+ const isActivePool = active === pool;
1255
+ const terminationGeneration = generation;
1256
+ if (isActivePool) {
1257
+ active = null;
1258
+ state = intentional ? "unloaded" : "error";
1259
+ errorCode = intentional ? null : workerFailureCode(trustedReason);
1260
+ lifecycleReason = intentional
1261
+ ? `${role}-worker-terminated`
1262
+ : trustedReason.reason ?? (errorCode === "ARCANE_AI_WORKER_MESSAGE_ERROR"
1263
+ || errorCode === "ARCANE_AI_WORKER_MESSAGE_REJECTED"
1264
+ ? `${role}-worker-message-rejected`
1265
+ : errorCode === "ARCANE_AI_ADAPTER_PROTOCOL_MISMATCH"
1266
+ ? `${role}-worker-protocol-mismatch`
1267
+ : `${role}-worker-crashed`);
1268
+ activeOperation = null;
1269
+ selectedDevice = null;
1270
+ }
1271
+ void terminatePool(pool, trustedReason, {
1272
+ releasePrepared: isActivePool,
1273
+ }).then(
1274
+ function completeUnexpectedPoolTermination() {
1275
+ if (active === pool) active = null;
1276
+ },
1277
+ function reportUnexpectedPoolTerminationFailure(error) {
1278
+ if (active === pool) active = null;
1279
+ if (
1280
+ isActivePool
1281
+ && generation === terminationGeneration
1282
+ && state !== "disposed"
1283
+ && state !== "unloading"
1284
+ ) {
1285
+ const failure = trustedWorkerFailure(error, role);
1286
+ state = "error";
1287
+ errorCode = failure.code;
1288
+ lifecycleReason = failure.reason;
1289
+ activeOperation = null;
1290
+ selectedDevice = null;
1291
+ }
1292
+ },
1293
+ );
1294
+ }
1295
+
1296
+ function createWorkerPool(preparation, device, warnings) {
1297
+ const pool = {
1298
+ preparation,
1299
+ device,
1300
+ warnings,
1301
+ slots: [],
1302
+ failure: null,
1303
+ terminating: false,
1304
+ terminationPromise: null,
1305
+ };
1306
+ for (let index = 0; index < speechExecution.maxConcurrentRequests; index += 1) {
1307
+ const slot = {
1308
+ client: null,
1309
+ requestOperation: null,
1310
+ terminated: false,
1311
+ };
1312
+ slot.client = createSpeechWorkerClient({
1313
+ role,
1314
+ onTermination: function handleSpeechWorkerTermination(termination) {
1315
+ handlePoolTermination(pool, slot, termination);
1316
+ },
1317
+ });
1318
+ pool.slots.push(slot);
1319
+ }
1320
+ return pool;
1321
+ }
1322
+
1323
+ async function loadWorkerPool(preparation, device, warnings, signal) {
1324
+ const pool = createWorkerPool(preparation, device, warnings);
1325
+ const configuration = completeValue({
1326
+ role,
1327
+ runtime: preparation.prepared.runtime,
1328
+ model: preparation.prepared.model,
1329
+ execution: completeValue({ device }),
1330
+ ...(authority.graph ? {
1331
+ artifactGraphProtocol: authority.graph.protocol,
1332
+ } : {}),
1333
+ });
1334
+ let failure = pool.failure;
1335
+ if (!failure) {
1146
1336
  try {
1147
- releaseSlot(slot);
1337
+ await pool.slots[0].client.request("load", { configuration }, { signal });
1148
1338
  } catch (error) {
1149
- throw trustedWorkerFailure(error, role);
1339
+ failure = error;
1150
1340
  }
1151
1341
  }
1342
+ if (!failure) {
1343
+ const remainingLoads = [];
1344
+ for (const slot of pool.slots.slice(1)) {
1345
+ remainingLoads.push(slot.client.request("load", { configuration }, { signal }));
1346
+ }
1347
+ const settlements = await Promise.allSettled(remainingLoads);
1348
+ failure = pool.failure;
1349
+ for (const settlement of settlements) {
1350
+ if (!failure && settlement.status === "rejected") failure = settlement.reason;
1351
+ }
1352
+ }
1353
+ if (failure) {
1354
+ await terminatePool(pool, failure, { releasePrepared: false });
1355
+ throw failure;
1356
+ }
1357
+ return pool;
1152
1358
  }
1153
1359
 
1154
1360
  const provider = {
@@ -1156,6 +1362,7 @@ function createBrowserSpeechProvider({
1156
1362
  role,
1157
1363
  id: providerId,
1158
1364
  localOnly: true,
1365
+ maxConcurrentRequests: speechExecution.maxConcurrentRequests,
1159
1366
 
1160
1367
  catalog() {
1161
1368
  return completeValue([catalogEntry]);
@@ -1282,27 +1489,37 @@ function createBrowserSpeechProvider({
1282
1489
  warnings: NO_PROVIDER_WARNINGS,
1283
1490
  observers: new Set(),
1284
1491
  settled: false,
1285
- abort: (
1492
+ abort(
1286
1493
  reason = `${role}-load-cancelled`,
1287
1494
  code = "ARCANE_AI_REQUEST_ABORTED",
1288
- ) => linked.abort(reason, code),
1495
+ ) {
1496
+ linked.abort(reason, code);
1497
+ },
1289
1498
  };
1290
1499
  lastWarnings = NO_PROVIDER_WARNINGS;
1291
- const promise = Promise.resolve().then(async () => {
1500
+ selectedDevice = null;
1501
+ const promise = Promise.resolve().then(async function loadBrowserSpeechProviderPool() {
1292
1502
  let prepared = null;
1293
- let slot = null;
1503
+ let preparation = null;
1504
+ let pool = null;
1294
1505
  try {
1295
1506
  prepared = await store.prepare(authority.graph ?? authority, {
1296
1507
  signal: linked.controller.signal,
1297
1508
  offline,
1298
1509
  });
1510
+ preparation = {
1511
+ prepared,
1512
+ released: false,
1513
+ };
1299
1514
  record.warnings = Array.isArray(prepared.warnings)
1300
1515
  ? completeValue([...prepared.warnings])
1301
1516
  : NO_PROVIDER_WARNINGS;
1302
1517
  lastWarnings = record.warnings;
1303
1518
  throwIfAborted(
1304
1519
  linked.controller.signal,
1305
- () => linked.reason(`${role}-load-cancelled`),
1520
+ function browserSpeechLoadAbortReason() {
1521
+ return linked.reason(`${role}-load-cancelled`);
1522
+ },
1306
1523
  );
1307
1524
  if (operationGeneration !== generation) {
1308
1525
  throw providerError(
@@ -1312,68 +1529,40 @@ function createBrowserSpeechProvider({
1312
1529
  `${role}-load-superseded-by-unload`,
1313
1530
  );
1314
1531
  }
1315
- slot = {
1316
- prepared,
1317
- released: false,
1318
- client: null,
1319
- warnings: record.warnings,
1320
- };
1321
- slot.client = createSpeechWorkerClient({
1322
- role,
1323
- onTermination({ reason, intentional }) {
1324
- const trustedReason = trustedWorkerFailure(reason, role);
1325
- if (active === slot) {
1326
- active = null;
1327
- if (state !== "disposed" && state !== "unloading") {
1328
- state = intentional ? "unloaded" : "error";
1329
- errorCode = intentional ? null : workerFailureCode(trustedReason);
1330
- lifecycleReason = intentional
1331
- ? `${role}-worker-terminated`
1332
- : trustedReason.reason ?? (errorCode === "ARCANE_AI_WORKER_MESSAGE_ERROR"
1333
- || errorCode === "ARCANE_AI_WORKER_MESSAGE_REJECTED"
1334
- ? `${role}-worker-message-rejected`
1335
- : errorCode === "ARCANE_AI_ADAPTER_PROTOCOL_MISMATCH"
1336
- ? `${role}-worker-protocol-mismatch`
1337
- : `${role}-worker-crashed`);
1338
- activeOperation = null;
1339
- }
1340
- try {
1341
- releaseSlot(slot);
1342
- } catch (error) {
1343
- const releaseFailure = trustedWorkerFailure(error, role);
1344
- if (state !== "disposed" && state !== "unloading") {
1345
- state = "error";
1346
- errorCode = releaseFailure.code;
1347
- lifecycleReason = releaseFailure.reason;
1348
- activeOperation = null;
1349
- }
1350
- throw releaseFailure;
1351
- }
1352
- }
1353
- },
1354
- });
1355
- if (!isSpeechWorkerClient(slot.client)) {
1356
- throw providerError(
1357
- "ARCANE_AI_ADAPTER_PROTOCOL_MISMATCH",
1358
- "The browser speech Worker client is not SDK-owned.",
1359
- undefined,
1360
- `${role}-worker-client-authority-mismatch`,
1532
+ const primaryDevice = role === "stt"
1533
+ ? "wasm"
1534
+ : speechExecution.device === "auto"
1535
+ ? navigatorHasWebGpu() ? "webgpu" : "wasm"
1536
+ : speechExecution.device;
1537
+ try {
1538
+ pool = await loadWorkerPool(
1539
+ preparation,
1540
+ primaryDevice,
1541
+ record.warnings,
1542
+ linked.controller.signal,
1543
+ );
1544
+ } catch (error) {
1545
+ if (
1546
+ role !== "tts"
1547
+ || speechExecution.device !== "auto"
1548
+ || primaryDevice !== "webgpu"
1549
+ || linked.controller.signal.aborted
1550
+ || operationGeneration !== generation
1551
+ ) {
1552
+ throw error;
1553
+ }
1554
+ pool = await loadWorkerPool(
1555
+ preparation,
1556
+ "wasm",
1557
+ record.warnings,
1558
+ linked.controller.signal,
1361
1559
  );
1362
1560
  }
1363
- const configuration = completeValue({
1364
- role,
1365
- runtime: prepared.runtime,
1366
- model: prepared.model,
1367
- ...(authority.graph ? {
1368
- artifactGraphProtocol: authority.graph.protocol,
1369
- } : {}),
1370
- });
1371
- await slot.client.request("load", { configuration }, {
1372
- signal: linked.controller.signal,
1373
- });
1374
1561
  throwIfAborted(
1375
1562
  linked.controller.signal,
1376
- () => linked.reason(`${role}-load-cancelled`),
1563
+ function browserSpeechLoadedPoolAbortReason() {
1564
+ return linked.reason(`${role}-load-cancelled`);
1565
+ },
1377
1566
  );
1378
1567
  if (operationGeneration !== generation) {
1379
1568
  throw providerError(
@@ -1383,7 +1572,8 @@ function createBrowserSpeechProvider({
1383
1572
  `${role}-load-superseded-by-unload`,
1384
1573
  );
1385
1574
  }
1386
- active = slot;
1575
+ active = pool;
1576
+ selectedDevice = pool.device;
1387
1577
  cache = prepared.cache;
1388
1578
  state = "ready";
1389
1579
  lifecycleReason = `${role}-load-completed`;
@@ -1394,13 +1584,16 @@ function createBrowserSpeechProvider({
1394
1584
  ? linkedAbortFailure(linked, `${role}-load-cancelled`)
1395
1585
  : trustedLoadFailure(error, role);
1396
1586
  try {
1397
- if (slot) await terminateSlot(slot, failure);
1398
- else prepared?.release();
1587
+ if (pool) {
1588
+ await terminatePool(pool, failure, { releasePrepared: false });
1589
+ }
1590
+ releasePreparation(preparation);
1399
1591
  } catch (cleanupError) {
1400
- failure = slot
1592
+ failure = pool
1401
1593
  ? trustedWorkerFailure(cleanupError, role)
1402
1594
  : trustedLoadFailure(cleanupError, role);
1403
1595
  }
1596
+ selectedDevice = null;
1404
1597
  if (operationGeneration === generation && state !== "unloading" && state !== "disposed") {
1405
1598
  state = failure?.code === "ARCANE_AI_REQUEST_ABORTED"
1406
1599
  || failure?.code === "ARCANE_AI_OPERATION_SUPERSEDED"
@@ -1465,7 +1658,15 @@ function createBrowserSpeechProvider({
1465
1658
  `${role}-provider-request-not-ready`,
1466
1659
  );
1467
1660
  }
1468
- if (requestOperation) {
1661
+ const pool = active;
1662
+ let slot = null;
1663
+ for (const candidate of pool.slots) {
1664
+ if (!candidate.terminated && !candidate.requestOperation) {
1665
+ slot = candidate;
1666
+ break;
1667
+ }
1668
+ }
1669
+ if (!slot) {
1469
1670
  throw providerError(
1470
1671
  "ARCANE_AI_PROVIDER_BUSY",
1471
1672
  "The browser speech provider is already processing a request.",
@@ -1483,32 +1684,44 @@ function createBrowserSpeechProvider({
1483
1684
  linked.release();
1484
1685
  throw linkedAbortFailure(linked, ROLE_REQUEST_REASON[role]);
1485
1686
  }
1486
- const slot = active;
1487
1687
  const requestGeneration = generation;
1488
- let workerRequestStarted = false;
1489
1688
  activeOperation = role === "stt"
1490
1689
  ? "stt-provider-transcription"
1491
1690
  : "tts-provider-synthesis";
1492
1691
  lifecycleReason = role === "stt"
1493
1692
  ? "stt-transcription-started"
1494
1693
  : "tts-synthesis-started";
1495
- const promise = (async () => {
1694
+ const requestRecord = {
1695
+ promise: null,
1696
+ slot,
1697
+ abort(
1698
+ reason = ROLE_REQUEST_REASON[role],
1699
+ code = "ARCANE_AI_REQUEST_ABORTED",
1700
+ ) {
1701
+ linked.abort(reason, code);
1702
+ },
1703
+ };
1704
+ slot.requestOperation = requestRecord;
1705
+ requestOperations.add(requestRecord);
1706
+ function browserSpeechRequestAbortReason() {
1707
+ return linked.reason(ROLE_REQUEST_REASON[role]);
1708
+ }
1709
+ const promise = Promise.resolve().then(async function runBrowserSpeechProviderRequest() {
1496
1710
  const normalized = await normalizeRequestPayload(
1497
1711
  role,
1498
1712
  context.payload,
1499
1713
  authority,
1500
1714
  linked.controller.signal,
1501
- () => linked.reason(ROLE_REQUEST_REASON[role]),
1715
+ browserSpeechRequestAbortReason,
1502
1716
  );
1503
1717
  throwIfAborted(
1504
1718
  linked.controller.signal,
1505
- () => linked.reason(ROLE_REQUEST_REASON[role]),
1719
+ browserSpeechRequestAbortReason,
1506
1720
  );
1507
- workerRequestStarted = true;
1508
1721
  const result = await slot.client.request("use", normalized.payload, {
1509
1722
  signal: linked.controller.signal,
1510
1723
  });
1511
- if (requestGeneration !== generation || active !== slot) {
1724
+ if (requestGeneration !== generation || active !== pool || state !== "ready") {
1512
1725
  throw providerError(
1513
1726
  "ARCANE_AI_OPERATION_SUPERSEDED",
1514
1727
  "The browser speech result was superseded.",
@@ -1521,48 +1734,36 @@ function createBrowserSpeechProvider({
1521
1734
  return role === "tts" && normalized.shared
1522
1735
  ? encodeSharedSynthesisResult(result, authority)
1523
1736
  : result;
1524
- })();
1525
- requestOperation = completeValue({
1526
- promise,
1527
- abort: (
1528
- reason = ROLE_REQUEST_REASON[role],
1529
- code = "ARCANE_AI_REQUEST_ABORTED",
1530
- ) => linked.abort(reason, code),
1531
1737
  });
1738
+ requestRecord.promise = promise;
1532
1739
  try {
1533
1740
  const result = await promise;
1534
- lifecycleReason = role === "stt"
1535
- ? "stt-transcription-completed"
1536
- : "tts-synthesis-completed";
1741
+ if (requestGeneration === generation && state === "ready") {
1742
+ lifecycleReason = role === "stt"
1743
+ ? "stt-transcription-completed"
1744
+ : "tts-synthesis-completed";
1745
+ }
1537
1746
  return result;
1538
1747
  } catch (error) {
1539
- let failure = linked.controller.signal.aborted
1748
+ const failure = linked.controller.signal.aborted
1540
1749
  && (error?.code === "ARCANE_AI_REQUEST_ABORTED"
1541
1750
  || linked.code() === "ARCANE_AI_OPERATION_SUPERSEDED")
1542
1751
  ? linkedAbortFailure(linked, ROLE_REQUEST_REASON[role])
1543
1752
  : trustedRequestFailure(error, role);
1544
- if (failure?.code === "ARCANE_AI_REQUEST_ABORTED"
1545
- && workerRequestStarted
1546
- && active === slot) {
1547
- active = null;
1548
- try {
1549
- releaseSlot(slot);
1550
- } catch (cleanupError) {
1551
- failure = trustedWorkerFailure(cleanupError, role);
1552
- }
1553
- state = "unloaded";
1753
+ if (requestGeneration === generation && state !== "unloading" && state !== "disposed") {
1754
+ lifecycleReason = failure?.reason
1755
+ ?? (failure?.code === "ARCANE_AI_REQUEST_ABORTED"
1756
+ ? ROLE_REQUEST_REASON[role]
1757
+ : role === "stt"
1758
+ ? "stt-transcription-engine-operation-rejected"
1759
+ : "tts-synthesis-engine-operation-rejected");
1554
1760
  }
1555
- lifecycleReason = failure?.reason
1556
- ?? (failure?.code === "ARCANE_AI_REQUEST_ABORTED"
1557
- ? ROLE_REQUEST_REASON[role]
1558
- : role === "stt"
1559
- ? "stt-transcription-engine-operation-rejected"
1560
- : "tts-synthesis-engine-operation-rejected");
1561
1761
  throw failure;
1562
1762
  } finally {
1563
1763
  linked.release();
1564
- if (requestOperation?.promise === promise) requestOperation = null;
1565
- activeOperation = null;
1764
+ if (slot.requestOperation === requestRecord) slot.requestOperation = null;
1765
+ requestOperations.delete(requestRecord);
1766
+ if (requestOperations.size === 0 && state === "ready") activeOperation = null;
1566
1767
  }
1567
1768
  },
1568
1769
 
@@ -1594,47 +1795,56 @@ function createBrowserSpeechProvider({
1594
1795
  errorCode = null;
1595
1796
  activeOperation = `${role}-provider-unload`;
1596
1797
  const capturedLoad = loadOperation?.promise ?? null;
1597
- const capturedRequest = requestOperation?.promise ?? null;
1798
+ const capturedRequests = [];
1799
+ for (const requestRecord of requestOperations) {
1800
+ if (requestRecord.promise) capturedRequests.push(requestRecord.promise);
1801
+ }
1598
1802
  if (loadOperation) {
1599
1803
  lifecycleReason = `${role}-load-superseded-by-unload`;
1600
1804
  loadOperation.abort(
1601
1805
  `${role}-load-superseded-by-unload`,
1602
1806
  "ARCANE_AI_OPERATION_SUPERSEDED",
1603
1807
  );
1604
- } else if (requestOperation) {
1808
+ }
1809
+ if (requestOperations.size > 0) {
1605
1810
  lifecycleReason = role === "stt"
1606
1811
  ? "stt-transcription-superseded-by-unload"
1607
1812
  : "tts-synthesis-superseded-by-unload";
1608
- requestOperation.abort(lifecycleReason, "ARCANE_AI_OPERATION_SUPERSEDED");
1609
- } else {
1813
+ for (const requestRecord of requestOperations) {
1814
+ requestRecord.abort(lifecycleReason, "ARCANE_AI_OPERATION_SUPERSEDED");
1815
+ }
1816
+ } else if (!loadOperation) {
1610
1817
  lifecycleReason = `${role}-unload-started`;
1611
1818
  }
1612
- const promise = (async () => {
1613
- await Promise.allSettled([
1614
- capturedLoad,
1615
- capturedRequest,
1616
- ].filter(Boolean));
1617
- const slot = active;
1819
+ const promise = Promise.resolve().then(async function unloadBrowserSpeechProviderPool() {
1820
+ const pendingOperations = [...capturedRequests];
1821
+ if (capturedLoad) pendingOperations.unshift(capturedLoad);
1822
+ await Promise.allSettled(pendingOperations);
1823
+ const pool = active;
1618
1824
  active = null;
1619
- await terminateSlot(slot, providerError(
1825
+ await terminatePool(pool, providerError(
1620
1826
  "ARCANE_AI_OPERATION_SUPERSEDED",
1621
1827
  "The browser speech Worker was terminated by unload().",
1622
1828
  undefined,
1623
1829
  `${role}-worker-terminated-by-unload`,
1624
1830
  ));
1831
+ selectedDevice = null;
1625
1832
  cache = null;
1626
1833
  state = "unloaded";
1627
1834
  lifecycleReason = `${role}-unload-completed`;
1628
1835
  activeOperation = null;
1629
1836
  return status();
1630
- })();
1631
- const tracked = promise.then((value) => {
1632
- if (unloadOperation === tracked) unloadOperation = null;
1633
- return value;
1634
- }, (error) => {
1635
- if (unloadOperation === tracked) unloadOperation = null;
1636
- throw error;
1637
1837
  });
1838
+ const tracked = promise.then(
1839
+ function completeBrowserSpeechProviderUnload(value) {
1840
+ if (unloadOperation === tracked) unloadOperation = null;
1841
+ return value;
1842
+ },
1843
+ function rejectBrowserSpeechProviderUnload(error) {
1844
+ if (unloadOperation === tracked) unloadOperation = null;
1845
+ throw error;
1846
+ },
1847
+ );
1638
1848
  unloadOperation = tracked;
1639
1849
  return awaitAbortableSpeechOperation(
1640
1850
  tracked,