arcane-os 0.5.10 → 0.5.12

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 (54) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +117 -26
  3. package/browser-runtime/ai/browser-speech-providers.mjs +1 -1
  4. package/docs/architecture.md +303 -0
  5. package/docs/compatibility.md +38 -0
  6. package/docs/event-manager.md +263 -0
  7. package/docs/platform-targets.md +104 -0
  8. package/docs/publishing.md +126 -0
  9. package/docs/reference/README.md +206 -0
  10. package/docs/reference/ai/browser-speech.md +879 -0
  11. package/docs/reference/ai/browser-wasm.md +637 -0
  12. package/docs/reference/ai/twin-cloud.md +156 -0
  13. package/docs/reference/arcane-ollama.md +288 -0
  14. package/docs/reference/availability-and-normalization.md +224 -0
  15. package/docs/reference/behavioral-testing.md +129 -0
  16. package/docs/reference/cli.md +820 -0
  17. package/docs/reference/core/README.md +61 -0
  18. package/docs/reference/core/arcane-ai-contracts.md +906 -0
  19. package/docs/reference/core/arcane-api.md +601 -0
  20. package/docs/reference/core/arcane-entities.md +59 -0
  21. package/docs/reference/core/arcane-events.md +134 -0
  22. package/docs/reference/core/ollama-module.md +181 -0
  23. package/docs/reference/core/reference/arcane-api/ai-and-ollama.md +1909 -0
  24. package/docs/reference/core/reference/arcane-api/applications-terminal-capabilities.md +1057 -0
  25. package/docs/reference/core/reference/arcane-api/core-and-events.md +320 -0
  26. package/docs/reference/core/reference/arcane-api/filesystem-storage-preferences-appearance.md +610 -0
  27. package/docs/reference/core/reference/arcane-api/namespaces.md +1157 -0
  28. package/docs/reference/core/reference/arcane-api/platform-installation-users-system.md +1423 -0
  29. package/docs/reference/core/reference/arcane-api/session-provisioning-diagnostics-development.md +315 -0
  30. package/docs/reference/event-manager.md +1409 -0
  31. package/docs/reference/inventory/package-api.json +3194 -0
  32. package/docs/reference/inventory/runtime-components.json +1015 -0
  33. package/docs/reference/inventory/runtime-entities.json +25 -0
  34. package/docs/reference/inventory/runtime-modules.json +1367 -0
  35. package/docs/reference/mail.md +309 -0
  36. package/docs/reference/protocols.md +749 -0
  37. package/docs/reference/runtime-components.md +1532 -0
  38. package/docs/reference/runtime-entities.md +305 -0
  39. package/docs/reference/runtime-modules.md +3310 -0
  40. package/docs/reference/sdk-api.md +6733 -0
  41. package/docs/roadmap.md +79 -0
  42. package/docs/work-amplification.md +66 -0
  43. package/examples/wasm-ai-demo/README.md +80 -0
  44. package/examples/wasm-ai-demo/app.js +787 -0
  45. package/examples/wasm-ai-demo/index.html +343 -0
  46. package/examples/wasm-ai-demo/profile-tools.js +217 -0
  47. package/examples/wasm-ai-demo/profiles/BOSS.Modelfile +106 -0
  48. package/examples/wasm-ai-demo/profiles/PreCrisis.Modelfile +693 -0
  49. package/examples/wasm-ai-demo/rag/boss-library.json +3006 -0
  50. package/examples/wasm-ai-demo/rag.js +295 -0
  51. package/examples/wasm-ai-demo/server.mjs +71 -0
  52. package/package.json +10 -1
  53. package/runtime/arcane/modules/AI.js +60 -11
  54. package/runtime/arcane/modules/AIProviderRuntime.js +26 -5
@@ -0,0 +1,295 @@
1
+ import DBOPFSDocumentLibrary from "arcane/DBOPFSDocumentLibrary";
2
+
3
+ const DEFAULT_BOSS_BUNDLE_URL = "./rag/boss-library.json";
4
+ const PROFILE_IDS = new Set(["general", "precrisis", "boss"]);
5
+ const libraryEntries = new WeakMap();
6
+
7
+ function normalizeProfileId(profileId) {
8
+ const normalized = String(profileId || "general").trim().toLowerCase();
9
+ if (!PROFILE_IDS.has(normalized)) {
10
+ throw new TypeError(`Unsupported example profile: ${normalized || "empty"}.`);
11
+ }
12
+ return normalized;
13
+ }
14
+
15
+ function reportStatus(onStatus, message) {
16
+ if (typeof onStatus === "function") onStatus(message);
17
+ }
18
+
19
+ function entryMap(dbopfs) {
20
+ let entries = libraryEntries.get(dbopfs);
21
+ if (!entries) {
22
+ entries = new Map();
23
+ libraryEntries.set(dbopfs, entries);
24
+ }
25
+ return entries;
26
+ }
27
+
28
+ function schemaFor(profileId, version = "1") {
29
+ return {
30
+ id: `wasm-ai-demo-${profileId}`,
31
+ table: `wasm_ai_demo_${profileId}_documents`,
32
+ version: String(version),
33
+ };
34
+ }
35
+
36
+ function createLibrary(dbopfs, profileId, version) {
37
+ return new DBOPFSDocumentLibrary({
38
+ db: dbopfs,
39
+ schema: schemaFor(profileId, version),
40
+ });
41
+ }
42
+
43
+ function splitTerms(value) {
44
+ if (Array.isArray(value)) {
45
+ return value.map((entry) => String(entry).trim()).filter(Boolean);
46
+ }
47
+ return String(value || "")
48
+ .split(",")
49
+ .map((entry) => entry.trim())
50
+ .filter(Boolean);
51
+ }
52
+
53
+ function bossRecord(document, index, sourcePath) {
54
+ const record = document && typeof document === "object" && !Array.isArray(document)
55
+ ? document
56
+ : {};
57
+ const id = String(record.id || `boss-library-${index + 1}`).trim();
58
+ const title = String(record.title || id).trim();
59
+ const body = String(record.text ?? record.body ?? "");
60
+ const terms = splitTerms(record.topics);
61
+ return {
62
+ body,
63
+ id,
64
+ kind: "boss-library",
65
+ mediaType: "text/markdown",
66
+ path: id,
67
+ searchTerms: terms,
68
+ sourcePath,
69
+ tags: ["boss", ...terms],
70
+ title,
71
+ };
72
+ }
73
+
74
+ async function fetchBossBundle(bundleUrl) {
75
+ const response = await fetch(bundleUrl, { cache: "no-store" });
76
+ if (!response.ok) {
77
+ throw new Error(`Unable to load the BOSS library: HTTP ${response.status}.`);
78
+ }
79
+ const bundle = await response.json();
80
+ if (!bundle || typeof bundle !== "object" || !Array.isArray(bundle.documents)) {
81
+ throw new TypeError("The BOSS library response must contain a documents array.");
82
+ }
83
+ return bundle;
84
+ }
85
+
86
+ async function readCorpus(library, signal) {
87
+ try {
88
+ return { ...(await library.search("", { signal })), bootstrapped: true };
89
+ } catch (error) {
90
+ if (error?.code === "DBOPFS_DOCUMENT_NOT_BOOTSTRAPPED") {
91
+ return { bootstrapped: false, failures: [], matches: [], total: 0 };
92
+ }
93
+ throw error;
94
+ }
95
+ }
96
+
97
+ async function prepareLibrary({ dbopfs, bundleUrl, initialLibrary, onStatus, profileId, signal }) {
98
+ let version = "1";
99
+ let bossDocuments = [];
100
+ if (profileId === "boss") {
101
+ reportStatus(onStatus, "Loading the BOSS document catalog…");
102
+ const bundle = await fetchBossBundle(bundleUrl);
103
+ version = String(bundle.schema_version ?? "1");
104
+ bossDocuments = bundle.documents.map((document, index) => (
105
+ bossRecord(document, index, bundleUrl)
106
+ ));
107
+ }
108
+
109
+ const library = profileId === "boss"
110
+ ? createLibrary(dbopfs, profileId, version)
111
+ : initialLibrary;
112
+ const existing = await readCorpus(library, signal);
113
+ const userDocuments = existing.matches.filter((document) => document.kind === "user-import");
114
+ const hasBossDocuments = existing.matches.some((document) => document.kind === "boss-library");
115
+ const needsBootstrap = !existing.bootstrapped || (profileId === "boss" && !hasBossDocuments);
116
+
117
+ if (needsBootstrap) {
118
+ const documents = profileId === "boss"
119
+ ? [...bossDocuments, ...userDocuments]
120
+ : userDocuments;
121
+ reportStatus(
122
+ onStatus,
123
+ profileId === "boss"
124
+ ? "Storing the BOSS library through the Arcane document library…"
125
+ : "Opening the Arcane document library…",
126
+ );
127
+ await library.bootstrap({
128
+ files: documents,
129
+ signal,
130
+ onProgress(progress) {
131
+ if (progress?.phase !== "writing") return;
132
+ reportStatus(
133
+ onStatus,
134
+ `Storing local documents ${progress.completed} of ${progress.total}…`,
135
+ );
136
+ },
137
+ });
138
+ }
139
+
140
+ return library;
141
+ }
142
+
143
+ async function libraryFor(profileId, { dbopfs, bundleUrl, onStatus, signal } = {}) {
144
+ const profile = normalizeProfileId(profileId);
145
+ const db = dbopfs ?? globalThis.dbopfs;
146
+ const initialLibrary = createLibrary(db, profile, "1");
147
+ if (db.readyPromise) await db.readyPromise;
148
+ const entries = entryMap(db);
149
+ let entry = entries.get(profile);
150
+ if (!entry) {
151
+ entry = {
152
+ library: null,
153
+ ready: prepareLibrary({
154
+ dbopfs: db,
155
+ bundleUrl: bundleUrl || DEFAULT_BOSS_BUNDLE_URL,
156
+ initialLibrary,
157
+ onStatus,
158
+ profileId: profile,
159
+ signal,
160
+ }),
161
+ };
162
+ entries.set(profile, entry);
163
+ try {
164
+ entry.library = await entry.ready;
165
+ } catch (error) {
166
+ entries.delete(profile);
167
+ throw error;
168
+ }
169
+ } else if (!entry.library) {
170
+ entry.library = await entry.ready;
171
+ }
172
+ return entry.library;
173
+ }
174
+
175
+ function corpusStats(corpus) {
176
+ const documents = corpus.matches || [];
177
+ const bossCount = documents.filter((document) => document.kind === "boss-library").length;
178
+ const userCount = documents.filter((document) => document.kind === "user-import").length;
179
+ return {
180
+ boss: bossCount,
181
+ bossCount,
182
+ bossDocuments: bossCount,
183
+ failures: corpus.failures || [],
184
+ imported: userCount,
185
+ total: documents.length,
186
+ userCount,
187
+ userDocuments: userCount,
188
+ };
189
+ }
190
+
191
+ async function initializeRag({
192
+ dbopfs = globalThis.dbopfs,
193
+ bundleUrl = DEFAULT_BOSS_BUNDLE_URL,
194
+ onStatus,
195
+ profileId = "general",
196
+ signal,
197
+ } = {}) {
198
+ const library = await libraryFor(profileId, {
199
+ dbopfs,
200
+ bundleUrl,
201
+ onStatus,
202
+ signal,
203
+ });
204
+ const corpus = await library.search("", { signal });
205
+ reportStatus(onStatus, "Local Arcane document library ready.");
206
+ return corpusStats(corpus);
207
+ }
208
+
209
+ async function getRagStats(profileId = "general", {
210
+ dbopfs = globalThis.dbopfs,
211
+ signal,
212
+ } = {}) {
213
+ const library = await libraryFor(profileId, { dbopfs, signal });
214
+ return corpusStats(await library.search("", { signal }));
215
+ }
216
+
217
+ async function importedDocument(file, profileId) {
218
+ const fallbackId = `${Date.now()}-${Math.random()}`;
219
+ const id = `user-${profileId}-${globalThis.crypto?.randomUUID?.() || fallbackId}`;
220
+ const body = await file.text();
221
+ return {
222
+ body,
223
+ id,
224
+ kind: "user-import",
225
+ mediaType: file.type === "text/markdown" ? "text/markdown" : "text/plain",
226
+ path: file.name || id,
227
+ searchTerms: splitTerms(file.name),
228
+ sourcePath: file.name || id,
229
+ tags: [profileId, "user-import"],
230
+ title: file.name || "Imported document",
231
+ };
232
+ }
233
+
234
+ async function importRagFiles(files, {
235
+ dbopfs = globalThis.dbopfs,
236
+ onStatus,
237
+ profileId = "general",
238
+ signal,
239
+ } = {}) {
240
+ if (!Array.isArray(files)) throw new TypeError("RAG files must be an array.");
241
+ const profile = normalizeProfileId(profileId);
242
+ const library = await libraryFor(profile, { dbopfs, onStatus, signal });
243
+ const corpus = await library.search("", { signal });
244
+ const imported = [];
245
+ for (let index = 0; index < files.length; index += 1) {
246
+ if (signal?.aborted) throw signal.reason || new DOMException("Aborted", "AbortError");
247
+ const file = files[index];
248
+ if (!file || typeof file.text !== "function") {
249
+ throw new TypeError(`RAG file ${index + 1} is not a browser File.`);
250
+ }
251
+ reportStatus(onStatus, `Reading ${file.name || `document ${index + 1}`}…`);
252
+ imported.push(await importedDocument(file, profile));
253
+ }
254
+ await library.bootstrap({
255
+ files: [...corpus.matches, ...imported],
256
+ signal,
257
+ onProgress(progress) {
258
+ if (progress?.phase === "writing") {
259
+ reportStatus(
260
+ onStatus,
261
+ `Storing local documents ${progress.completed} of ${progress.total}…`,
262
+ );
263
+ }
264
+ },
265
+ });
266
+ return {
267
+ imported: imported.length,
268
+ stats: corpusStats(await library.search("", { signal })),
269
+ };
270
+ }
271
+
272
+ async function retrieveRagContext(query, {
273
+ dbopfs = globalThis.dbopfs,
274
+ profileId = "general",
275
+ signal,
276
+ } = {}) {
277
+ const profile = normalizeProfileId(profileId);
278
+ const library = await libraryFor(profile, { dbopfs, signal });
279
+ const result = await library.buildContext(String(query || ""), { signal });
280
+ const text = result.text && profile === "boss"
281
+ ? ["<boss_library_context>", result.text, "</boss_library_context>"].join("\n\n")
282
+ : result.text;
283
+ return {
284
+ failures: result.failures,
285
+ matches: result.documents,
286
+ text,
287
+ };
288
+ }
289
+
290
+ export {
291
+ getRagStats,
292
+ importRagFiles,
293
+ initializeRag,
294
+ retrieveRagContext,
295
+ };
@@ -0,0 +1,71 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { startSourceExampleServer } from "arcane-os";
5
+
6
+ const root = path.dirname(fileURLToPath(import.meta.url));
7
+ const sdkRoot = path.resolve(root, "..", "..");
8
+ const examplePath = "/examples/wasm-ai-demo";
9
+ const requestedPort = Number.parseInt(process.argv[2] || "4173", 10);
10
+ const port = Number.isFinite(requestedPort) ? requestedPort : 4173;
11
+ const host = process.argv[3] || "0.0.0.0";
12
+ const modelDirectory = process.env.ARCANE_WASM_MODEL_ROOT
13
+ ? path.resolve(process.env.ARCANE_WASM_MODEL_ROOT)
14
+ : path.join(root, "models");
15
+ const tlsDirectory = process.env.ARCANE_WASM_TLS_ROOT
16
+ ? path.resolve(process.env.ARCANE_WASM_TLS_ROOT)
17
+ : path.join(root, "tls");
18
+ const tlsKeyPath = path.join(tlsDirectory, "server-key.pem");
19
+ const tlsCertificatePath = path.join(tlsDirectory, "server.pem");
20
+ const tls = existsSync(tlsKeyPath) && existsSync(tlsCertificatePath)
21
+ ? {
22
+ key: readFileSync(tlsKeyPath),
23
+ cert: readFileSync(tlsCertificatePath),
24
+ }
25
+ : undefined;
26
+
27
+ const running = await startSourceExampleServer({
28
+ crossOriginIsolated: true,
29
+ host,
30
+ mounts: [
31
+ {
32
+ include: [
33
+ "index.html",
34
+ "app.js",
35
+ "rag.js",
36
+ "profile-tools.js",
37
+ "profiles/",
38
+ "rag/",
39
+ ],
40
+ index: "index.html",
41
+ root,
42
+ urlPath: examplePath,
43
+ },
44
+ {
45
+ root: modelDirectory,
46
+ urlPath: `${examplePath}/models`,
47
+ },
48
+ {
49
+ root: path.join(sdkRoot, "src"),
50
+ urlPath: "/src",
51
+ },
52
+ {
53
+ root: path.join(sdkRoot, "browser-runtime"),
54
+ urlPath: "/browser-runtime",
55
+ },
56
+ {
57
+ include: ["arcane/", "strong-type/"],
58
+ root: path.join(sdkRoot, "runtime"),
59
+ urlPath: "/runtime",
60
+ },
61
+ ],
62
+ port,
63
+ startPath: `${examplePath}/`,
64
+ tls,
65
+ });
66
+
67
+ console.log(`Arcane WASM Voice Chat listening on ${running.origin}`);
68
+ console.log(`Example URL: ${running.url}`);
69
+ console.log(`Live Arcane SDK source: ${sdkRoot}`);
70
+
71
+ await running.closed;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-os",
3
- "version": "0.5.10",
3
+ "version": "0.5.12",
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",
@@ -44,6 +44,15 @@
44
44
  "src/",
45
45
  "runtime/",
46
46
  "schemas/",
47
+ "docs/",
48
+ "examples/wasm-ai-demo/README.md",
49
+ "examples/wasm-ai-demo/index.html",
50
+ "examples/wasm-ai-demo/app.js",
51
+ "examples/wasm-ai-demo/server.mjs",
52
+ "examples/wasm-ai-demo/profile-tools.js",
53
+ "examples/wasm-ai-demo/rag.js",
54
+ "examples/wasm-ai-demo/profiles/",
55
+ "examples/wasm-ai-demo/rag/",
47
56
  "README.md",
48
57
  "CHANGELOG.md",
49
58
  "LICENSE",
@@ -23,7 +23,7 @@ const DEFAULT_TTS_SEGMENTATION={
23
23
  };
24
24
  const DEFAULT_BROWSER_TTS_EXECUTION={
25
25
  device:'auto',
26
- maxConcurrentRequests:2
26
+ maxConcurrentRequests:4
27
27
  };
28
28
  const BROWSER_TTS_EXECUTION_DEVICES=new Set(['auto','webgpu','wasm']);
29
29
  const MAX_BROWSER_TTS_CONCURRENT_REQUESTS=4;
@@ -5673,7 +5673,8 @@ class AI {
5673
5673
 
5674
5674
  streamTTS(
5675
5675
  text='',
5676
- end=false
5676
+ end=false,
5677
+ options={}
5677
5678
  ){
5678
5679
  if(this.muted){
5679
5680
  if(end){
@@ -5682,6 +5683,12 @@ class AI {
5682
5683
  return Promise.resolve(false);
5683
5684
  }
5684
5685
 
5686
+ const {voice,speed,waitForPlayback=false}=options;
5687
+ const pauseAfterMs=Number(options.pauseAfterMs??0);
5688
+ if(!Number.isFinite(pauseAfterMs)||pauseAfterMs<0){
5689
+ throw new RangeError('Speech pauses must be a nonnegative number of milliseconds.');
5690
+ }
5691
+
5685
5692
  this.audioMessageChunks+=String(text||'');
5686
5693
  const outputs=this.#extractSpeechSegments(end);
5687
5694
 
@@ -5702,8 +5709,13 @@ class AI {
5702
5709
  const generation=this.speechGeneration;
5703
5710
  const jobs=[];
5704
5711
 
5705
- for(const output of outputs){
5706
- jobs.push(this.#queueSpeechJob(output,generation));
5712
+ for(const [index,output] of outputs.entries()){
5713
+ jobs.push(this.#queueSpeechJob(output,generation,{
5714
+ voice,
5715
+ speed,
5716
+ waitForPlayback,
5717
+ pauseAfterMs:index===outputs.length-1?pauseAfterMs:0
5718
+ }));
5707
5719
  }
5708
5720
 
5709
5721
  return Promise.all(jobs).then(
@@ -5857,7 +5869,7 @@ class AI {
5857
5869
  return this.#speechAbbreviations.has(token);
5858
5870
  }
5859
5871
 
5860
- #queueSpeechJob(text,generation){
5872
+ #queueSpeechJob(text,generation,options={}){
5861
5873
  const job={
5862
5874
  abortController:null,
5863
5875
  audioBuffer:null,
@@ -5867,13 +5879,22 @@ class AI {
5867
5879
  scheduledStart:null,
5868
5880
  sourceNode:null,
5869
5881
  state:'queued',
5870
- text
5882
+ text,
5883
+ voice:options.voice,
5884
+ speed:options.speed,
5885
+ pauseAfterMs:options.pauseAfterMs??0,
5886
+ resolvePlayback:null
5871
5887
  };
5872
5888
  const runtime=this;
5889
+ const playback=options.waitForPlayback===true
5890
+ ?new Promise(function captureSpeechPlaybackCompletion(resolve){
5891
+ job.resolvePlayback=resolve;
5892
+ })
5893
+ :null;
5873
5894
 
5874
5895
  this.speechJobs.push(job);
5875
5896
 
5876
- return Promise.resolve().then(
5897
+ const preparation=Promise.resolve().then(
5877
5898
  function synthesizeAvailableSpeech(){
5878
5899
  return runtime.#prepareSpeechJob(job);
5879
5900
  }
@@ -5886,6 +5907,7 @@ class AI {
5886
5907
  );
5887
5908
  }
5888
5909
  );
5910
+ return playback||preparation;
5889
5911
  }
5890
5912
 
5891
5913
  async #prepareSpeechJob(job){
@@ -5914,16 +5936,18 @@ class AI {
5914
5936
  async #requestSpeechAudio(job){
5915
5937
  job.abortController=new AbortController();
5916
5938
  const selection=this.#providerRuntime.selection('tts');
5917
- const voice=selection?this.#providerSpeechVoice():null;
5939
+ const voice=job.voice===undefined
5940
+ ?(selection?this.#providerSpeechVoice():null)
5941
+ :job.voice;
5918
5942
  const response=await this.fetchTTS(
5919
5943
  {
5920
5944
  model:selection?.modelId||this.modelTTS,
5921
5945
  input:job.text,
5922
- ...(voice?{voice}:{}),
5946
+ ...(job.voice!==undefined||voice?{voice}:{}),
5923
5947
  responseFormat:selection
5924
5948
  ?this.#providerSpeechResponseFormat()
5925
5949
  :this.audioFormat,
5926
- speed:this.voiceSpeed
5950
+ speed:job.speed===undefined?this.voiceSpeed:job.speed
5927
5951
  },
5928
5952
  job.abortController.signal
5929
5953
  );
@@ -6294,6 +6318,8 @@ class AI {
6294
6318
  for(const job of this.speechJobs){
6295
6319
  job.abortController?.abort();
6296
6320
  job.state='cancelled';
6321
+ job.resolvePlayback?.(false);
6322
+ job.resolvePlayback=null;
6297
6323
 
6298
6324
  if(job.sourceNode){
6299
6325
  job.sourceNode.onended=null;
@@ -6370,6 +6396,22 @@ class AI {
6370
6396
  if(attempt===this.speechResumeAttempt){
6371
6397
  this.speechResumePending=false;
6372
6398
  }
6399
+ if(context?.state==='closed'){
6400
+ this.#clearSpeechUnlock();
6401
+ const jobs=this.speechJobs.filter(function usesClosedSpeechContext(job){
6402
+ return job.audioContext===context;
6403
+ });
6404
+ if(!jobs.length){
6405
+ this.#publishTTSFailure(error,{
6406
+ boundary:'playback-resume',
6407
+ generation:this.speechGeneration
6408
+ });
6409
+ }
6410
+ for(const job of jobs){
6411
+ this.#failSpeechJob(job,error,'playback-resume');
6412
+ }
6413
+ return false;
6414
+ }
6373
6415
  this.#waitForSpeechGesture(error,context);
6374
6416
  if(error?.name!=='NotAllowedError'){
6375
6417
  this.#publishTTSFailure(error,{
@@ -6543,7 +6585,7 @@ class AI {
6543
6585
  job.state='scheduled';
6544
6586
  job.scheduledStart=scheduledStart;
6545
6587
  job.scheduledEnd=hasKnownDuration
6546
- ?scheduledStart+duration
6588
+ ?scheduledStart+duration+(job.pauseAfterMs??0)/1000
6547
6589
  :null;
6548
6590
  job.sourceNode.__arcaneStarted=true;
6549
6591
  if(!this.currentSpeechJob){
@@ -6606,6 +6648,11 @@ class AI {
6606
6648
  job.sourceNode.onended=null;
6607
6649
  }
6608
6650
 
6651
+ if(job.scheduledEnd===null&&job.pauseAfterMs>0){
6652
+ this.speechScheduleContext=job.audioContext;
6653
+ this.speechScheduleTime=
6654
+ (Number(job.audioContext?.currentTime)||0)+job.pauseAfterMs/1000;
6655
+ }
6609
6656
  job.state='complete';
6610
6657
  this.#removeSpeechJob(job);
6611
6658
 
@@ -6727,6 +6774,8 @@ class AI {
6727
6774
  }
6728
6775
 
6729
6776
  #removeSpeechJob(job){
6777
+ job.resolvePlayback?.(job.state==='complete');
6778
+ job.resolvePlayback=null;
6730
6779
  const jobIndex=this.speechJobs.indexOf(job);
6731
6780
 
6732
6781
  if(jobIndex>=0){
@@ -2415,12 +2415,33 @@ export class AIProviderRuntime {
2415
2415
  return this.configure(selections);
2416
2416
  }
2417
2417
 
2418
- status(role = null) {
2419
- if (role === null) {
2420
- return getAIRuntimeState();
2418
+ status(role = null, options = {}) {
2419
+ if (role !== null) {
2420
+ assertRole(role);
2421
2421
  }
2422
- assertRole(role);
2423
- return getAIRuntimeState().roles[role];
2422
+ const snapshot = getAIRuntimeState();
2423
+ if (options?.execution !== true) {
2424
+ return role === null ? snapshot : snapshot.roles[role];
2425
+ }
2426
+
2427
+ // Live provider inspection is explicit so ordinary lifecycle reads
2428
+ // retain their sticky snapshot identity and never call a provider.
2429
+ const roles = {...snapshot.roles};
2430
+ for (const selectedRole of role === null ? AI_RUNTIME_ROLES : [role]) {
2431
+ const provider = this.#providerFor(this.#slots[selectedRole]);
2432
+ if (!provider) {
2433
+ continue;
2434
+ }
2435
+ const providerStatus = validateProviderStatus(provider.status());
2436
+ if (providerStatus.execution == null) {
2437
+ continue;
2438
+ }
2439
+ roles[selectedRole] = {
2440
+ ...roles[selectedRole],
2441
+ execution: {...providerStatus.execution}
2442
+ };
2443
+ }
2444
+ return role === null ? {...snapshot, roles} : roles[role];
2424
2445
  }
2425
2446
 
2426
2447
  catalog(role) {