arcane-os 0.2.0 → 0.2.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.1
4
+
5
+ - Added explicit selected-unloaded chat activation, truthful legacy Cloud/Core
6
+ route readiness, and provider-native chat-response normalization without
7
+ startup downloads, hidden provider selection, or application policy.
8
+ - Normalized shared STT/TTS requests at the browser-speech provider boundary
9
+ while retaining caller-supplied immutable runtime, model, and voice
10
+ authorities and fail-closed format, lifecycle, error, and cancellation rules.
11
+ - Applied authenticated dependency import maps to every packaged browser
12
+ document and published a deterministic browser-readable runtime projection
13
+ inventory derived from the verified package receipt.
14
+ - Added caller-budgeted complete DBOPFS source evaluation with deterministic
15
+ zero-score fallback, independent scoring/excerpt/output bounds, and opt-in
16
+ preserve-readable bootstrap/evaluation read coverage.
17
+ - Bound external workspace validation, packaging, scaffolding, doctor,
18
+ toolchain, and development serving to one authenticated installed package,
19
+ including the exact `arcane-sdk@npm:arcane-os@0.2.1` alias form.
20
+
3
21
  ## 0.2.0
4
22
 
5
23
  - Made the SDK the canonical owner of the portable Arcane runtime and retired
package/README.md CHANGED
@@ -19,7 +19,7 @@ version-locked SDK runtime, while an integrated Arcane checkout uses its live
19
19
  `arcane/` runtime. Both profiles preserve the same app URLs, theme, packaging,
20
20
  event, cancellation, and browser run contracts.
21
21
 
22
- This checkout defines the `0.2.0` SDK contract. Applications pin one exact npm
22
+ This checkout defines the `0.2.1` SDK contract. Applications pin one exact npm
23
23
  version and lockfile; registry state is deliberately not baked into immutable
24
24
  application artifacts.
25
25
 
@@ -132,7 +132,7 @@ uses the same controller for automatic memory extraction.
132
132
  Create a new repository-shaped Arcane application with the exact stable SDK:
133
133
 
134
134
  ```bash
135
- npx arcane-os@0.2.0 new my-app --path ./my-app --target portable --git
135
+ npx arcane-os@0.2.1 new my-app --path ./my-app --target portable --git
136
136
  cd my-app
137
137
  npm install
138
138
  npm run check
@@ -143,7 +143,7 @@ To enroll an existing repository, install the exact SDK and initialize only
143
143
  missing Arcane files:
144
144
 
145
145
  ```bash
146
- npm install --save-dev --save-exact arcane-os@0.2.0
146
+ npm install --save-dev --save-exact arcane-os@0.2.1
147
147
  npm exec -- arcane init my-app --target portable
148
148
  ```
149
149
 
@@ -159,7 +159,7 @@ npm exec -- arcane-os targets
159
159
  No global SDK install or standalone Arcane CLI is required. The application
160
160
  repository's exact npm dependency and lockfile own the CLI and toolchain version.
161
161
 
162
- Use `npx arcane-os@0.2.0` for the initial bootstrap because it names this npm
162
+ Use `npx arcane-os@0.2.1` for the initial bootstrap because it names this npm
163
163
  package explicitly; bare `npx arcane` outside an installed project could resolve
164
164
  a different package. Both installed commands invoke the same headless toolchain.
165
165
  Project-local npm scripts use the SDK pinned by that app's `package-lock.json`,
@@ -180,7 +180,7 @@ node ./bin/arcane.mjs new local-app --path ../local-app --target portable --git
180
180
 
181
181
  # From the generated app repository
182
182
  cd ../local-app
183
- npm install --save-dev --save-exact ../arcane-os-sdk/arcane-os-0.2.0.tgz
183
+ npm install --save-dev --save-exact ../arcane-os-sdk/arcane-os-0.2.1.tgz
184
184
  npm run check
185
185
  npm ci
186
186
  ```
@@ -191,7 +191,7 @@ still verifies the installed package name and exact version, the locked runtime
191
191
  identity, and the runtime bytes. Local directory `file:` dependencies are not
192
192
  accepted because npm may install them as links; use a packed `.tgz`. A GitHub
193
193
  runner also needs that tarball at the locked path. After publication, replace
194
- the local declaration with the exact `arcane-os@0.2.0` registry package and
194
+ the local declaration with the exact `arcane-os@0.2.1` registry package and
195
195
  commit the regenerated lock.
196
196
 
197
197
  Generated repositories use `npm ci --ignore-scripts` in CI. Run dependency
@@ -304,7 +304,7 @@ recorded length through a final identity check; an appended byte, concurrent
304
304
  growth, path replacement, or hard link fails closed. NFC paths use defined
305
305
  UTF-8 byte ordering, covered by one pinned golden bundle digest on every
306
306
  supported Node/runner combination. This SDK accepts only the explicitly listed
307
- `0.2.0` bundle generation; structural validity does not imply cross-SDK
307
+ `0.2.1` bundle generation; structural validity does not imply cross-SDK
308
308
  compatibility, and a release with zero payload bytes cannot be created. Portable
309
309
  path validation rejects file/directory prefix conflicts, case-colliding prefix
310
310
  spellings, and Windows device aliases including superscript COM/LPT digits.
@@ -350,7 +350,7 @@ package installation, or assertions.
350
350
 
351
351
  ## Current target support
352
352
 
353
- Version `0.2.0` exposes one browser target and five explicitly paired
353
+ Version `0.2.1` exposes one browser target and five explicitly paired
354
354
  native development targets: a verified non-runnable portable directory, a
355
355
  Windows x64 unsigned-local-test EXE bundle, Linux x64 and Linux ARM64
356
356
  unsigned-local-test DEBs, and an Android development-signed APK. The
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "builder": "arcane-sdk-browser-runtime-v1",
4
- "sdkVersion": "0.2.0",
4
+ "sdkVersion": "0.2.1",
5
5
  "source": {
6
6
  "authority": "arcane-os-sdk",
7
7
  "repository": "https://github.com/TheWizardNexus/arcane-os-sdk.git",
@@ -29,8 +29,8 @@
29
29
  ]
30
30
  },
31
31
  "fileCount": 25,
32
- "totalBytes": 9269048,
33
- "contentSha256": "d965eaf3ccf12593ae2628e7549794f4efbf1aa4cf56a39f51cc3869f0852671",
32
+ "totalBytes": 9279974,
33
+ "contentSha256": "a9715c4b3aef70ec4042e4738089568d6588877b29582e0f758ed83897b6814f",
34
34
  "files": [
35
35
  {
36
36
  "path": "ai/ARCANE_AI_BROWSER_WASM_COMPONENTS.json",
@@ -57,8 +57,8 @@
57
57
  "path": "ai/browser-speech-providers.mjs",
58
58
  "sourcePath": "browser-runtime/ai/browser-speech-providers.mjs",
59
59
  "provenance": "sdk-source-identity",
60
- "bytes": 15056,
61
- "sha256": "1eac35e1ca895e8862853a63def2cbf275fbbfe9ee6b1d36573a8dfa5f8924c6"
60
+ "bytes": 25982,
61
+ "sha256": "65e0a8da2bcd46b11c3ea2ca496105875977decc53d4e4c94ca56949dbb569b1"
62
62
  },
63
63
  {
64
64
  "path": "ai/browser-speech.mjs",
@@ -15,6 +15,9 @@ import {
15
15
 
16
16
  const AI_PROVIDER_PROTOCOL = "arcane-ai-provider/2";
17
17
  const ROLE_OPERATION = Object.freeze({ stt: "transcribe", tts: "synthesize" });
18
+ const STT_SAMPLE_RATE = 16_000;
19
+ const TTS_SAMPLE_RATE = 24_000;
20
+ const TTS_RESPONSE_FORMAT = "wav";
18
21
  const WORKER_FAILURE_CODES = new Set([
19
22
  "ARCANE_AI_WORKER_CRASHED",
20
23
  "ARCANE_AI_WORKER_MESSAGE_ERROR",
@@ -82,23 +85,237 @@ function publicStatus({ role, id, authority, state, busy, generation, errorCode,
82
85
  });
83
86
  }
84
87
 
85
- function cloneRequestPayload(role, payload, authority) {
86
- if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
87
- throw providerError("ARCANE_AI_INVALID_REQUEST", "Speech request payload must be an object.");
88
+ function assertRequestAuthority(context, authority, providerId) {
89
+ if (!Object.hasOwn(context, "selection")) return;
90
+ const selection = context.selection;
91
+ if (selection?.providerId !== providerId
92
+ || selection?.modelId !== authority.modelId
93
+ || selection?.localOnly === false) {
94
+ throw providerError(
95
+ "ARCANE_AI_MODEL_AUTHORITY_REQUIRED",
96
+ "The browser speech request selection changed from its immutable provider authority.",
97
+ );
88
98
  }
89
- if (role === "stt") {
90
- if (!(payload.audio instanceof Float32Array) || payload.sampleRate !== 16_000) {
99
+ }
100
+
101
+ function assertPayloadModel(payload, authority, { required = false } = {}) {
102
+ if (!Object.hasOwn(payload, "model")) {
103
+ if (!required) return;
104
+ throw providerError(
105
+ "ARCANE_AI_MODEL_AUTHORITY_REQUIRED",
106
+ "The shared speech request must identify the provider's immutable model authority.",
107
+ );
108
+ }
109
+ const descriptor = Object.getOwnPropertyDescriptor(payload, "model");
110
+ if (!Object.hasOwn(descriptor ?? {}, "value") || descriptor.value !== authority.modelId) {
111
+ throw providerError(
112
+ "ARCANE_AI_MODEL_AUTHORITY_REQUIRED",
113
+ "The shared speech request model does not match this provider authority.",
114
+ );
115
+ }
116
+ }
117
+
118
+ function genericPayloadDescriptors(payload, allowedKeys, requiredKeys, label) {
119
+ const prototype = Object.getPrototypeOf(payload);
120
+ if (prototype !== Object.prototype && prototype !== null) {
121
+ throw providerError("ARCANE_AI_INVALID_REQUEST", `${label} must be a plain object.`);
122
+ }
123
+ const descriptors = Object.getOwnPropertyDescriptors(payload);
124
+ for (const key of Reflect.ownKeys(descriptors)) {
125
+ if (typeof key === "symbol" || !allowedKeys.includes(key)) {
126
+ throw providerError("ARCANE_AI_INVALID_REQUEST", `${label} contains an unknown field.`);
127
+ }
128
+ if (!Object.hasOwn(descriptors[key], "value")) {
129
+ throw providerError("ARCANE_AI_INVALID_REQUEST", `${label}.${key} must be a data property.`);
130
+ }
131
+ }
132
+ for (const key of requiredKeys) {
133
+ if (!Object.hasOwn(descriptors, key)) {
134
+ throw providerError("ARCANE_AI_INVALID_REQUEST", `${label}.${key} is required.`);
135
+ }
136
+ }
137
+ return descriptors;
138
+ }
139
+
140
+ function audioMimeEssence(value, label) {
141
+ if (typeof value !== "string") {
142
+ throw providerError("ARCANE_AI_INVALID_REQUEST", `${label} must be an audio MIME type.`);
143
+ }
144
+ const essence = value.split(";", 1)[0].trim().toLowerCase();
145
+ if (!/^audio\/[a-z0-9!#$%&'*+.^_`|~-]+$/u.test(essence)) {
146
+ throw providerError("ARCANE_AI_INVALID_REQUEST", `${label} must be an audio MIME type.`);
147
+ }
148
+ return essence;
149
+ }
150
+
151
+ function awaitAbortableSpeechOperation(operation, signal) {
152
+ const observed = Promise.resolve(operation);
153
+ if (!signal) return observed;
154
+ if (signal.aborted) {
155
+ // Blob and Web Audio operations cannot be preempted; retain their eventual
156
+ // rejection after cancellation instead of leaving an unobserved promise.
157
+ observed.catch(function retainCancelledSpeechOperation() {});
158
+ return Promise.reject(abortError(signal));
159
+ }
160
+ return new Promise((resolve, reject) => {
161
+ let settled = false;
162
+ const release = () => signal.removeEventListener("abort", cancel);
163
+ const settle = (callback, value) => {
164
+ if (settled) return;
165
+ settled = true;
166
+ release();
167
+ callback(value);
168
+ };
169
+ const cancel = () => settle(reject, abortError(signal));
170
+ signal.addEventListener("abort", cancel, { once: true });
171
+ observed.then(
172
+ (value) => settle(resolve, value),
173
+ (error) => settle(reject, error),
174
+ );
175
+ });
176
+ }
177
+
178
+ async function decodeSharedTranscriptionPayload(payload, authority, signal) {
179
+ const descriptors = genericPayloadDescriptors(
180
+ payload,
181
+ ["audio", "mimeType", "model"],
182
+ ["audio", "mimeType", "model"],
183
+ "Shared speech transcription payload",
184
+ );
185
+ assertPayloadModel(payload, authority, { required: true });
186
+ const audio = descriptors.audio.value;
187
+ if (typeof Blob !== "function" || !(audio instanceof Blob) || audio.size < 1) {
188
+ throw providerError(
189
+ "ARCANE_AI_INVALID_REQUEST",
190
+ "Shared speech transcription requires a nonempty audio Blob or File.",
191
+ );
192
+ }
193
+ const mimeType = audioMimeEssence(descriptors.mimeType.value, "Speech transcription mimeType");
194
+ if (audio.type && audioMimeEssence(audio.type, "Speech transcription Blob.type") !== mimeType) {
195
+ throw providerError(
196
+ "ARCANE_AI_INVALID_REQUEST",
197
+ "Speech transcription mimeType does not match the audio Blob type.",
198
+ );
199
+ }
200
+ const OfflineAudioContext = globalThis.OfflineAudioContext
201
+ ?? globalThis.webkitOfflineAudioContext;
202
+ if (typeof OfflineAudioContext !== "function") {
203
+ throw providerError(
204
+ "ARCANE_AI_AUDIO_DECODE_UNAVAILABLE",
205
+ "Shared Blob transcription requires OfflineAudioContext decoding at 16000 Hz.",
206
+ );
207
+ }
208
+ let decoder;
209
+ try {
210
+ decoder = new OfflineAudioContext(1, 1, STT_SAMPLE_RATE);
211
+ } catch (error) {
212
+ throw providerError(
213
+ "ARCANE_AI_AUDIO_DECODE_UNAVAILABLE",
214
+ "Unable to create the required 16000 Hz speech decoder.",
215
+ error,
216
+ );
217
+ }
218
+ if (typeof decoder.decodeAudioData !== "function") {
219
+ throw providerError(
220
+ "ARCANE_AI_AUDIO_DECODE_UNAVAILABLE",
221
+ "Shared Blob transcription requires browser audio decoding.",
222
+ );
223
+ }
224
+ let decoded;
225
+ try {
226
+ const encoded = await awaitAbortableSpeechOperation(audio.arrayBuffer(), signal);
227
+ throwIfAborted(signal);
228
+ decoded = await awaitAbortableSpeechOperation(decoder.decodeAudioData(encoded), signal);
229
+ throwIfAborted(signal);
230
+ } catch (error) {
231
+ if (signal?.aborted && error?.code === "ARCANE_AI_REQUEST_ABORTED") throw error;
232
+ throw providerError(
233
+ "ARCANE_AI_AUDIO_DECODE_FAILED",
234
+ "The browser could not decode the supplied speech audio.",
235
+ error,
236
+ );
237
+ }
238
+ if (decoded?.sampleRate !== STT_SAMPLE_RATE
239
+ || !Number.isSafeInteger(decoded.length)
240
+ || decoded.length < 1
241
+ || !Number.isSafeInteger(decoded.numberOfChannels)
242
+ || decoded.numberOfChannels < 1
243
+ || typeof decoded.getChannelData !== "function") {
244
+ throw providerError(
245
+ "ARCANE_AI_AUDIO_DECODE_FAILED",
246
+ "Decoded speech audio must provide nonempty 16000 Hz channel data.",
247
+ );
248
+ }
249
+ const channels = [];
250
+ for (let index = 0; index < decoded.numberOfChannels; index += 1) {
251
+ const channel = decoded.getChannelData(index);
252
+ if (!(channel instanceof Float32Array) || channel.length !== decoded.length) {
91
253
  throw providerError(
92
- "ARCANE_AI_INVALID_REQUEST",
93
- "Whisper requires Float32Array audio sampled at exactly 16000 Hz.",
254
+ "ARCANE_AI_AUDIO_DECODE_FAILED",
255
+ "Decoded speech audio returned invalid channel data.",
94
256
  );
95
257
  }
96
- return Object.freeze({
258
+ channels.push(channel);
259
+ }
260
+ const mono = new Float32Array(decoded.length);
261
+ for (let index = 0; index < mono.length; index += 1) {
262
+ let sample = 0;
263
+ for (const channel of channels) sample += channel[index];
264
+ sample /= channels.length;
265
+ if (!Number.isFinite(sample)) {
266
+ throw providerError(
267
+ "ARCANE_AI_AUDIO_DECODE_FAILED",
268
+ "Decoded speech audio contains a non-finite sample.",
269
+ );
270
+ }
271
+ mono[index] = sample;
272
+ }
273
+ throwIfAborted(signal);
274
+ return Object.freeze({
275
+ payload: Object.freeze({ audio: mono, sampleRate: STT_SAMPLE_RATE }),
276
+ shared: true,
277
+ });
278
+ }
279
+
280
+ function cloneNativeTranscriptionPayload(payload, authority) {
281
+ assertPayloadModel(payload, authority);
282
+ if (!(payload.audio instanceof Float32Array) || payload.sampleRate !== STT_SAMPLE_RATE) {
283
+ throw providerError(
284
+ "ARCANE_AI_INVALID_REQUEST",
285
+ "Whisper requires Float32Array audio sampled at exactly 16000 Hz.",
286
+ );
287
+ }
288
+ return Object.freeze({
289
+ payload: Object.freeze({
97
290
  audio: new Float32Array(payload.audio),
98
- sampleRate: 16_000,
99
- });
291
+ sampleRate: STT_SAMPLE_RATE,
292
+ }),
293
+ shared: false,
294
+ });
295
+ }
296
+
297
+ function normalizeSynthesisPayload(payload, authority) {
298
+ const shared = Object.hasOwn(payload, "input");
299
+ let textValue = payload.text;
300
+ if (shared) {
301
+ const descriptors = genericPayloadDescriptors(
302
+ payload,
303
+ ["model", "voice", "input", "responseFormat", "speed"],
304
+ ["model", "input", "responseFormat"],
305
+ "Shared speech synthesis payload",
306
+ );
307
+ assertPayloadModel(payload, authority, { required: true });
308
+ if (descriptors.responseFormat.value !== TTS_RESPONSE_FORMAT) {
309
+ throw providerError(
310
+ "ARCANE_AI_UNSUPPORTED_RESPONSE_FORMAT",
311
+ "Browser Kokoro supports only wav responses for shared speech requests.",
312
+ );
313
+ }
314
+ textValue = descriptors.input.value;
315
+ } else {
316
+ assertPayloadModel(payload, authority);
100
317
  }
101
- const text = typeof payload.text === "string" ? payload.text.trim() : "";
318
+ const text = typeof textValue === "string" ? textValue.trim() : "";
102
319
  const voice = typeof (payload.voice ?? authority.defaultVoice) === "string"
103
320
  ? (payload.voice ?? authority.defaultVoice).trim()
104
321
  : "";
@@ -109,7 +326,75 @@ function cloneRequestPayload(role, payload, authority) {
109
326
  "Kokoro requires text, a voice, and speed greater than 0 and at most 4.",
110
327
  );
111
328
  }
112
- return Object.freeze({ text, voice, speed });
329
+ return Object.freeze({
330
+ payload: Object.freeze({ text, voice, speed }),
331
+ shared,
332
+ });
333
+ }
334
+
335
+ async function normalizeRequestPayload(role, payload, authority, signal) {
336
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
337
+ throw providerError("ARCANE_AI_INVALID_REQUEST", "Speech request payload must be an object.");
338
+ }
339
+ if (role === "stt") {
340
+ const audio = Object.getOwnPropertyDescriptor(payload, "audio");
341
+ if (Object.hasOwn(audio ?? {}, "value")
342
+ && typeof Blob === "function"
343
+ && audio.value instanceof Blob) {
344
+ return decodeSharedTranscriptionPayload(payload, authority, signal);
345
+ }
346
+ return cloneNativeTranscriptionPayload(payload, authority);
347
+ }
348
+ return normalizeSynthesisPayload(payload, authority);
349
+ }
350
+
351
+ function encodeSharedSynthesisResult(result) {
352
+ if (!(result?.audio instanceof Float32Array)
353
+ || result.sampleRate !== TTS_SAMPLE_RATE
354
+ || result.audio.length < 1
355
+ || result.audio.length > (0xffffffff - 44) / 2) {
356
+ throw providerError(
357
+ "ARCANE_AI_INVALID_PROVIDER_RESULT",
358
+ "Browser Kokoro must return nonempty 24000 Hz Float32 PCM.",
359
+ );
360
+ }
361
+ const buffer = new ArrayBuffer(44 + result.audio.length * 2);
362
+ const bytes = new Uint8Array(buffer);
363
+ const view = new DataView(buffer);
364
+ const writeText = (offset, value) => {
365
+ for (let index = 0; index < value.length; index += 1) {
366
+ view.setUint8(offset + index, value.charCodeAt(index));
367
+ }
368
+ };
369
+ writeText(0, "RIFF");
370
+ view.setUint32(4, buffer.byteLength - 8, true);
371
+ writeText(8, "WAVE");
372
+ writeText(12, "fmt ");
373
+ view.setUint32(16, 16, true);
374
+ view.setUint16(20, 1, true);
375
+ view.setUint16(22, 1, true);
376
+ view.setUint32(24, TTS_SAMPLE_RATE, true);
377
+ view.setUint32(28, TTS_SAMPLE_RATE * 2, true);
378
+ view.setUint16(32, 2, true);
379
+ view.setUint16(34, 16, true);
380
+ writeText(36, "data");
381
+ view.setUint32(40, result.audio.length * 2, true);
382
+ for (let index = 0; index < result.audio.length; index += 1) {
383
+ const sample = result.audio[index];
384
+ if (!Number.isFinite(sample)) {
385
+ throw providerError(
386
+ "ARCANE_AI_INVALID_PROVIDER_RESULT",
387
+ "Browser Kokoro returned a non-finite PCM sample.",
388
+ );
389
+ }
390
+ const clamped = Math.max(-1, Math.min(1, sample));
391
+ view.setInt16(
392
+ 44 + index * 2,
393
+ Math.round(clamped < 0 ? clamped * 0x8000 : clamped * 0x7fff),
394
+ true,
395
+ );
396
+ }
397
+ return Object.freeze({ audio: bytes, contentType: "audio/wav" });
113
398
  }
114
399
 
115
400
  function createBrowserSpeechProvider({
@@ -152,6 +437,13 @@ function createBrowserSpeechProvider({
152
437
  throw new TypeError("Browser speech authority construction failed.");
153
438
  }
154
439
  const operation = ROLE_OPERATION[role];
440
+ const speech = role === "stt"
441
+ ? Object.freeze({ inputSampleRate: STT_SAMPLE_RATE })
442
+ : Object.freeze({
443
+ outputSampleRate: TTS_SAMPLE_RATE,
444
+ responseFormats: Object.freeze([TTS_RESPONSE_FORMAT]),
445
+ defaultResponseFormat: TTS_RESPONSE_FORMAT,
446
+ });
155
447
  const catalogEntry = Object.freeze({
156
448
  id: authority.modelId,
157
449
  providerId,
@@ -161,6 +453,7 @@ function createBrowserSpeechProvider({
161
453
  revision: authority.revision,
162
454
  runtime: authority.runtime,
163
455
  files: authority.files,
456
+ speech,
164
457
  });
165
458
  let state = "unloaded";
166
459
  let errorCode = null;
@@ -362,6 +655,7 @@ function createBrowserSpeechProvider({
362
655
  if (context.role !== role || context.operation !== operation) {
363
656
  throw providerError("ARCANE_AI_INVALID_REQUEST", `Browser ${role} supports only ${operation}.`);
364
657
  }
658
+ assertRequestAuthority(context, authority, providerId);
365
659
  const externalSignal = context.signal ?? null;
366
660
  throwIfAborted(externalSignal);
367
661
  if (state !== "ready" || !active) {
@@ -370,10 +664,6 @@ function createBrowserSpeechProvider({
370
664
  if (requestOperation) {
371
665
  throw providerError("ARCANE_AI_PROVIDER_BUSY", "The browser speech provider is already processing a request.");
372
666
  }
373
- const payload = cloneRequestPayload(role, context.payload, {
374
- defaultVoice: authority.defaultVoice,
375
- });
376
- throwIfAborted(externalSignal);
377
667
  const linked = linkSignal(externalSignal);
378
668
  if (linked.controller.signal.aborted) {
379
669
  linked.release();
@@ -381,21 +671,36 @@ function createBrowserSpeechProvider({
381
671
  }
382
672
  const slot = active;
383
673
  const requestGeneration = generation;
384
- const promise = slot.client.request("use", payload, {
385
- signal: linked.controller.signal,
386
- });
674
+ let workerRequestStarted = false;
675
+ const promise = (async () => {
676
+ const normalized = await normalizeRequestPayload(
677
+ role,
678
+ context.payload,
679
+ authority,
680
+ linked.controller.signal,
681
+ );
682
+ throwIfAborted(linked.controller.signal);
683
+ workerRequestStarted = true;
684
+ const result = await slot.client.request("use", normalized.payload, {
685
+ signal: linked.controller.signal,
686
+ });
687
+ if (requestGeneration !== generation || active !== slot) {
688
+ throw providerError("ARCANE_AI_OPERATION_SUPERSEDED", "The browser speech result was superseded.");
689
+ }
690
+ return role === "tts" && normalized.shared
691
+ ? encodeSharedSynthesisResult(result)
692
+ : result;
693
+ })();
387
694
  requestOperation = Object.freeze({
388
695
  promise,
389
696
  abort: () => linked.controller.abort(),
390
697
  });
391
698
  try {
392
- const result = await promise;
393
- if (requestGeneration !== generation || active !== slot) {
394
- throw providerError("ARCANE_AI_OPERATION_SUPERSEDED", "The browser speech result was superseded.");
395
- }
396
- return result;
699
+ return await promise;
397
700
  } catch (error) {
398
- if (error?.code === "ARCANE_AI_REQUEST_ABORTED" && active === slot) {
701
+ if (error?.code === "ARCANE_AI_REQUEST_ABORTED"
702
+ && workerRequestStarted
703
+ && active === slot) {
399
704
  active = null;
400
705
  releaseSlot(slot);
401
706
  state = "unloaded";
@@ -200,7 +200,7 @@ length with an EOF growth probe, and rechecked by handle and pathname identity.
200
200
  Every cumulative path prefix has one case-folded spelling and one file/directory
201
201
  kind; prefix topology conflicts and the complete portable Windows device-name
202
202
  set fail before creation or admission.
203
- The current SDK admits only the explicitly compatible `0.2.0` bundle
203
+ The current SDK admits only the explicitly compatible `0.2.1` bundle
204
204
  generation and rejects zero-byte payload releases.
205
205
 
206
206
  Promotion retains any prior output as an identity-bound backup until the new
@@ -320,7 +320,7 @@ through SDK-bound verified readers rather than accepting a mutable source path
320
320
  as authority. Build completion requires provider verification, and later
321
321
  verify/run calls receive the exact artifact receipt.
322
322
 
323
- The SDK `0.2.0` runtime requires Arcane `0.8.12` or newer. Compatibility
323
+ The SDK `0.2.1` runtime requires Arcane `0.8.12` or newer. Compatibility
324
324
  is contractual rather than exact-version pinning: the prepared Core must meet
325
325
  the highest minimum declared by the runtime, selected app, and bundled app
326
326
  dependencies; keep each app's Arcane protocol generation; and provide every
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-os",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Arcane OS JavaScript SDK, project-local CLI, browser runtime, and repository-portable application packager.",
5
5
  "type": "module",
6
6
  "main": "./src/index.mjs",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "builder": "arcane-sdk-runtime-v1",
4
- "sdkVersion": "0.2.0",
4
+ "sdkVersion": "0.2.1",
5
5
  "source": {
6
6
  "authority": "sdk-canonical",
7
7
  "repository": "https://github.com/TheWizardNexus/arcane-os-sdk.git",
@@ -14,8 +14,8 @@
14
14
  }
15
15
  },
16
16
  "fileCount": 160,
17
- "totalBytes": 3549207,
18
- "contentSha256": "fa3bcddb5479817971a400049dbeff8431c6499d708e9a8b714176715c5a015a",
17
+ "totalBytes": 3605154,
18
+ "contentSha256": "5b921d50b6a0cf36a13f7a7dedf96cd3a68104a322e5139136fd4197aa1ca7cb",
19
19
  "files": [
20
20
  {
21
21
  "path": "arcane/components/app-bar.html",
@@ -39,8 +39,8 @@
39
39
  },
40
40
  {
41
41
  "path": "arcane/components/chat.html",
42
- "bytes": 52132,
43
- "sha256": "42d4769a074e70b5c2768e850eaf29a25c93ee9e8734724b72b2ad585e19b2ca"
42
+ "bytes": 61650,
43
+ "sha256": "d644848fbc3ccc85d491db7ada75231f393a371dd3a0ccd92f223c172571e47b"
44
44
  },
45
45
  {
46
46
  "path": "arcane/components/conversation-view.html",
@@ -374,8 +374,8 @@
374
374
  },
375
375
  {
376
376
  "path": "arcane/modules/AI.js",
377
- "bytes": 81061,
378
- "sha256": "8e34445ad394064dae970583147b635de81d878a6caebb8a60d64b34dbc9e658"
377
+ "bytes": 103733,
378
+ "sha256": "3a051d67b1e895b7c6f14af36a34daf1d92fe70e02c95c1c4ffd80cc7c622561"
379
379
  },
380
380
  {
381
381
  "path": "arcane/modules/AIPreferenceRuntime.js",
@@ -499,8 +499,8 @@
499
499
  },
500
500
  {
501
501
  "path": "arcane/modules/ConfiguredAIChatSession.js",
502
- "bytes": 21462,
503
- "sha256": "12e02b169be6867d82ef8be75bcb1c2b367e778609e8d065389ed109d896c4c8"
502
+ "bytes": 24501,
503
+ "sha256": "f95c3421bf07397e987f77fc3d02166cf780059dd3d6de90ceaded990752f51b"
504
504
  },
505
505
  {
506
506
  "path": "arcane/modules/ConversationActionItems.js",
@@ -534,8 +534,8 @@
534
534
  },
535
535
  {
536
536
  "path": "arcane/modules/DBOPFSDocumentLibrary.js",
537
- "bytes": 29977,
538
- "sha256": "4c551b5917d1577b8b46b08457cf762d7ab7548222abd6f4a72c7c7ab5c697b7"
537
+ "bytes": 50695,
538
+ "sha256": "2831470ce54c3393595bf068d60c2e774804cbd51dba7491ad734bbfbea7f32f"
539
539
  },
540
540
  {
541
541
  "path": "arcane/modules/DBOPFSWorker.js",