arcane-os 0.5.18 → 0.5.19

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,6 +1,17 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 0.5.19
4
+
5
+ - Report observed Wllama initialization stages and runtime activity through
6
+ direct provider loading and the shared AI lifecycle. Keep initialization
7
+ indeterminate when the runtime supplies no meaningful completion total.
8
+ - Show the current initialization stage, stage duration, and time since runtime
9
+ activity in shared chat instead of a completed file count during activation.
10
+ Preserve download progress, cancellation, and complete runtime logging.
11
+ - Remove an adjacent duplicate cancellation check while retaining cancellation
12
+ handling before initialization and after loading.
13
+
14
+ ## 0.5.18
4
15
 
5
16
  - Use `strong-type` predicates throughout SDK-owned toolchain, runtime,
6
17
  browser-provider, and component code while retaining existing defaults,
@@ -12,8 +23,6 @@
12
23
  - Remove `AIResponseLength` exports; applications own response verbosity.
13
24
  Parse URL-audit HTML with the native HTML parser.
14
25
 
15
- ## 0.5.18
16
-
17
26
  - Add `AI.prepareTTS()` for detached punctuation-segmented synthesis, optional
18
27
  DBOPFS audio storage, complete semantic-input reuse, shared pending work and
19
28
  independent preparation cancellation. Persist MIME metadata with each audio
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.5.18` SDK contract. Applications pin one exact npm
22
+ This checkout defines the `0.5.19` SDK contract. Applications pin one exact npm
23
23
  version and lockfile; registry state is deliberately not baked into application
24
24
  artifacts.
25
25
 
@@ -35,7 +35,7 @@ Create one browser application, install its pinned SDK, and start its source
35
35
  server:
36
36
 
37
37
  ```bash
38
- npx arcane-os@0.5.18 new hello-speech --path ./hello-speech --target browser
38
+ npx arcane-os@0.5.19 new hello-speech --path ./hello-speech --target browser
39
39
  cd hello-speech
40
40
  npm install
41
41
  npm run dev
@@ -357,7 +357,7 @@ uses the same controller for automatic memory extraction.
357
357
  Create a new repository-shaped Arcane application with the exact stable SDK:
358
358
 
359
359
  ```bash
360
- npx arcane-os@0.5.18 new my-app --path ./my-app --target portable --git
360
+ npx arcane-os@0.5.19 new my-app --path ./my-app --target portable --git
361
361
  cd my-app
362
362
  npm install
363
363
  npm run dev
@@ -367,7 +367,7 @@ To enroll an existing repository, install the exact SDK and initialize only
367
367
  missing Arcane files:
368
368
 
369
369
  ```bash
370
- npm install --save-dev --save-exact arcane-os@0.5.18
370
+ npm install --save-dev --save-exact arcane-os@0.5.19
371
371
  npm exec -- arcane init my-app --target portable
372
372
  ```
373
373
 
@@ -383,7 +383,7 @@ npm exec -- arcane-os targets
383
383
  No global SDK install or standalone Arcane CLI is required. The application
384
384
  repository's exact npm dependency and lockfile own the CLI and toolchain version.
385
385
 
386
- Use `npx arcane-os@0.5.18` for the initial bootstrap because it names this npm
386
+ Use `npx arcane-os@0.5.19` for the initial bootstrap because it names this npm
387
387
  package explicitly; bare `npx arcane` outside an installed project could resolve
388
388
  a different package. Both installed commands invoke the same headless toolchain.
389
389
  Project-local npm scripts use the SDK pinned by that app's `package-lock.json`,
@@ -403,7 +403,7 @@ node ./bin/arcane.mjs new local-app --path ../local-app --target portable --git
403
403
 
404
404
  # From the generated app repository
405
405
  cd ../local-app
406
- npm install --save-dev --save-exact ../arcane-os-sdk/arcane-os-0.5.18.tgz
406
+ npm install --save-dev --save-exact ../arcane-os-sdk/arcane-os-0.5.19.tgz
407
407
  npm ci
408
408
  ```
409
409
 
@@ -412,7 +412,7 @@ same location. The lockfile retains the selected package dependency while
412
412
  Arcane uses the installed package name and version. Local directory `file:` dependencies are not
413
413
  accepted because npm may install them as links; use a packed `.tgz`. A GitHub
414
414
  runner also needs that tarball at the locked path. After publication, replace
415
- the local declaration with the exact `arcane-os@0.5.18` registry package and
415
+ the local declaration with the exact `arcane-os@0.5.19` registry package and
416
416
  commit the regenerated lock.
417
417
 
418
418
  Generated repositories use `npm ci --ignore-scripts` in CI. Run dependency
@@ -551,7 +551,7 @@ package installation, or assertions.
551
551
 
552
552
  ## Current target support
553
553
 
554
- Version `0.5.18` exposes one browser target and five explicitly paired
554
+ Version `0.5.19` exposes one browser target and five explicitly paired
555
555
  native development targets: a non-runnable portable directory, a
556
556
  Windows x64 unsigned-local-test EXE bundle, Linux x64 and Linux ARM64
557
557
  unsigned-local-test DEBs, and an Android development-signed APK. The
@@ -3000,24 +3000,47 @@ export function createBrowserWasmLlmProvider({
3000
3000
  ? context.reportProgress
3001
3001
  : options.onProgress ?? null;
3002
3002
  const progressStartedAt = Date.now();
3003
+ let progressStageStartedAt = progressStartedAt;
3004
+ let progressUpdatedAt = progressStartedAt;
3003
3005
  let currentProgress = null;
3004
3006
  let progressHeartbeat = null;
3005
3007
 
3006
3008
  function publishModelLoadProgress(progress) {
3007
- if (!reportProgress) return;
3009
+ if (!reportProgress || signal.aborted || generation !== lifecycleGeneration || state !== "loading") return;
3010
+ const now = Date.now();
3011
+ if (currentProgress?.phase !== progress.phase || currentProgress?.stage !== progress.stage) {
3012
+ progressStageStartedAt = now;
3013
+ }
3014
+ progressUpdatedAt = now;
3008
3015
  currentProgress = { ...progress, heartbeat: false };
3009
3016
  reportProgress(completeValue({
3010
3017
  ...currentProgress,
3011
- elapsedMs: Math.max(0, Date.now() - progressStartedAt),
3018
+ elapsedMs: Math.max(0, now - progressStartedAt),
3019
+ ...(progress.phase === "initialize" ? {
3020
+ phaseElapsedMs: Math.max(0, now - progressStageStartedAt),
3021
+ activityElapsedMs: 0,
3022
+ } : {}),
3012
3023
  }));
3013
3024
  }
3014
3025
 
3026
+ function publishModelInitializationProgress(progress) {
3027
+ if (!reportProgress || signal.aborted || generation !== lifecycleGeneration || state !== "loading") return;
3028
+ progressUpdatedAt = Date.now();
3029
+ if (!progress || (currentProgress?.stage === progress.stage && currentProgress?.message === progress.message)) return;
3030
+ publishModelLoadProgress(progress);
3031
+ }
3032
+
3015
3033
  function publishModelLoadHeartbeat() {
3016
- if (!reportProgress || !currentProgress) return;
3034
+ if (!reportProgress || !currentProgress || signal.aborted || generation !== lifecycleGeneration || state !== "loading") return;
3035
+ const now = Date.now();
3017
3036
  reportProgress(completeValue({
3018
3037
  ...currentProgress,
3019
3038
  heartbeat: true,
3020
- elapsedMs: Math.max(0, Date.now() - progressStartedAt),
3039
+ elapsedMs: Math.max(0, now - progressStartedAt),
3040
+ ...(currentProgress.phase === "initialize" ? {
3041
+ phaseElapsedMs: Math.max(0, now - progressStageStartedAt),
3042
+ activityElapsedMs: Math.max(0, now - progressUpdatedAt),
3043
+ } : {}),
3021
3044
  }));
3022
3045
  }
3023
3046
 
@@ -3047,16 +3070,12 @@ export function createBrowserWasmLlmProvider({
3047
3070
  if (generation !== lifecycleGeneration || state !== "loading") {
3048
3071
  throw fail("ARCANE_AI_OPERATION_SUPERSEDED", "The model load was superseded by unload.");
3049
3072
  }
3050
- throwIfAborted(signal, "load");
3051
- if (generation !== lifecycleGeneration || state !== "loading") {
3052
- throw fail("ARCANE_AI_OPERATION_SUPERSEDED", "The model load was superseded by unload.");
3053
- }
3054
3073
  const members = sourceMetadata(activeSource).files;
3055
3074
  publishModelLoadProgress({
3056
3075
  phase: "initialize",
3057
- completed: members.length,
3058
- total: members.length,
3059
- unit: "files",
3076
+ stage: "runtime",
3077
+ message: "Starting the WebAssembly runtime and opening model files",
3078
+ total: null,
3060
3079
  heartbeat: false,
3061
3080
  });
3062
3081
  const modelFiles = admitted.files.map((file, index) => (
@@ -3074,6 +3093,7 @@ export function createBrowserWasmLlmProvider({
3074
3093
  ...runtimeOptions,
3075
3094
  ...activeLoadPlan,
3076
3095
  signal,
3096
+ onProgress: publishModelInitializationProgress,
3077
3097
  });
3078
3098
  if (!runtime.isLoaded()) {
3079
3099
  throw fail(
@@ -94,6 +94,59 @@ function createEvidenceLogger(logger) {
94
94
  let offload = null;
95
95
  let invalid = false;
96
96
  let completionCapture = null;
97
+ let loadProgress = null;
98
+ let tensorCount = null;
99
+
100
+ function observeLoadLine(line) {
101
+ if (!loadProgress) return;
102
+ let stage;
103
+ let message;
104
+ const metadata = line.match(/^llama_model_loader: loaded meta data with \d+ key-value pairs and (\d+) tensors from /u);
105
+ const layers = line.match(GPU_OFFLOAD_PATTERN);
106
+ if (line.startsWith('Loading "wllama.wasm" from ')) {
107
+ stage = "runtime";
108
+ message = "Loading the WebAssembly runtime";
109
+ } else if (line === "Calling wllamaStart...") {
110
+ stage = "backend";
111
+ message = "Starting the inference engine";
112
+ } else if (line === "Loading model...") {
113
+ stage = "metadata";
114
+ message = "Reading model metadata";
115
+ } else if (WEBGPU_ADAPTER_PATTERN.test(line)) {
116
+ stage = "gpu";
117
+ message = "Graphics device initialized; preparing the model";
118
+ } else if (metadata) {
119
+ tensorCount = Number(metadata[1]);
120
+ stage = "metadata";
121
+ message = `Model metadata read: ${tensorCount} tensors`;
122
+ } else if (line.startsWith("load_tensors: loading model tensors,")) {
123
+ stage = "weights";
124
+ message = tensorCount === null
125
+ ? "Loading model weights"
126
+ : `Loading model weights for ${tensorCount} tensors`;
127
+ } else if (layers) {
128
+ // Upstream reports layer assignment before the weight reads finish.
129
+ stage = "weights";
130
+ message = `Loading model weights; ${layers[1]} of ${layers[2]} layers assigned to the GPU`;
131
+ } else if (line === "llama_context: constructing llama_context") {
132
+ stage = "context";
133
+ message = "Preparing the inference context";
134
+ } else if (/^[^:]+:\s+graph (?:nodes|splits)\s+=\s+\d+/u.test(line)) {
135
+ stage = "graph";
136
+ message = "Preparing the inference graph";
137
+ } else if (/^cmn\s+common_init_:\s+warming up the model with an empty run\b/u.test(line)) {
138
+ stage = "warmup";
139
+ message = "Warming up the model with an empty run";
140
+ }
141
+ if (stage) {
142
+ loadProgress({
143
+ phase: "initialize",
144
+ stage,
145
+ message,
146
+ total: null,
147
+ });
148
+ }
149
+ }
97
150
 
98
151
  function same(left, right) {
99
152
  return JSON.stringify(left) === JSON.stringify(right);
@@ -119,6 +172,7 @@ function createEvidenceLogger(logger) {
119
172
  observeCompletionLine(level, value);
120
173
  const line = String(value).trim();
121
174
  if (!line) return;
175
+ observeLoadLine(line);
122
176
  const adapterMatch = line.match(WEBGPU_ADAPTER_PATTERN);
123
177
  if (adapterMatch) {
124
178
  const next = completeValue({
@@ -144,6 +198,8 @@ function createEvidenceLogger(logger) {
144
198
  }
145
199
 
146
200
  function observe(level, args) {
201
+ // Activity without a new stage updates its age without repainting the UI.
202
+ loadProgress?.(null);
147
203
  for (const value of args) {
148
204
  if (!is.string(value)) continue;
149
205
  for (const line of value.split(/\r?\n/u)) observeLine(level, line);
@@ -160,6 +216,13 @@ function createEvidenceLogger(logger) {
160
216
 
161
217
  return completeValue({
162
218
  logger: completeValue(wrapped),
219
+ beginLoadProgress(report) {
220
+ loadProgress = report;
221
+ tensorCount = null;
222
+ return function releaseLoadProgress() {
223
+ loadProgress = null;
224
+ };
225
+ },
163
226
  beginCompletionCapture() {
164
227
  if (completionCapture) {
165
228
  throw runtimeFailure(
@@ -469,6 +532,18 @@ export function createPackagedWllamaRuntime({ logger = arcaneLogging } = {}) {
469
532
  cleanup: null,
470
533
  });
471
534
  const loadController = new AbortController();
535
+ let progressFailure = null;
536
+ const releaseLoadProgress = sessionObservers.get(next).beginLoadProgress(
537
+ function reportRuntimeLoadProgress(progress) {
538
+ if (loadController.signal.aborted || !is.function(options.onProgress)) return;
539
+ try {
540
+ options.onProgress(progress);
541
+ } catch (error) {
542
+ progressFailure = error;
543
+ loadController.abort(error);
544
+ }
545
+ },
546
+ );
472
547
  const loadOperation = Promise.resolve().then(() => (
473
548
  next.arcaneLoadModel(files, loadOptions, loadController.signal)
474
549
  ));
@@ -484,6 +559,7 @@ export function createPackagedWllamaRuntime({ logger = arcaneLogging } = {}) {
484
559
  else signal?.addEventListener?.("abort", onAbort, { once: true });
485
560
  try {
486
561
  await loadOperation;
562
+ if (progressFailure) throw progressFailure;
487
563
  if (pending?.engine !== next) throw new Error("Wllama load was cancelled.");
488
564
  if (!is.function(next.isModelLoaded) || next.isModelLoaded() !== true) {
489
565
  throw runtimeFailure(
@@ -496,6 +572,7 @@ export function createPackagedWllamaRuntime({ logger = arcaneLogging } = {}) {
496
572
  engine = next;
497
573
  publishEvidence({ state: "ready", webgpu, cancellation: null, cleanup: null });
498
574
  } catch (error) {
575
+ releaseLoadProgress();
499
576
  let cleanupFailure = null;
500
577
  try {
501
578
  await exitSession(next);
@@ -513,6 +590,7 @@ export function createPackagedWllamaRuntime({ logger = arcaneLogging } = {}) {
513
590
  if (cleanupFailure) throw cleanupFailure;
514
591
  throw error;
515
592
  } finally {
593
+ releaseLoadProgress();
516
594
  signal?.removeEventListener?.("abort", onAbort);
517
595
  }
518
596
 
@@ -279,7 +279,7 @@ paths are withheld from the native provider. The provider copies the complete
279
279
  selected release rather than accepting an unrelated source path. Verification
280
280
  is a separate explicit operation for a selected release artifact.
281
281
 
282
- The SDK `0.5.17` runtime requires Arcane `0.8.12` or newer. Compatibility
282
+ The SDK `0.5.19` runtime requires Arcane `0.8.12` or newer. Compatibility
283
283
  is contractual rather than exact-version pinning: the prepared Core must meet
284
284
  the highest minimum declared by the runtime, selected app, and bundled app
285
285
  dependencies; keep each app's Arcane protocol generation; and provide every
@@ -16,7 +16,13 @@ dependency and invoke its local CLI with `npm exec -- arcane`. A separate
16
16
  global installer, standalone SDK executable, NuGet package, Homebrew formula,
17
17
  or OS package is not part of this release surface.
18
18
 
19
- Publication checks run only after the user explicitly selects an npm release.
19
+ The user's standing instruction selects publication when a coherent SDK change
20
+ is complete. Publish every ready change that preserves required functionality
21
+ and remains relevant, while excluding unfinished concurrent or explicitly
22
+ deferred work. Default to a patch release and assess whether a new capability
23
+ warrants a minor revision. Honor an explicit no-publish instruction.
24
+
25
+ Publication checks run only for that selected npm release output.
20
26
  That selected-release workflow validates package metadata, the executable and
21
27
  `.gitattributes` boundary, the complete package inventory, version/channel
22
28
  agreement, and required license notices. One unprivileged producer packs one
@@ -94,9 +100,9 @@ locked installation, runs one selected `arcane package`, creates one selected
94
100
  identities, receipts, provenance records, or attestation sidecars and does not
95
101
  run a second admission job.
96
102
 
97
- Stable versioning, the npm `latest` tag, and an official GitHub release remain a
98
- separate explicit release decision. Current `main` development does not
99
- silently convert a `-dev` package into an official release. A stable release
103
+ Stable versioning, the npm `latest` tag, and an official GitHub release follow
104
+ the selected release decision, including the standing completed-work authority
105
+ above. Unfinished `main` development does not select a release. A stable release
100
106
  must publish the exact selected Check artifact under `latest`; GitHub may then
101
107
  attach that same package. Its Git
102
108
  tag and GitHub release title must both be the same bare numeric
@@ -101,10 +101,31 @@ active-worker count changes, and the download completes. At known completion, re
101
101
  and active transfers are zero. Applications format the raw measures into B,
102
102
  KB, MB, GB, transfer-rate, and duration labels.
103
103
 
104
- While a load remains active, the provider also repeats its current record every
105
- five seconds with `heartbeat:true` and an updated `elapsedMs`. A heartbeat
106
- confirms that the owned operation is still active; it does not invent
107
- additional transferred content.
104
+ Initialization keeps `phase:'initialize'` and reports a `stage` and a readable
105
+ `message` from the packaged runtime's existing log signals: runtime startup,
106
+ engine startup, model metadata, graphics-device initialization, model weights,
107
+ inference context, and inference graph. Warmup is shown only if the runtime
108
+ actually reports it; the default upstream log level may omit that signal.
109
+ These messages do not change the runtime's log level or replace its complete
110
+ diagnostic output.
111
+
112
+ Initialization has `total:null` and omits `completed` and `unit`, so its progress
113
+ bar is indeterminate. Downloaded-file completion is not activation completion.
114
+ The metadata tensor count and GPU layer assignment are descriptive only:
115
+ upstream reports assigned layers before model-weight loading has finished.
116
+ The pinned runtime does not expose a measured overall initialization fraction
117
+ or a shader-compilation completion count. Readiness still requires the load
118
+ operation to resolve and the runtime to confirm the model is loaded.
119
+
120
+ While a load remains active, the provider repeats its current record every five
121
+ seconds with `heartbeat:true` and an updated `elapsedMs`. During initialization,
122
+ `phaseElapsedMs` measures time in the current stage and `activityElapsedMs`
123
+ measures time since the most recent runtime log signal. A signal without a
124
+ recognized stage updates activity time without triggering another UI render.
125
+ The shared chat loading panel shows the observed stage and those timings. A
126
+ heartbeat only shows that the provider's timer ran; it does not establish
127
+ runtime advancement, estimate a completion time, or advance the progress bar.
128
+ Late runtime progress is ignored after cancellation or supersession.
108
129
 
109
130
  The DBOPFS store uses one bounded transfer axis. Ordered multi-file GGUF sets
110
131
  download several members concurrently and preserve completed shards across a
@@ -470,8 +491,10 @@ identity, default model metadata,
470
491
  `streamChat`, `use`, `probe`, and `dispose`. Direct provider `load()` selects a
471
492
  catalog model and returns `{model,status}`;
472
493
  the public AI API module's `ai.load()` returns the flat controller status.
473
- Direct `load({onProgress})` forwards the same additive file and byte progress
474
- records used by the controller and provider/2 adapter.
494
+ Direct `load({onProgress})` forwards the same download progress and observed
495
+ initialization-stage/activity records used by the controller and provider/2
496
+ adapter. Initialization has no completion percentage unless the runtime reports
497
+ a meaningful completion total.
475
498
  Provider `security` carries the provider/model-binding `secure` intent. Direct
476
499
  `provider.load({security})` and `ai.load({security})` supply the operation
477
500
  intent. They do not activate checking in the ordinary development contract.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-os",
3
- "version": "0.5.18",
3
+ "version": "0.5.19",
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",
@@ -2039,6 +2039,20 @@
2039
2039
  }
2040
2040
 
2041
2041
  function determinateProgress(){
2042
+ if(role?.progress?.phase==='initialize'){
2043
+ const {completed,total,unit}=role.progress;
2044
+ if(
2045
+ !is.finite(total)
2046
+ ||total<=0
2047
+ ||!is.finite(completed)
2048
+ ||completed<0
2049
+ ||!is.string(unit)
2050
+ ||!unit
2051
+ ){
2052
+ return null;
2053
+ }
2054
+ return {completed,total,unit,byteTelemetry:false};
2055
+ }
2042
2056
  const totalBytes=role?.progress?.totalBytes;
2043
2057
  const loadedBytes=role?.progress?.loadedBytes;
2044
2058
  if(
@@ -2160,8 +2174,10 @@
2160
2174
  return `${active} of ${limit} ${noun}${limit===1?'':'s'} active`;
2161
2175
  }
2162
2176
 
2163
- function progressElapsedText(){
2164
- const elapsedMilliseconds=Number(role?.progress?.elapsedMs);
2177
+ function progressElapsedText(
2178
+ elapsedMilliseconds,
2179
+ suffix='elapsed'
2180
+ ){
2165
2181
  if(!is.finite(elapsedMilliseconds)||elapsedMilliseconds<0){
2166
2182
  return '';
2167
2183
  }
@@ -2170,12 +2186,12 @@
2170
2186
  const minutes=Math.floor(elapsedSeconds%3600/60);
2171
2187
  const seconds=elapsedSeconds%60;
2172
2188
  if(hours>0){
2173
- return `${hours}h ${minutes}m elapsed`;
2189
+ return `${hours}h ${minutes}m ${suffix}`;
2174
2190
  }
2175
2191
  if(minutes>0){
2176
- return `${minutes}m ${String(seconds).padStart(2,'0')}s elapsed`;
2192
+ return `${minutes}m ${String(seconds).padStart(2,'0')}s ${suffix}`;
2177
2193
  }
2178
- return `${seconds}s elapsed`;
2194
+ return `${seconds}s ${suffix}`;
2179
2195
  }
2180
2196
 
2181
2197
  function progressMessage(){
@@ -2183,7 +2199,14 @@
2183
2199
  const phaseValue=is.string(role?.progress?.phase)&&role.progress.phase
2184
2200
  ?role.progress.phase
2185
2201
  :'loading';
2186
- const phase=phaseValue.replaceAll('-',' ');
2202
+ const initializing=phaseValue==='initialize';
2203
+ const phase=initializing
2204
+ ?is.string(role?.progress?.message)&&role.progress.message
2205
+ ?role.progress.message
2206
+ :is.string(role?.progress?.stage)&&role.progress.stage
2207
+ ?role.progress.stage.replaceAll('-',' ')
2208
+ :'Initializing the selected model'
2209
+ :phaseValue.replaceAll('-',' ');
2187
2210
  const parts=[
2188
2211
  `Loading ${modelDisplayName()} through the Arcane SDK`,
2189
2212
  phase
@@ -2211,18 +2234,38 @@
2211
2234
  :measured.unit;
2212
2235
  parts.push(`${measured.completed} of ${measured.total} ${suffix}`);
2213
2236
  }
2214
- const transfers=activeTransferText();
2237
+ const transfers=initializing?'':activeTransferText();
2215
2238
  if(transfers){
2216
2239
  parts.push(transfers);
2217
2240
  }
2218
- const elapsed=progressElapsedText();
2241
+ const elapsed=progressElapsedText(Number(role?.progress?.elapsedMs));
2219
2242
  if(elapsed){
2220
2243
  parts.push(elapsed);
2221
- if(role?.progress?.heartbeat===true){
2244
+ if(!initializing&&role?.progress?.heartbeat===true){
2222
2245
  parts.push('Still working');
2223
2246
  }
2224
2247
  }
2225
- if(!measured?.byteTelemetry){
2248
+ if(initializing){
2249
+ const phaseElapsed=progressElapsedText(
2250
+ role.progress.phaseElapsedMs,
2251
+ 'in this stage'
2252
+ );
2253
+ if(phaseElapsed){
2254
+ parts.push(phaseElapsed);
2255
+ }
2256
+ if(role.progress.heartbeat===true){
2257
+ const activityElapsed=progressElapsedText(
2258
+ role.progress.activityElapsedMs,
2259
+ 'ago'
2260
+ );
2261
+ if(activityElapsed){
2262
+ parts.push(`Last runtime activity ${activityElapsed}`);
2263
+ }
2264
+ }
2265
+ parts.push(measured
2266
+ ?'Keep this tab open'
2267
+ :'Completion percentage unavailable; keep this tab open');
2268
+ }else if(!measured?.byteTelemetry){
2226
2269
  parts.push('The first activation can take several minutes; keep this tab open');
2227
2270
  }
2228
2271
  return parts.join(' · ');