arcane-os 0.3.1 → 0.3.2

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 (153) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +86 -117
  3. package/bin/arcane-test.mjs +170 -46
  4. package/browser-runtime/ai/browser-speech-artifacts.mjs +855 -895
  5. package/browser-runtime/ai/browser-speech-providers.mjs +80 -204
  6. package/browser-runtime/ai/browser-wasm-llm-provider.mjs +627 -819
  7. package/browser-runtime/ai/browser-wasm.mjs +24 -35
  8. package/browser-runtime/ai/browser-wllama-runtime.mjs +64 -316
  9. package/browser-runtime/ai/model-controller.mjs +584 -181
  10. package/browser-runtime/ai/speech-worker-client.mjs +8 -148
  11. package/browser-runtime/ai/speech-worker-runtime.mjs +642 -374
  12. package/browser-runtime/dom-event-instrumentation.mjs +55 -147
  13. package/browser-runtime/event-manager.mjs +239 -624
  14. package/package.json +5 -6
  15. package/runtime/arcane/components/app-bar.html +3 -15
  16. package/runtime/arcane/components/assistant-panel.html +10 -10
  17. package/runtime/arcane/components/calculator.html +1 -1
  18. package/runtime/arcane/components/chat.html +1359 -135
  19. package/runtime/arcane/components/conversation-view.html +2 -2
  20. package/runtime/arcane/components/document-inspector.html +11 -17
  21. package/runtime/arcane/components/file-manager.html +13 -56
  22. package/runtime/arcane/components/markdown-document.html +82 -281
  23. package/runtime/arcane/components/markdown-editor.html +7 -10
  24. package/runtime/arcane/components/media-embed.html +6 -6
  25. package/runtime/arcane/components/screen-capture.html +4 -4
  26. package/runtime/arcane/components/source-explanation.html +2 -2
  27. package/runtime/arcane/components/speech.html +112 -68
  28. package/runtime/arcane/components/terminal-workspace.html +4 -4
  29. package/runtime/arcane/components/theme-editor.html +1 -1
  30. package/runtime/arcane/components/unified-inbox.html +2 -2
  31. package/runtime/arcane/components/voice-transcription.html +31 -21
  32. package/runtime/arcane/entities/Calculation.js +2 -3
  33. package/runtime/arcane/entities/Chat.js +228 -43
  34. package/runtime/arcane/entities/Preference.js +3 -5
  35. package/runtime/arcane/entities/Weather.js +5 -5
  36. package/runtime/arcane/modules/AI.js +1042 -427
  37. package/runtime/arcane/modules/AIProviderRuntime.js +618 -359
  38. package/runtime/arcane/modules/AIResponseLength.js +9 -19
  39. package/runtime/arcane/modules/AIRuntimeState.js +109 -72
  40. package/runtime/arcane/modules/ArcaneNavigationPolicy.js +45 -32
  41. package/runtime/arcane/modules/BrowserTestSuite.js +78 -122
  42. package/runtime/arcane/modules/CalculatorEngine.js +9 -9
  43. package/runtime/arcane/modules/CommunicationAppController.js +3 -7
  44. package/runtime/arcane/modules/ComponentContracts.js +30 -32
  45. package/runtime/arcane/modules/ConfiguredAIChatSession.js +281 -230
  46. package/runtime/arcane/modules/ConversationActionItems.js +26 -59
  47. package/runtime/arcane/modules/ConversationClosingReport.js +34 -61
  48. package/runtime/arcane/modules/ConversationTimebox.js +27 -15
  49. package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +152 -344
  50. package/runtime/arcane/modules/DocumentLexicalSearch.js +25 -91
  51. package/runtime/arcane/modules/HTMLImport.js +54 -1
  52. package/runtime/arcane/modules/IsolatedModelQuestionRunner.js +40 -203
  53. package/runtime/arcane/modules/LocalAIReadiness.js +40 -60
  54. package/runtime/arcane/modules/LocalAIReadinessController.js +15 -13
  55. package/runtime/arcane/modules/MD.js +1 -45
  56. package/runtime/arcane/modules/Mail.js +51 -103
  57. package/runtime/arcane/modules/MailOutbox.mjs +95 -193
  58. package/runtime/arcane/modules/MailTransport.mjs +36 -57
  59. package/runtime/arcane/modules/ModelDefinition.js +22 -106
  60. package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +39 -101
  61. package/runtime/arcane/modules/PersistentAIChatSession.js +281 -18
  62. package/runtime/arcane/modules/PreferenceStore.js +102 -30
  63. package/runtime/arcane/modules/RiskSignalAnalyzer.js +8 -9
  64. package/runtime/arcane/modules/ScopedOPFSCache.js +7 -42
  65. package/runtime/arcane/modules/ScreenCapture.js +175 -128
  66. package/runtime/arcane/modules/SpeechPlayback.js +46 -149
  67. package/runtime/arcane/modules/StaticDocumentCatalog.js +173 -407
  68. package/runtime/arcane/modules/ToolCallRouter.js +25 -12
  69. package/runtime/arcane/modules/YouTubeMedia.js +6 -5
  70. package/schemas/arcane-app-bundle.schema.json +13 -78
  71. package/schemas/arcane-app.schema.json +9 -25
  72. package/schemas/arcane-lock.schema.json +18 -151
  73. package/schemas/arcane-package.schema.json +2 -16
  74. package/schemas/native-build-plan.schema.json +119 -122
  75. package/src/app-descriptor.mjs +75 -132
  76. package/src/application-tests.mjs +200 -0
  77. package/src/cli/main.mjs +27 -46
  78. package/src/constants.mjs +3 -4
  79. package/src/dev-server.mjs +30 -324
  80. package/src/doctor.mjs +92 -154
  81. package/src/dom-event-instrumentation.mjs +55 -147
  82. package/src/errors.mjs +2 -3
  83. package/src/event-manager.mjs +239 -624
  84. package/src/event-queue.mjs +3 -3
  85. package/src/import-map.mjs +273 -1028
  86. package/src/index.mjs +14 -16
  87. package/src/installed-sdk-runtime.mjs +27 -67
  88. package/src/integrated-provider-loader.mjs +53 -382
  89. package/src/mail-api.mjs +0 -2
  90. package/src/mail-server.mjs +224 -580
  91. package/src/mail.mjs +4 -10
  92. package/src/native-plan.mjs +163 -598
  93. package/src/native-provider-loader.mjs +104 -1063
  94. package/src/packager/core.mjs +485 -3229
  95. package/src/process.mjs +5 -10
  96. package/src/release-bundle.mjs +292 -2405
  97. package/src/runtime.mjs +76 -396
  98. package/src/scaffold.mjs +30 -80
  99. package/src/sdk-browser-runtime.mjs +70 -626
  100. package/src/source-server.mjs +588 -0
  101. package/src/targets/index.mjs +78 -188
  102. package/src/templates/workspace-template.mjs +19 -135
  103. package/src/testing-loader.mjs +164 -0
  104. package/src/testing.mjs +1 -1
  105. package/src/toolchain.mjs +131 -544
  106. package/src/update-check.mjs +26 -64
  107. package/src/workspace-operation-lock.mjs +139 -430
  108. package/src/workspace-runtime.mjs +109 -1558
  109. package/src/workspace.mjs +40 -302
  110. package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +0 -218
  111. package/browser-runtime/ai/ARCANE_AI_BROWSER_SPEECH_COMPONENTS.json +0 -203
  112. package/browser-runtime/ai/ARCANE_AI_BROWSER_WASM_COMPONENTS.json +0 -80
  113. package/browser-runtime/ai/internal/sha256.mjs +0 -166
  114. package/docs/architecture.md +0 -344
  115. package/docs/compatibility.md +0 -36
  116. package/docs/event-manager.md +0 -294
  117. package/docs/platform-targets.md +0 -108
  118. package/docs/publishing.md +0 -201
  119. package/docs/reference/README.md +0 -185
  120. package/docs/reference/ai/browser-speech-package-authority.json +0 -835
  121. package/docs/reference/ai/browser-speech.md +0 -1295
  122. package/docs/reference/ai/browser-wasm.md +0 -530
  123. package/docs/reference/arcane-ollama.md +0 -288
  124. package/docs/reference/availability-and-normalization.md +0 -183
  125. package/docs/reference/behavioral-testing.md +0 -133
  126. package/docs/reference/cli.md +0 -779
  127. package/docs/reference/core/README.md +0 -62
  128. package/docs/reference/core/arcane-ai-contracts.md +0 -907
  129. package/docs/reference/core/arcane-api.md +0 -601
  130. package/docs/reference/core/arcane-entities.md +0 -65
  131. package/docs/reference/core/arcane-events.md +0 -134
  132. package/docs/reference/core/ollama-module.md +0 -181
  133. package/docs/reference/core/reference/arcane-api/ai-and-ollama.md +0 -1909
  134. package/docs/reference/core/reference/arcane-api/applications-terminal-capabilities.md +0 -1057
  135. package/docs/reference/core/reference/arcane-api/core-and-events.md +0 -320
  136. package/docs/reference/core/reference/arcane-api/filesystem-storage-preferences-appearance.md +0 -610
  137. package/docs/reference/core/reference/arcane-api/namespaces.md +0 -1157
  138. package/docs/reference/core/reference/arcane-api/platform-installation-users-system.md +0 -1423
  139. package/docs/reference/core/reference/arcane-api/session-provisioning-diagnostics-development.md +0 -315
  140. package/docs/reference/event-manager.md +0 -1511
  141. package/docs/reference/inventory/package-api.json +0 -3284
  142. package/docs/reference/inventory/runtime-components.json +0 -1011
  143. package/docs/reference/inventory/runtime-entities.json +0 -26
  144. package/docs/reference/inventory/runtime-modules.json +0 -1431
  145. package/docs/reference/mail.md +0 -316
  146. package/docs/reference/protocols.md +0 -719
  147. package/docs/reference/runtime-components.md +0 -1366
  148. package/docs/reference/runtime-entities.md +0 -303
  149. package/docs/reference/runtime-modules.md +0 -2965
  150. package/docs/reference/sdk-api.md +0 -6698
  151. package/docs/roadmap.md +0 -79
  152. package/docs/work-amplification.md +0 -129
  153. package/runtime/ARCANE_RUNTIME_RELEASE.json +0 -826
@@ -1,24 +1,24 @@
1
- export const DEFAULT_MAIL_REQUEST_TIMEOUT_MS=590_000;
2
- export const MAX_MAIL_RESPONSE_BYTES=65_536;
1
+ export const DEFAULT_MAIL_REQUEST_TIMEOUT_MS=null;
3
2
 
4
- const REPORT_KEY_PATTERN=/^[a-zA-Z0-9._:-]{8,128}$/;
5
- const REQUEST_ID_PATTERN=/^[a-zA-Z0-9-]{8,128}$/;
6
- const PROVIDER_ID_PATTERN=/^[a-zA-Z0-9._:-]{1,256}$/;
7
- const ERROR_CODE_PATTERN=/^[a-zA-Z0-9._:-]{1,80}$/;
3
+ const REPORT_KEY_PATTERN=/^[a-zA-Z0-9._:-]+$/;
4
+ const REQUEST_ID_PATTERN=/^[a-zA-Z0-9-]+$/;
5
+ const PROVIDER_ID_PATTERN=/^[a-zA-Z0-9._:-]+$/;
6
+ const ERROR_CODE_PATTERN=/^[a-zA-Z0-9._:-]+$/;
8
7
  const RETRYABLE_STATUS_CODES=new Set([408,425,429,500,502,503,504]);
9
8
  const NON_RETRYABLE_RATE_CODES=new Set(['daily_quota_exceeded','monthly_quota_exceeded']);
10
- const RESPONSE_CONTRACT=Object.freeze({
9
+ const RESPONSE_CONTRACT={
11
10
  accepted:202,
12
11
  delivery_uncertain:207,
13
12
  partially_accepted:207,
14
- });
13
+ };
15
14
 
16
15
  export class MailTransportError extends Error {
17
- constructor(message,{cause,code='MAIL_TRANSPORT_ERROR',retryable=false,
16
+ constructor(message,{cause,code='MAIL_TRANSPORT_ERROR',details=null,retryable=false,
18
17
  retryAfterMs=0,statusCode=0,uncertain=false}={}){
19
18
  super(message,{cause});
20
19
  this.name='MailTransportError';
21
20
  this.code=code;
21
+ this.details=details;
22
22
  this.retryable=Boolean(retryable);
23
23
  this.retryAfterMs=Number.isSafeInteger(retryAfterMs)&&retryAfterMs>0
24
24
  ? retryAfterMs
@@ -94,17 +94,18 @@ function parseRetryAfter(value,now=Date.now()){
94
94
  }
95
95
  const trimmed=value.trim();
96
96
  if(/^\d+(?:\.\d+)?$/u.test(trimmed)){
97
- return Math.min(86_400_000,Math.max(0,Math.ceil(Number(trimmed)*1000)));
97
+ return Math.max(0,Math.ceil(Number(trimmed)*1000));
98
98
  }
99
99
  const timestamp=Date.parse(trimmed);
100
100
  return Number.isFinite(timestamp)
101
- ? Math.min(86_400_000,Math.max(0,timestamp-now))
101
+ ? Math.max(0,timestamp-now)
102
102
  : 0;
103
103
  }
104
104
 
105
- function invalidSuccessResponse(response){
105
+ function invalidSuccessResponse(response,responseText){
106
106
  return new MailTransportError('Mail server returned an invalid success response',{
107
107
  code:'MAIL_INVALID_RESPONSE',statusCode:response.status,uncertain:true,
108
+ details:parseJsonObject(responseText)??responseText
108
109
  });
109
110
  }
110
111
 
@@ -114,20 +115,21 @@ function parseDeliveryResponse(response,responseText){
114
115
  || typeof body.requestId!=='string'||!REQUEST_ID_PATTERN.test(body.requestId)
115
116
  || !Object.hasOwn(RESPONSE_CONTRACT,body.status)
116
117
  || RESPONSE_CONTRACT[body.status]!==response.status) {
117
- throw invalidSuccessResponse(response);
118
+ throw invalidSuccessResponse(response,responseText);
118
119
  }
119
120
  for(const field of ['accepted','rejected']){
120
121
  if(body[field]!==undefined
121
122
  && (!Number.isSafeInteger(body[field])||body[field]<0)) {
122
- throw invalidSuccessResponse(response);
123
+ throw invalidSuccessResponse(response,responseText);
123
124
  }
124
125
  }
125
126
  if(body.providerId!==undefined
126
127
  && (typeof body.providerId!=='string'||!PROVIDER_ID_PATTERN.test(body.providerId))){
127
- throw invalidSuccessResponse(response);
128
+ throw invalidSuccessResponse(response,responseText);
128
129
  }
129
130
 
130
131
  return {
132
+ ...body,
131
133
  requestId:body.requestId,
132
134
  sent:body.status==='accepted',
133
135
  partial:body.status==='partially_accepted',
@@ -164,6 +166,7 @@ function parseRejection(response,responseText){
164
166
  : 0;
165
167
  return new MailTransportError(`Mail server rejected the request (${response.status})`,{
166
168
  code,
169
+ details:body??responseText,
167
170
  retryable,
168
171
  retryAfterMs:bodyRetryAfter||parseRetryAfter(response.headers?.get?.('retry-after')),
169
172
  statusCode:response.status,
@@ -171,37 +174,18 @@ function parseRejection(response,responseText){
171
174
  });
172
175
  }
173
176
 
174
- async function readBoundedResponseText(response){
175
- const declaredLength=response.headers?.get?.('content-length');
176
- if(declaredLength!==null&&declaredLength!==undefined&&declaredLength!==''){
177
- const parsedLength=Number(declaredLength);
178
- if(!Number.isSafeInteger(parsedLength)||parsedLength<0||parsedLength>MAX_MAIL_RESPONSE_BYTES){
179
- throw new MailTransportError(
180
- `Mail server response cannot exceed ${MAX_MAIL_RESPONSE_BYTES.toLocaleString('en-US')} bytes`,
181
- {code:'MAIL_RESPONSE_TOO_LARGE',statusCode:response.status,uncertain:true}
182
- );
183
- }
184
- }
185
-
177
+ async function readResponseText(response){
186
178
  if(!response.body||typeof response.body.getReader!=='function'){
187
179
  if(typeof response.text!=='function'){
188
180
  throw new MailTransportError('Mail server returned an unreadable response',{
189
181
  code:'MAIL_UNREADABLE_RESPONSE',statusCode:response.status,uncertain:true,
190
182
  });
191
183
  }
192
- const text=await response.text();
193
- if(new TextEncoder().encode(text).byteLength>MAX_MAIL_RESPONSE_BYTES){
194
- throw new MailTransportError(
195
- `Mail server response cannot exceed ${MAX_MAIL_RESPONSE_BYTES.toLocaleString('en-US')} bytes`,
196
- {code:'MAIL_RESPONSE_TOO_LARGE',statusCode:response.status,uncertain:true}
197
- );
198
- }
199
- return text;
184
+ return response.text();
200
185
  }
201
186
 
202
187
  const reader=response.body.getReader();
203
188
  const decoder=new TextDecoder();
204
- let byteLength=0;
205
189
  let text='';
206
190
  try{
207
191
  while(true){
@@ -212,14 +196,6 @@ async function readBoundedResponseText(response){
212
196
  code:'MAIL_UNREADABLE_RESPONSE',statusCode:response.status,uncertain:true,
213
197
  });
214
198
  }
215
- byteLength+=value.byteLength;
216
- if(byteLength>MAX_MAIL_RESPONSE_BYTES){
217
- await reader.cancel().catch(function ignoreReaderCancellation(){});
218
- throw new MailTransportError(
219
- `Mail server response cannot exceed ${MAX_MAIL_RESPONSE_BYTES.toLocaleString('en-US')} bytes`,
220
- {code:'MAIL_RESPONSE_TOO_LARGE',statusCode:response.status,uncertain:true}
221
- );
222
- }
223
199
  text+=decoder.decode(value,{stream:true});
224
200
  }
225
201
  text+=decoder.decode();
@@ -235,7 +211,7 @@ function requestBodyFrom({report,serializedReport}){
235
211
  }
236
212
  const validated=validateSerializedReport(serializedReport);
237
213
  if(report!==undefined&&serializeMailReport(report)!==validated){
238
- throw new Error('Mail report does not match its immutable serialized request body');
214
+ throw new Error('Mail report does not match its stored serialized request body');
239
215
  }
240
216
  return validated;
241
217
  }
@@ -259,10 +235,11 @@ export async function sendMailReport({
259
235
  throw new Error('Mail application identity is invalid');
260
236
  }
261
237
  if(typeof reportKey!=='string'||!REPORT_KEY_PATTERN.test(reportKey)){
262
- throw new Error('Mail report key must contain 8-128 safe characters');
238
+ throw new Error('Mail report key must contain safe characters');
263
239
  }
264
- if(!Number.isSafeInteger(requestTimeout)||requestTimeout<1_000||requestTimeout>600_000){
265
- throw new Error('Mail request timeout must be an integer between 1000 and 600000 milliseconds');
240
+ if(requestTimeout!==null&&requestTimeout!==undefined
241
+ &&(!Number.isSafeInteger(requestTimeout)||requestTimeout<1)){
242
+ throw new Error('Mail request timeout must be a positive integer');
266
243
  }
267
244
  if(appKey!==undefined&&appKey!==null&&typeof appKey!=='string'){
268
245
  throw new Error('Mail application key must be a string');
@@ -290,13 +267,15 @@ export async function sendMailReport({
290
267
  if(signal?.aborted){
291
268
  forwardAbort();
292
269
  }
293
- const timeout=setTimeout(
294
- function abortTimedOutMail(){
295
- timedOut=true;
296
- controller.abort(new Error('Mail request timed out'));
297
- },
298
- requestTimeout
299
- );
270
+ const timeout=requestTimeout==null
271
+ ?null
272
+ :setTimeout(
273
+ function abortTimedOutMail(){
274
+ timedOut=true;
275
+ controller.abort(new Error('Mail request timed out'));
276
+ },
277
+ requestTimeout
278
+ );
300
279
 
301
280
  try{
302
281
  let response;
@@ -326,13 +305,13 @@ export async function sendMailReport({
326
305
  );
327
306
  }
328
307
 
329
- const responseText=await readBoundedResponseText(response);
308
+ const responseText=await readResponseText(response);
330
309
  if(!response.ok){
331
310
  throw parseRejection(response,responseText);
332
311
  }
333
312
  return parseDeliveryResponse(response,responseText);
334
313
  }finally{
335
- clearTimeout(timeout);
314
+ if(timeout!==null) clearTimeout(timeout);
336
315
  signal?.removeEventListener('abort',forwardAbort);
337
316
  }
338
317
  }
@@ -1,9 +1,5 @@
1
- const MAX_MODEL_DEFINITION_CHARACTERS=128*1024;
2
- const MAX_SYSTEM_PROMPT_CHARACTERS=96*1024;
3
- const MAX_PARAMETER_COUNT=32;
4
- const MAX_RESPONSE_BYTES=MAX_MODEL_DEFINITION_CHARACTERS*4;
5
- const MODEL_REFERENCE=/^[A-Za-z0-9][A-Za-z0-9._/-]{0,191}(?::[A-Za-z0-9][A-Za-z0-9._-]{0,63})?$/;
6
- const PARAMETER_NAME=/^[a-z][a-z0-9_]{0,63}$/;
1
+ const MODEL_REFERENCE=/^[A-Za-z0-9][A-Za-z0-9._/-]*(?::[A-Za-z0-9][A-Za-z0-9._-]*)?$/;
2
+ const PARAMETER_NAME=/^[a-z][a-z0-9_]*$/;
7
3
 
8
4
  function fail(message){
9
5
  const error=new TypeError(message);
@@ -11,83 +7,18 @@ function fail(message){
11
7
  throw error;
12
8
  }
13
9
 
14
- function freeze(value){
15
- if(!value||typeof value!=='object'||Object.isFrozen(value)){
16
- return value;
10
+ async function completeResponseText(response){
11
+ if(typeof response?.text!=='function'){
12
+ fail('The model definition response is not readable.');
17
13
  }
18
14
 
19
- for(const child of Object.values(value)){
20
- freeze(child);
21
- }
22
-
23
- return Object.freeze(value);
24
- }
25
-
26
- function responseLength(response){
27
- const raw=response?.headers?.get?.('content-length');
15
+ const text=await response.text();
28
16
 
29
- if(raw===null||raw===undefined||raw===''){
30
- return null;
17
+ if(typeof text!=='string'){
18
+ fail('The model definition response is not readable text.');
31
19
  }
32
20
 
33
- const value=Number(raw);
34
- return Number.isSafeInteger(value)&&value>=0?value:Number.POSITIVE_INFINITY;
35
- }
36
-
37
- async function boundedResponseText(response,maxBytes=MAX_RESPONSE_BYTES){
38
- const declaredLength=responseLength(response);
39
-
40
- if(declaredLength!==null&&declaredLength>maxBytes){
41
- fail('The model definition response exceeds the allowed size.');
42
- }
43
-
44
- const reader=response?.body?.getReader?.();
45
-
46
- if(!reader){
47
- if(typeof response?.text!=='function'){
48
- fail('The model definition response is not readable.');
49
- }
50
-
51
- const text=await response.text();
52
-
53
- if(typeof text!=='string'||new TextEncoder().encode(text).byteLength>maxBytes){
54
- fail('The model definition response exceeds the allowed size.');
55
- }
56
-
57
- return text;
58
- }
59
-
60
- const decoder=new TextDecoder('utf-8',{fatal:true});
61
- const parts=[];
62
- let total=0;
63
-
64
- try{
65
- while(true){
66
- const {done,value}=await reader.read();
67
-
68
- if(done){
69
- break;
70
- }
71
- if(!(value instanceof Uint8Array)){
72
- fail('The model definition response contained an invalid byte chunk.');
73
- }
74
-
75
- total+=value.byteLength;
76
- if(total>maxBytes){
77
- await reader.cancel?.();
78
- fail('The model definition response exceeds the allowed size.');
79
- }
80
- parts.push(decoder.decode(value,{stream:true}));
81
- }
82
- parts.push(decoder.decode());
83
- }catch(error){
84
- if(error?.code==='MODEL_DEFINITION_INVALID'){
85
- throw error;
86
- }
87
- fail('The model definition response is not valid UTF-8 text.');
88
- }
89
-
90
- return parts.join('');
21
+ return text;
91
22
  }
92
23
 
93
24
  /**
@@ -98,17 +29,15 @@ async function boundedResponseText(response,maxBytes=MAX_RESPONSE_BYTES){
98
29
  export function parseModelDefinition(source){
99
30
  if(typeof source!=='string'
100
31
  ||!source
101
- ||source.length>MAX_MODEL_DEFINITION_CHARACTERS
102
32
  ||source.includes('\0')
103
33
  ){
104
- fail('The model definition must be bounded non-empty text.');
34
+ fail('The model definition must be non-empty text without null characters.');
105
35
  }
106
36
 
107
- const normalized=source.replaceAll('\r\n','\n');
108
-
109
- if(normalized.includes('\r')||normalized.startsWith('\uFEFF')){
110
- fail('The model definition must use canonical UTF-8 line endings.');
111
- }
37
+ const normalized=source
38
+ .replace(/^\uFEFF/u,'')
39
+ .replaceAll('\r\n','\n')
40
+ .replaceAll('\r','\n');
112
41
 
113
42
  const match=normalized.match(
114
43
  /^FROM ([^\n]+)\n\nSYSTEM """\n([\s\S]*?)\n"""(?:\n\n([\s\S]+?))?\n?$/
@@ -125,18 +54,9 @@ export function parseModelDefinition(source){
125
54
  if(!MODEL_REFERENCE.test(from)){
126
55
  fail('The model definition contains an invalid base-model reference.');
127
56
  }
128
- if(!system.trim()
129
- ||system!==system.trim()
130
- ||system.length>MAX_SYSTEM_PROMPT_CHARACTERS
131
- ||system.split('\n').some(function systemLineTooLong(line){
132
- return line.length>4096;
133
- })
134
- ){
57
+ if(!system.trim()){
135
58
  fail('The model definition contains an invalid SYSTEM prompt.');
136
59
  }
137
- if(parameterLines.length>MAX_PARAMETER_COUNT){
138
- fail('The model definition contains an invalid parameter count.');
139
- }
140
60
 
141
61
  const parameters={};
142
62
 
@@ -145,8 +65,7 @@ export function parseModelDefinition(source){
145
65
 
146
66
  if(!parameter
147
67
  ||!PARAMETER_NAME.test(parameter[1])
148
- ||parameter[2]!==parameter[2].trim()
149
- ||parameter[2].length>256
68
+ ||!parameter[2].trim()
150
69
  ||Object.hasOwn(parameters,parameter[1])
151
70
  ){
152
71
  fail(`The model definition contains an invalid parameter line: ${line}`);
@@ -155,13 +74,13 @@ export function parseModelDefinition(source){
155
74
  parameters[parameter[1]]=parameter[2];
156
75
  }
157
76
 
158
- return freeze({from,system,parameters});
77
+ return {from,system,parameters};
159
78
  }
160
79
 
161
80
  /**
162
- * Loads a packaged definition with a read-only same-origin GET and returns the
163
- * SYSTEM block. The definition is never evaluated and no model service is
164
- * contacted by this helper.
81
+ * Loads a packaged definition with an ordinary read-only GET and returns the
82
+ * complete SYSTEM block. The definition is never evaluated and no model
83
+ * service is contacted by this helper.
165
84
  */
166
85
  export async function loadModelDefinitionSystemPrompt(url,{
167
86
  fetchImpl=globalThis.fetch
@@ -171,10 +90,7 @@ export async function loadModelDefinitionSystemPrompt(url,{
171
90
  }
172
91
 
173
92
  const response=await fetchImpl(url,{
174
- method:'GET',
175
- credentials:'same-origin',
176
- cache:'default',
177
- redirect:'error'
93
+ method:'GET'
178
94
  });
179
95
 
180
96
  if(!response||response.ok!==true){
@@ -185,5 +101,5 @@ export async function loadModelDefinitionSystemPrompt(url,{
185
101
  throw error;
186
102
  }
187
103
 
188
- return parseModelDefinition(await boundedResponseText(response)).system;
104
+ return parseModelDefinition(await completeResponseText(response)).system;
189
105
  }
@@ -2,40 +2,40 @@ import {createArcaneEventSource} from 'arcane-os/event-manager';
2
2
  import ApiModelDatabase from './ApiModelDatabase.js';
3
3
  import {WeatherDay,WeatherLocation,WeatherObservation,WeatherSnapshot} from '../entities/Weather.js';
4
4
 
5
- export const OPEN_METEO_ENDPOINTS=Object.freeze({
5
+ export const OPEN_METEO_ENDPOINTS={
6
6
  geocoding:'https://geocoding-api.open-meteo.com/v1/search',
7
7
  forecast:'https://api.open-meteo.com/v1/forecast'
8
- });
8
+ };
9
9
 
10
- export const OPEN_METEO_WEATHER_EVENTS=Object.freeze({
10
+ export const OPEN_METEO_WEATHER_EVENTS={
11
11
  requestStarted:'weather-request',
12
12
  requestFailed:'weather-error',
13
13
  locationSearchSucceeded:'weather-locations',
14
14
  forecastLoadSucceeded:'weather-weather'
15
- });
16
-
17
- const WEATHER_EVENT_TYPES=Object.freeze(Object.values(OPEN_METEO_WEATHER_EVENTS));
18
-
19
- export const OPEN_METEO_WEATHER_ERRORS=Object.freeze({
20
- providerDisposed:Object.freeze({code:'ARCANE_OPEN_METEO_WEATHER_PROVIDER_DISPOSED',reason:'open-meteo-weather-provider-disposed'}),
21
- weatherEventTypeInvalid:Object.freeze({code:'ARCANE_OPEN_METEO_WEATHER_EVENT_TYPE_INVALID',reason:'weather-event-type-invalid'}),
22
- weatherLocationInvalid:Object.freeze({code:'ARCANE_OPEN_METEO_WEATHER_LOCATION_INVALID',reason:'weather-location-invalid'}),
23
- weatherOperationOptionsInvalid:Object.freeze({code:'ARCANE_OPEN_METEO_WEATHER_OPERATION_OPTIONS_INVALID',reason:'weather-operation-options-invalid'}),
24
- weatherLocationQueryInvalid:Object.freeze({code:'ARCANE_OPEN_METEO_WEATHER_LOCATION_QUERY_INVALID',reason:'weather-location-query-invalid'}),
25
- locationSearchAborted:Object.freeze({code:'ARCANE_OPEN_METEO_WEATHER_LOCATION_SEARCH_ABORTED',reason:'weather-location-search-aborted'}),
26
- locationSearchFailed:Object.freeze({code:'ARCANE_OPEN_METEO_WEATHER_LOCATION_SEARCH_FAILED',reason:'weather-location-search-rejected'}),
27
- locationSearchSuperseded:Object.freeze({code:'ARCANE_OPEN_METEO_WEATHER_LOCATION_SEARCH_SUPERSEDED',reason:'weather-location-search-superseded'}),
28
- forecastLoadAborted:Object.freeze({code:'ARCANE_OPEN_METEO_WEATHER_FORECAST_LOAD_ABORTED',reason:'weather-forecast-load-aborted'}),
29
- forecastLoadFailed:Object.freeze({code:'ARCANE_OPEN_METEO_WEATHER_FORECAST_LOAD_FAILED',reason:'weather-forecast-load-rejected'}),
30
- forecastLoadSuperseded:Object.freeze({code:'ARCANE_OPEN_METEO_WEATHER_FORECAST_LOAD_SUPERSEDED',reason:'weather-forecast-load-superseded'})
31
- });
32
-
33
- const WEATHER_EVENT_NAMES=Object.freeze({
15
+ };
16
+
17
+ const WEATHER_EVENT_TYPES=Object.values(OPEN_METEO_WEATHER_EVENTS);
18
+
19
+ export const OPEN_METEO_WEATHER_ERRORS={
20
+ providerDisposed:{code:'ARCANE_OPEN_METEO_WEATHER_PROVIDER_DISPOSED',reason:'open-meteo-weather-provider-disposed'},
21
+ weatherEventTypeInvalid:{code:'ARCANE_OPEN_METEO_WEATHER_EVENT_TYPE_INVALID',reason:'weather-event-type-invalid'},
22
+ weatherLocationInvalid:{code:'ARCANE_OPEN_METEO_WEATHER_LOCATION_INVALID',reason:'weather-location-invalid'},
23
+ weatherOperationOptionsInvalid:{code:'ARCANE_OPEN_METEO_WEATHER_OPERATION_OPTIONS_INVALID',reason:'weather-operation-options-invalid'},
24
+ weatherLocationQueryInvalid:{code:'ARCANE_OPEN_METEO_WEATHER_LOCATION_QUERY_INVALID',reason:'weather-location-query-invalid'},
25
+ locationSearchAborted:{code:'ARCANE_OPEN_METEO_WEATHER_LOCATION_SEARCH_ABORTED',reason:'weather-location-search-aborted'},
26
+ locationSearchFailed:{code:'ARCANE_OPEN_METEO_WEATHER_LOCATION_SEARCH_FAILED',reason:'weather-location-search-rejected'},
27
+ locationSearchSuperseded:{code:'ARCANE_OPEN_METEO_WEATHER_LOCATION_SEARCH_SUPERSEDED',reason:'weather-location-search-superseded'},
28
+ forecastLoadAborted:{code:'ARCANE_OPEN_METEO_WEATHER_FORECAST_LOAD_ABORTED',reason:'weather-forecast-load-aborted'},
29
+ forecastLoadFailed:{code:'ARCANE_OPEN_METEO_WEATHER_FORECAST_LOAD_FAILED',reason:'weather-forecast-load-rejected'},
30
+ forecastLoadSuperseded:{code:'ARCANE_OPEN_METEO_WEATHER_FORECAST_LOAD_SUPERSEDED',reason:'weather-forecast-load-superseded'}
31
+ };
32
+
33
+ const WEATHER_EVENT_NAMES={
34
34
  error:OPEN_METEO_WEATHER_EVENTS.requestFailed,
35
35
  locations:OPEN_METEO_WEATHER_EVENTS.locationSearchSucceeded,
36
36
  request:OPEN_METEO_WEATHER_EVENTS.requestStarted,
37
37
  weather:OPEN_METEO_WEATHER_EVENTS.forecastLoadSucceeded
38
- });
38
+ };
39
39
 
40
40
  function signalLike(value){
41
41
  return value===undefined
@@ -110,7 +110,7 @@ function normalizedOperationError(error,record){
110
110
  }
111
111
 
112
112
  function operationOptions(value){
113
- if(value===undefined)return Object.freeze({signal:null});
113
+ if(value===undefined)return {signal:null};
114
114
  if(!value||typeof value!=='object'||Array.isArray(value)){
115
115
  throw invalidOptionsError('Open-Meteo operation options must be an object.');
116
116
  }
@@ -152,14 +152,10 @@ function parseLocations(raw){
152
152
  function parseForecast(raw,{context}){return mapForecast(raw,context.location);}
153
153
 
154
154
  export default class OpenMeteoWeatherProvider extends EventTarget{
155
- #activeLoad=null;
156
- #activeSearch=null;
157
155
  #disposed=false;
158
156
  #events;
159
- #loadGeneration=0;
160
157
  #operationSequence=0;
161
158
  #operations=new Map();
162
- #searchGeneration=0;
163
159
  #unsubscribe=[];
164
160
 
165
161
  constructor({
@@ -210,12 +206,6 @@ export default class OpenMeteoWeatherProvider extends EventTarget{
210
206
  }
211
207
  }
212
208
 
213
- #currentOperation(record){
214
- return record.kind==='location-search'
215
- ?this.#activeSearch===record&&this.#searchGeneration===record.generation
216
- :this.#activeLoad===record&&this.#loadGeneration===record.generation;
217
- }
218
-
219
209
  #releaseSignal(record){
220
210
  if(!Array.isArray(record.cleanup))return;
221
211
  for(const remove of record.cleanup.splice(0))remove();
@@ -225,8 +215,6 @@ export default class OpenMeteoWeatherProvider extends EventTarget{
225
215
  if(record.settled)return;
226
216
  record.settled=true;
227
217
  this.#releaseSignal(record);
228
- if(record.kind==='location-search'&&this.#activeSearch===record)this.#activeSearch=null;
229
- if(record.kind==='forecast-load'&&this.#activeLoad===record)this.#activeLoad=null;
230
218
  }
231
219
 
232
220
  #finishOperation(record){
@@ -243,30 +231,8 @@ export default class OpenMeteoWeatherProvider extends EventTarget{
243
231
  'The weather event type is invalid.'
244
232
  );
245
233
  }
246
- const compatibilityDetail=Object.freeze({...detail,operationId});
247
- const operation=typeof detail?.operation==='string'?detail.operation:null;
248
- let publicDetail;
249
- if(type==='request'){
250
- publicDetail=Object.freeze(operation?{operation}:{});
251
- }else if(type==='error'){
252
- publicDetail=Object.freeze({
253
- ...(operation?{operation}:{}),
254
- ...(typeof detail?.error?.code==='string'?{code:detail.error.code}:{}),
255
- ...(typeof detail?.error?.reason==='string'?{reason:detail.error.reason}:{})
256
- });
257
- }else if(type==='locations'){
258
- publicDetail=Object.freeze({
259
- ...(operation?{operation}:{}),
260
- count:Array.isArray(detail?.locations)?detail.locations.length:0
261
- });
262
- }else{
263
- publicDetail=Object.freeze({
264
- ...(operation?{operation}:{}),
265
- ...(typeof detail?.weather?.location?.id==='string'
266
- ?{locationId:detail.weather.location.id}
267
- :{})
268
- });
269
- }
234
+ const compatibilityDetail={...detail,operationId};
235
+ const publicDetail={...compatibilityDetail};
270
236
  return this.#events.dispatch(eventType,compatibilityDetail,{operationId,publicDetail});
271
237
  }
272
238
 
@@ -309,16 +275,12 @@ export default class OpenMeteoWeatherProvider extends EventTarget{
309
275
  operationMessage(kind,'aborted')
310
276
  );
311
277
  }
312
- const generation=kind==='location-search'
313
- ?++this.#searchGeneration
314
- :++this.#loadGeneration;
315
278
  const operationId=`${this.#events.instanceId}:${kind}:${(++this.#operationSequence).toString(36)}`;
316
279
  const controller=new AbortController();
317
280
  const record={
318
281
  cleanup:[],
319
282
  controller,
320
283
  errorPublished:false,
321
- generation,
322
284
  kind,
323
285
  operationId,
324
286
  publicError:null,
@@ -326,20 +288,7 @@ export default class OpenMeteoWeatherProvider extends EventTarget{
326
288
  terminalError:null
327
289
  };
328
290
  linkAbortSignal(signal??null,controller,record.cleanup);
329
- const previous=kind==='location-search'?this.#activeSearch:this.#activeLoad;
330
- if(kind==='location-search')this.#activeSearch=record;
331
- else this.#activeLoad=record;
332
291
  this.#operations.set(operationId,record);
333
- if(previous){
334
- this.#terminateOperation(
335
- previous,
336
- defineWeatherError(
337
- new Error(operationMessage(kind,'superseded')),
338
- operationContract(kind,'superseded'),
339
- operationMessage(kind,'superseded')
340
- )
341
- );
342
- }
343
292
  return record;
344
293
  }
345
294
 
@@ -348,7 +297,6 @@ export default class OpenMeteoWeatherProvider extends EventTarget{
348
297
  const operationId=event?.operationId??event?.detail?.requestId;
349
298
  if(typeof operationId!=='string'||!operationId)return;
350
299
  const record=this.#operations.get(operationId);
351
- if(record&&!this.#currentOperation(record))return;
352
300
  this.#dispatch(
353
301
  'request',
354
302
  {...(event.detail||{}),operation:record?.kind??kind},
@@ -383,37 +331,30 @@ export default class OpenMeteoWeatherProvider extends EventTarget{
383
331
  this.#assertOpen();
384
332
  if(geocoding)this.geocoder.setEndpoint(geocoding);
385
333
  if(forecast)this.forecast.setEndpoint(forecast);
386
- return Object.freeze({
334
+ return {
387
335
  geocoding:this.geocoder.endpoint,
388
336
  forecast:this.forecast.endpoint
389
- });
337
+ };
390
338
  }
391
339
 
392
340
  async search(query,optionsValue={}){
393
341
  this.#assertOpen();
394
- const name=String(query||'').trim();
395
- if(name.length<2){
342
+ const name=String(query??'');
343
+ if(!name.trim()){
396
344
  throw defineWeatherError(
397
- new TypeError('Enter at least two characters for a location search.'),
345
+ new TypeError('Enter a location to search.'),
398
346
  OPEN_METEO_WEATHER_ERRORS.weatherLocationQueryInvalid,
399
- 'Enter at least two characters for a location search.'
347
+ 'Enter a location to search.'
400
348
  );
401
349
  }
402
350
  const options=operationOptions(optionsValue);
403
351
  const record=this.#startOperation('location-search',options.signal??null);
404
352
  try{
405
353
  const result=await this.geocoder.fetch(
406
- {name,count:8,language:'en',format:'json'},
354
+ {name,language:'en',format:'json'},
407
355
  {},
408
356
  {signal:record.controller.signal,operationId:record.operationId}
409
357
  );
410
- if(!this.#currentOperation(record)){
411
- throw record.terminalError??defineWeatherError(
412
- new Error(operationMessage(record.kind,'superseded')),
413
- operationContract(record.kind,'superseded'),
414
- operationMessage(record.kind,'superseded')
415
- );
416
- }
417
358
  this.#finishOperation(record);
418
359
  this.#dispatch(
419
360
  'locations',
@@ -450,8 +391,12 @@ export default class OpenMeteoWeatherProvider extends EventTarget{
450
391
  const {
451
392
  temperatureUnit='fahrenheit',
452
393
  windSpeedUnit='mph',
453
- precipitationUnit='inch'
394
+ precipitationUnit='inch',
395
+ forecastDays
454
396
  }=options;
397
+ if(forecastDays!==undefined&&(!Number.isSafeInteger(forecastDays)||forecastDays<=0)){
398
+ throw invalidOptionsError('Open-Meteo forecastDays must be a positive integer when provided.');
399
+ }
455
400
  const record=this.#startOperation('forecast-load',options.signal??null);
456
401
  try{
457
402
  const result=await this.forecast.fetch(
@@ -459,7 +404,7 @@ export default class OpenMeteoWeatherProvider extends EventTarget{
459
404
  latitude:place.latitude,
460
405
  longitude:place.longitude,
461
406
  timezone:'auto',
462
- forecast_days:7,
407
+ ...(forecastDays===undefined?{}:{forecast_days:forecastDays}),
463
408
  temperature_unit:temperatureUnit,
464
409
  wind_speed_unit:windSpeedUnit,
465
410
  precipitation_unit:precipitationUnit,
@@ -484,13 +429,6 @@ export default class OpenMeteoWeatherProvider extends EventTarget{
484
429
  {location:place},
485
430
  {signal:record.controller.signal,operationId:record.operationId}
486
431
  );
487
- if(!this.#currentOperation(record)){
488
- throw record.terminalError??defineWeatherError(
489
- new Error(operationMessage(record.kind,'superseded')),
490
- operationContract(record.kind,'superseded'),
491
- operationMessage(record.kind,'superseded')
492
- );
493
- }
494
432
  this.#finishOperation(record);
495
433
  this.#dispatch(
496
434
  'weather',