@amalgm/shell 0.1.73 → 0.1.74

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.
@@ -6,8 +6,10 @@ import { buildUserHomeManifest, buildUserManifest, liveMachineStateDir, scopedAm
6
6
  import { CHUNK_BYTES, CONTENT_CONTRACT, ENTITY_CLOUD_CONTRACT, ENTITY_CLOUD_SCHEMA_VERSION, artifactForRecord, canonicalVersion, checkContentManifest, classifyDirectory, classifyFile, classifyRegisteredRoot, createLocalEntityRecord, createEntityRecordAuthorityPort, convergeUserGround, createUserGroundEnrollmentPolicy, isRepositoryMetadataEntry, indexRepositoryTerritory, download as downloadArtifact, encodeEntityRecord, EntityApplyRail, EntityRecordRail, membershipHash, parseSnapshot, pathIsSuspect, privateEntityResourceId, rootReplacementFromRecords, sameRecords, snapshotFromRecords, stableJson, travelingRecords, upload as uploadArtifact, userGroundRecords, } from "@amalgm/live";
7
7
  import Database from "better-sqlite3";
8
8
  import { atomicCopy, atomicWrite, ensurePrivateDir } from "./filesystem.js";
9
+ import { emitFilesStage, } from "./files-observability.js";
9
10
  import { EntityApplyHost, } from "./entity-apply-host.js";
10
11
  import { EntityRecordSqliteStore } from "./entity-record-store.js";
12
+ import { observeEntityRecordPorts } from "./entity-record-observability.js";
11
13
  import { indexRowsByOutermostRoot } from "./ground-root-index.js";
12
14
  import { ContentCacheDownload, captureContentFile, hashContentFile, sealCapturedArtifact, } from "./content-cache-host.js";
13
15
  import { decodeContentWireBytes, encodeContentWireBytes } from "./content-wire-codec.js";
@@ -67,7 +69,29 @@ export class UserGroundHost {
67
69
  this.watchHost = new NodeWatchHost({
68
70
  open: options.openWatch,
69
71
  onSignal: () => this.scheduleRescan(),
70
- onHealth: options.onWatchHealth,
72
+ onEvent: (evidence) => this.stage({
73
+ primitive: "watch",
74
+ stage: "native-suspicion",
75
+ status: "observed",
76
+ path: evidence.path === null
77
+ ? evidence.directory
78
+ : join(evidence.directory, ...evidence.path.split("/")),
79
+ measurements: { kind: evidence.kind, named: evidence.path !== null },
80
+ }),
81
+ onHealth: (health) => {
82
+ options.onWatchHealth?.(health);
83
+ this.stage({
84
+ primitive: "watch",
85
+ stage: "coverage-health",
86
+ status: "observed",
87
+ measurements: {
88
+ state: health.state,
89
+ logicalRoots: health.logicalRoots,
90
+ contentHandles: health.contentHandles,
91
+ addressHandles: health.addressHandles,
92
+ },
93
+ });
94
+ },
71
95
  });
72
96
  this.port = {
73
97
  converge: async (identity) => {
@@ -80,7 +104,29 @@ export class UserGroundHost {
80
104
  local,
81
105
  });
82
106
  await this.resumeWorkspaceAdds(this.activeIdentity);
83
- this.namedDetect ??= new NamedDetectRuntime(this.databasePath(this.activeIdentity), this.cacheDir(this.activeIdentity), this.userRoot(this.activeIdentity));
107
+ this.namedDetect ??= new NamedDetectRuntime(this.databasePath(this.activeIdentity), this.cacheDir(this.activeIdentity), this.userRoot(this.activeIdentity), ({ durationMs, records }) => {
108
+ if (records.length === 0) {
109
+ this.stage({
110
+ primitive: "record",
111
+ stage: "sqlite-transaction",
112
+ status: "completed",
113
+ durationMs,
114
+ measurements: { records: 0 },
115
+ });
116
+ return;
117
+ }
118
+ for (const record of records) {
119
+ this.stage({
120
+ primitive: "record",
121
+ stage: "outbox-committed",
122
+ status: "completed",
123
+ entityId: record.entityId,
124
+ mutationId: record.mutationId,
125
+ durationMs,
126
+ measurements: { records: records.length },
127
+ });
128
+ }
129
+ });
84
130
  const watchHealth = this.watchHost.health();
85
131
  if (watchHealth.state !== "healthy") {
86
132
  throw new Error(`Watch coverage is degraded: ${watchHealth.reason ?? "unknown failure"}`);
@@ -103,6 +149,30 @@ export class UserGroundHost {
103
149
  },
104
150
  };
105
151
  }
152
+ stage(input) {
153
+ emitFilesStage(this.options.onFilesStage, input);
154
+ }
155
+ reportDetectScan(evidence) {
156
+ this.options.onDetectScan?.(evidence);
157
+ this.stage({
158
+ primitive: "detect",
159
+ stage: "scan",
160
+ status: "completed",
161
+ rootId: evidence.rootId,
162
+ path: evidence.directory,
163
+ durationMs: evidence.durationMs,
164
+ measurements: {
165
+ scope: evidence.scope,
166
+ entries: evidence.entries,
167
+ metadataReused: evidence.metadataReused,
168
+ contentReads: evidence.contentReads,
169
+ gitInspections: evidence.gitInspections,
170
+ repositoryCaptures: evidence.repositoryCaptures,
171
+ records: evidence.records ?? 0,
172
+ recordBytes: evidence.recordBytes ?? 0,
173
+ },
174
+ });
175
+ }
106
176
  databasePath(identity) {
107
177
  return join(liveMachineStateDir(this.userRoot(identity), identity.deviceId), "identity.db");
108
178
  }
@@ -151,14 +221,39 @@ export class UserGroundHost {
151
221
  });
152
222
  const add = {
153
223
  lookupRegistry: async () => {
224
+ const started = performance.now();
225
+ this.stage({
226
+ primitive: "download", stage: "registry-lookup", status: "started",
227
+ });
154
228
  const resourceId = privateEntityResourceId(identity.userId, sha256Hex);
155
229
  const cloud = await this.cloudPort(identity).lookup(resourceId);
156
230
  if (!cloud)
157
231
  throw new Error("this user has no cloud entity registry");
232
+ this.stage({
233
+ primitive: "download",
234
+ stage: "registry-lookup",
235
+ status: "completed",
236
+ durationMs: performance.now() - started,
237
+ });
158
238
  return cloud;
159
239
  },
160
240
  materializeWorkspace: async (input) => {
241
+ const groundWaitStarted = performance.now();
242
+ this.stage({
243
+ primitive: "download",
244
+ stage: "ground-wait",
245
+ status: "started",
246
+ workspaceId: input.workspace.uuid,
247
+ });
161
248
  const release = await this.acquireGroundEffect();
249
+ this.stage({
250
+ primitive: "download",
251
+ stage: "ground-wait",
252
+ status: "completed",
253
+ workspaceId: input.workspace.uuid,
254
+ durationMs: performance.now() - groundWaitStarted,
255
+ });
256
+ let milestoneAt = performance.now();
162
257
  try {
163
258
  const installed = await installCloudWorkspace({
164
259
  identity,
@@ -173,11 +268,22 @@ export class UserGroundHost {
173
268
  records: input.records,
174
269
  destinationParent: input.destinationParent,
175
270
  cloudHead: input.cloudHead,
176
- readContent: (artifact) => this.downloadContent(privateEntityResourceId(identity.userId, sha256Hex), this.cacheDir(identity), artifact),
177
- onStage: (stage) => this.options.onWorkspaceAddStage?.({
178
- workspaceId: input.workspace.uuid,
179
- stage,
180
- }),
271
+ readContent: (artifact) => this.downloadContent(privateEntityResourceId(identity.userId, sha256Hex), this.cacheDir(identity), artifact, 10, { journey: "add", workspaceId: input.workspace.uuid }),
272
+ onStage: async (stage) => {
273
+ const now = performance.now();
274
+ this.stage({
275
+ primitive: stage === "watching" ? "watch" : "download",
276
+ stage,
277
+ status: "completed",
278
+ workspaceId: input.workspace.uuid,
279
+ durationMs: now - milestoneAt,
280
+ });
281
+ milestoneAt = now;
282
+ await this.options.onWorkspaceAddStage?.({
283
+ workspaceId: input.workspace.uuid,
284
+ stage,
285
+ });
286
+ },
181
287
  });
182
288
  // Add changes both disk and the exact SQLite projection. Open the
183
289
  // selected root's singular Watch coverage before Detect can observe
@@ -192,6 +298,10 @@ export class UserGroundHost {
192
298
  }
193
299
  },
194
300
  coverWorkspace: async ({ workspaceId, baseline }) => {
301
+ const started = performance.now();
302
+ this.stage({
303
+ primitive: "watch", stage: "add-coverage", status: "started", workspaceId,
304
+ });
195
305
  const pending = readWorkspaceAddIntent(this.databasePath(identity), workspaceId);
196
306
  const health = this.ensureWatchers(identity, pending || baseline === "authoritative-install" ? [workspaceId] : []);
197
307
  assertHealthyWatch(this.watchHost.evidence(), workspaceId);
@@ -199,6 +309,17 @@ export class UserGroundHost {
199
309
  await this.options.onWorkspaceAddStage?.({ workspaceId, stage: "watching" });
200
310
  deleteWorkspaceAddIntent(this.databasePath(identity), workspaceId);
201
311
  }
312
+ this.stage({
313
+ primitive: "watch",
314
+ stage: "add-coverage",
315
+ status: "completed",
316
+ workspaceId,
317
+ durationMs: performance.now() - started,
318
+ measurements: {
319
+ contentHandles: health.contentHandles,
320
+ addressHandles: health.addressHandles,
321
+ },
322
+ });
202
323
  return { active: true, roots: health.contentHandles };
203
324
  },
204
325
  };
@@ -236,7 +357,7 @@ export class UserGroundHost {
236
357
  records: intent.records,
237
358
  destinationParent: dirname(intent.destinationPath),
238
359
  cloudHead: intent.cloudHead,
239
- readContent: (artifact) => this.downloadContent(intent.resourceId, this.cacheDir(identity), artifact),
360
+ readContent: (artifact) => this.downloadContent(intent.resourceId, this.cacheDir(identity), artifact, 10, { journey: "add", workspaceId: intent.workspaceId }),
240
361
  onStage: (stage) => this.options.onWorkspaceAddStage?.({
241
362
  workspaceId: intent.workspaceId,
242
363
  stage,
@@ -277,9 +398,24 @@ export class UserGroundHost {
277
398
  this.rescanGenerationStartedAt = null;
278
399
  let watch;
279
400
  try {
401
+ const watchStarted = performance.now();
402
+ this.stage({
403
+ primitive: "watch", stage: "register-coverage", status: "started", workspaceId,
404
+ });
280
405
  this.ensureWatchers(identity);
281
406
  watch = this.watchHost.evidence();
282
407
  assertHealthyWatch(watch, workspaceId);
408
+ this.stage({
409
+ primitive: "watch",
410
+ stage: "register-coverage",
411
+ status: "completed",
412
+ workspaceId,
413
+ durationMs: performance.now() - watchStarted,
414
+ measurements: {
415
+ contentHandles: watch.health.contentHandles,
416
+ addressHandles: watch.health.addressHandles,
417
+ },
418
+ });
283
419
  }
284
420
  catch (error) {
285
421
  this.finishColdOperation();
@@ -305,9 +441,20 @@ export class UserGroundHost {
305
441
  // a parallel stream of Detect records for the same initial contents.
306
442
  let releaseGround = null;
307
443
  try {
444
+ const groundWaitStarted = performance.now();
445
+ this.stage({
446
+ primitive: "register", stage: "ground-wait", status: "started", workspaceId,
447
+ });
308
448
  if (this.syncing)
309
449
  await this.syncing;
310
450
  releaseGround = await this.acquireGroundEffect();
451
+ this.stage({
452
+ primitive: "register",
453
+ stage: "ground-wait",
454
+ status: "completed",
455
+ workspaceId,
456
+ durationMs: performance.now() - groundWaitStarted,
457
+ });
311
458
  const contentOwner = watch.roots.find((root) => root.rootId === workspaceId)?.contentOwner;
312
459
  if (!contentOwner) {
313
460
  throw new Error(`registered workspace ${workspaceId} has no content coverage owner`);
@@ -315,12 +462,16 @@ export class UserGroundHost {
315
462
  const observations = this.watchHost.observations().filter((observation) => observation.directory === this.userRoot(identity)
316
463
  || pathWithin(contentOwner, observation.directory));
317
464
  this.watchDirty = false;
465
+ const registrationStarted = performance.now();
466
+ this.stage({
467
+ primitive: "register", stage: "entity-registration", status: "started", workspaceId,
468
+ });
318
469
  const scanned = await reconcileGround({
319
470
  identity,
320
471
  userRoot: this.userRoot(identity),
321
472
  database: this.databasePath(identity),
322
473
  cacheDir: this.cacheDir(identity),
323
- onScan: this.options.onDetectScan,
474
+ onScan: (evidence) => this.reportDetectScan(evidence),
324
475
  registrationWorkspaceId: workspaceId,
325
476
  suspicions: new Map(observations.map((observation) => [observation.directory, observation.suspicion])),
326
477
  });
@@ -328,17 +479,39 @@ export class UserGroundHost {
328
479
  if (retry?.kind === "retry") {
329
480
  throw new Error(retry.reasons.join("; "));
330
481
  }
482
+ this.stage({
483
+ primitive: "register",
484
+ stage: "entity-registration",
485
+ status: "completed",
486
+ workspaceId,
487
+ durationMs: performance.now() - registrationStarted,
488
+ measurements: {
489
+ roots: scanned.publicationRootIds?.length ?? 0,
490
+ rows: scanned.acceptedMaterializedRows.length,
491
+ },
492
+ });
493
+ const commitStarted = performance.now();
494
+ this.stage({
495
+ primitive: "register", stage: "entity-sqlite-commit", status: "started", workspaceId,
496
+ });
331
497
  commitDetectedState(this.databasePath(identity), {
332
498
  acceptedMaterializedRows: scanned.acceptedMaterializedRows,
333
499
  materializedRemovals: scanned.materializedRemovals,
334
500
  acceptedNotebookRows: scanned.acceptedNotebookRows,
335
501
  notebookRemovals: scanned.notebookRemovals,
336
502
  });
503
+ this.stage({
504
+ primitive: "register",
505
+ stage: "entity-sqlite-commit",
506
+ status: "completed",
507
+ workspaceId,
508
+ durationMs: performance.now() - commitStarted,
509
+ });
337
510
  this.namedDetect?.refreshEnrollmentPolicy();
338
511
  if (!scanned.publicationRootIds) {
339
512
  throw new Error("cold registration reconciliation did not name its publication roots");
340
513
  }
341
- await this.publishMaterializedSnapshot(identity, new Set(scanned.publicationRootIds));
514
+ await this.publishMaterializedSnapshot(identity, new Set(scanned.publicationRootIds), { journey: "register", workspaceId });
342
515
  this.watchHost.settle(observations);
343
516
  }
344
517
  catch (error) {
@@ -353,9 +526,21 @@ export class UserGroundHost {
353
526
  this.finishColdOperation();
354
527
  }
355
528
  try {
529
+ const finalWatchStarted = performance.now();
356
530
  this.ensureWatchers(identity);
357
531
  watch = this.watchHost.evidence();
358
532
  assertHealthyWatch(watch, workspaceId);
533
+ this.stage({
534
+ primitive: "watch",
535
+ stage: "register-final-proof",
536
+ status: "completed",
537
+ workspaceId,
538
+ durationMs: performance.now() - finalWatchStarted,
539
+ measurements: {
540
+ contentHandles: watch.health.contentHandles,
541
+ addressHandles: watch.health.addressHandles,
542
+ },
543
+ });
359
544
  }
360
545
  catch (error) {
361
546
  throw stageError("Watch coverage", error);
@@ -434,18 +619,28 @@ export class UserGroundHost {
434
619
  throw new Error("gateway does not support the event-driven entity Record wire");
435
620
  }
436
621
  const store = new EntityRecordSqliteStore(initializeDatabase(this.databasePath(identity)));
437
- const authority = createEntityRecordAuthorityPort({
622
+ const wireAuthority = createEntityRecordAuthorityPort({
438
623
  request: (frame, acceptedTypes) => this.wire.request({ ...frame }, acceptedTypes),
439
624
  onFrame: (listener) => this.wire.onEvent(listener),
440
625
  onDisconnect: (listener) => this.wire.onDisconnect(listener),
441
626
  });
442
- const rail = new EntityRecordRail({
443
- authorityId: this.cloudState.resourceId,
627
+ const ports = observeEntityRecordPorts({
444
628
  store,
445
- authority,
629
+ authority: wireAuthority,
446
630
  cargo: {
447
- ensure: (record) => this.uploadSnapshotContent(identity, record.authorityId, [record.change.result]),
631
+ ensure: (record) => this.uploadSnapshotContent(identity, record.authorityId, [record.change.result], {
632
+ journey: "send",
633
+ entityId: record.entityId,
634
+ mutationId: record.mutationId,
635
+ }),
448
636
  },
637
+ onStage: this.options.onFilesStage,
638
+ });
639
+ const rail = new EntityRecordRail({
640
+ authorityId: this.cloudState.resourceId,
641
+ store: ports.store,
642
+ authority: ports.authority,
643
+ cargo: ports.cargo,
449
644
  schedule: {
450
645
  schedule(delayMs, callback) {
451
646
  const timer = setTimeout(callback, delayMs);
@@ -482,7 +677,7 @@ export class UserGroundHost {
482
677
  userRoot: this.userRoot(identity),
483
678
  cacheDir: this.cacheDir(identity),
484
679
  bindingDir: workspaceBindingDir(this.userRoot(identity), identity.deviceId),
485
- readContent: (artifact) => this.downloadContent(this.cloudState.resourceId, this.cacheDir(identity), artifact, DOWNLOAD_CONTENT_RETRY_ATTEMPTS),
680
+ readContent: (artifact) => this.downloadContent(this.cloudState.resourceId, this.cacheDir(identity), artifact, DOWNLOAD_CONTENT_RETRY_ATTEMPTS, { journey: "apply" }),
486
681
  captureLocal: async (intent, plan, position) => {
487
682
  // Apply never suppresses Watch. This explicit whole-root suspicion is
488
683
  // the synchronous proof boundary used only when Apply observed a
@@ -495,7 +690,17 @@ export class UserGroundHost {
495
690
  }
496
691
  },
497
692
  acquireGround: () => this.acquireGroundEffect(APPLY_GROUND_WAIT_TIMEOUT_MS),
498
- onStage: (evidence) => this.options.onApplyStage?.(evidence),
693
+ onStage: (evidence) => {
694
+ this.options.onApplyStage?.(evidence);
695
+ this.stage({
696
+ primitive: "apply",
697
+ stage: evidence.stage,
698
+ status: "observed",
699
+ entityId: evidence.entityId,
700
+ globalSequence: evidence.throughSequence,
701
+ measurements: { type: evidence.type, phase: evidence.phase },
702
+ });
703
+ },
499
704
  now: () => this.options.now?.().getTime() ?? Date.now(),
500
705
  });
501
706
  const rail = new EntityApplyRail({
@@ -672,7 +877,7 @@ export class UserGroundHost {
672
877
  });
673
878
  await reconcileGround({
674
879
  identity, userRoot, database, cacheDir,
675
- onScan: this.options.onDetectScan,
880
+ onScan: (evidence) => this.reportDetectScan(evidence),
676
881
  });
677
882
  return localValue(identity, userRoot, database);
678
883
  },
@@ -693,7 +898,7 @@ export class UserGroundHost {
693
898
  },
694
899
  };
695
900
  }
696
- async uploadSnapshotContent(identity, resourceId, records) {
901
+ async uploadSnapshotContent(identity, resourceId, records, trace) {
697
902
  const cache = this.cacheDir(identity);
698
903
  const byHash = new Map();
699
904
  for (const record of records) {
@@ -714,6 +919,27 @@ export class UserGroundHost {
714
919
  bytes: manifest?.bytes ?? statSync(file).size,
715
920
  };
716
921
  });
922
+ const started = performance.now();
923
+ let uploadedChunks = 0;
924
+ let reusedChunks = 0;
925
+ let uploadedBytes = 0;
926
+ let inventoryRequests = 0;
927
+ const identifiers = {
928
+ ...(trace?.workspaceId ? { workspaceId: trace.workspaceId } : {}),
929
+ ...(trace?.entityId ? { entityId: trace.entityId } : {}),
930
+ ...(trace?.mutationId ? { mutationId: trace.mutationId } : {}),
931
+ };
932
+ this.stage({
933
+ primitive: "upload",
934
+ stage: "immutable-content",
935
+ status: "started",
936
+ ...identifiers,
937
+ measurements: {
938
+ journey: trace?.journey ?? "bootstrap",
939
+ artifacts: sources.length,
940
+ declaredBytes: sources.reduce((total, source) => total + source.bytes, 0),
941
+ },
942
+ });
717
943
  const small = sources.filter((source) => source.bytes <= CHUNK_BYTES);
718
944
  const large = sources.filter((source) => source.bytes > CHUNK_BYTES);
719
945
  const transfer = async (source) => {
@@ -735,6 +961,7 @@ export class UserGroundHost {
735
961
  sha256: chunk.sha256,
736
962
  bytes: chunk.bytes,
737
963
  }, bytes, ["private.entity-content.stored"]));
964
+ uploadedBytes += bytes.byteLength;
738
965
  };
739
966
  try {
740
967
  const receipt = await uploadArtifact(artifact, {
@@ -752,6 +979,7 @@ export class UserGroundHost {
752
979
  }, {
753
980
  sha256Hex,
754
981
  missingChunks: async (_candidate, manifest) => {
982
+ inventoryRequests += 1;
755
983
  const frame = await retryContent(() => this.wire.request({
756
984
  type: "private.entity-content.inventory",
757
985
  resource_id: resourceId,
@@ -794,6 +1022,7 @@ export class UserGroundHost {
794
1022
  wire_bytes: encoded.bytes.byteLength,
795
1023
  ...(encoded.encoding ? { content_encoding: encoded.encoding } : {}),
796
1024
  }, encoded.bytes, ["private.entity-content.stored-batch"]));
1025
+ uploadedBytes += encoded.bytes.byteLength;
797
1026
  },
798
1027
  putManifest: async (_candidate, manifest) => {
799
1028
  if (manifestPresent)
@@ -810,8 +1039,11 @@ export class UserGroundHost {
810
1039
  sha256: sha256Hex(bytes),
811
1040
  bytes: bytes.length,
812
1041
  }, bytes, ["private.entity-content.stored"]));
1042
+ uploadedBytes += bytes.byteLength;
813
1043
  },
814
1044
  });
1045
+ uploadedChunks += receipt.uploadedChunks;
1046
+ reusedChunks += receipt.reusedChunks;
815
1047
  atomicWrite(manifestCacheFile(cache, artifact.contentHash), stableJson(receipt.manifest));
816
1048
  }
817
1049
  finally {
@@ -819,17 +1051,63 @@ export class UserGroundHost {
819
1051
  await (await sourceFile.handle).close();
820
1052
  }
821
1053
  };
822
- await boundedForEach(small, SMALL_CONTENT_UPLOAD_CONCURRENCY, transfer);
823
- for (const source of large) {
824
- await transfer(source);
1054
+ try {
1055
+ await boundedForEach(small, SMALL_CONTENT_UPLOAD_CONCURRENCY, transfer);
1056
+ for (const source of large) {
1057
+ await transfer(source);
1058
+ }
1059
+ this.stage({
1060
+ primitive: "upload",
1061
+ stage: "immutable-content",
1062
+ status: "completed",
1063
+ ...identifiers,
1064
+ durationMs: performance.now() - started,
1065
+ measurements: {
1066
+ journey: trace?.journey ?? "bootstrap",
1067
+ artifacts: sources.length,
1068
+ declaredBytes: sources.reduce((total, source) => total + source.bytes, 0),
1069
+ uploadedBytes,
1070
+ uploadedChunks,
1071
+ reusedChunks,
1072
+ inventoryRequests,
1073
+ },
1074
+ });
1075
+ }
1076
+ catch (error) {
1077
+ this.stage({
1078
+ primitive: "upload",
1079
+ stage: "immutable-content",
1080
+ status: "failed",
1081
+ ...identifiers,
1082
+ durationMs: performance.now() - started,
1083
+ measurements: { journey: trace?.journey ?? "bootstrap" },
1084
+ });
1085
+ throw error;
825
1086
  }
826
1087
  }
827
- async downloadContent(resourceId, cacheDir, artifact, retryAttempts = 10) {
1088
+ async downloadContent(resourceId, cacheDir, artifact, retryAttempts = 10, trace) {
1089
+ const started = performance.now();
1090
+ let downloadedBytes = 0;
1091
+ let requests = 0;
1092
+ const identifiers = {
1093
+ ...(trace?.workspaceId ? { workspaceId: trace.workspaceId } : {}),
1094
+ entityId: trace?.entityId ?? artifact.entityId,
1095
+ ...(trace?.mutationId ? { mutationId: trace.mutationId } : {}),
1096
+ ...(trace?.globalSequence ? { globalSequence: trace.globalSequence } : {}),
1097
+ };
1098
+ this.stage({
1099
+ primitive: "download",
1100
+ stage: "immutable-content",
1101
+ status: "started",
1102
+ ...identifiers,
1103
+ measurements: { journey: trace?.journey ?? "bootstrap", kind: artifact.kind },
1104
+ });
828
1105
  const cache = { target: null };
829
1106
  const getChunk = async (manifest, index) => {
830
1107
  const chunk = manifest.chunks[index];
831
1108
  if (!chunk)
832
1109
  throw new Error(`content manifest has no chunk ${index}`);
1110
+ requests += 1;
833
1111
  const frame = await retryContent(() => this.wire.request({
834
1112
  type: "private.entity-content.get",
835
1113
  resource_id: resourceId,
@@ -839,12 +1117,15 @@ export class UserGroundHost {
839
1117
  part_index: index,
840
1118
  sha256: chunk.sha256,
841
1119
  }, ["private.entity-content.data"]), true, retryAttempts);
842
- return binaryFrameBytes(frame);
1120
+ const bytes = binaryFrameBytes(frame);
1121
+ downloadedBytes += bytes.byteLength;
1122
+ return bytes;
843
1123
  };
844
1124
  try {
845
1125
  const receipt = await downloadArtifact(artifact, {
846
1126
  sha256Hex,
847
1127
  getManifest: async () => {
1128
+ requests += 1;
848
1129
  const frame = await retryContent(() => this.wire.request({
849
1130
  type: "private.entity-content.get",
850
1131
  resource_id: resourceId,
@@ -852,7 +1133,9 @@ export class UserGroundHost {
852
1133
  kind: "manifest",
853
1134
  part_index: 0,
854
1135
  }, ["private.entity-content.data"]), false, retryAttempts);
855
- const candidate = JSON.parse(verifiedFrameBytes(frame).toString("utf8"));
1136
+ const manifestBytes = verifiedFrameBytes(frame);
1137
+ downloadedBytes += manifestBytes.byteLength;
1138
+ const candidate = JSON.parse(manifestBytes.toString("utf8"));
856
1139
  const checked = checkContentManifest(candidate, artifact.contentHash);
857
1140
  if (!checked.ok)
858
1141
  throw new Error(checked.error);
@@ -883,7 +1166,10 @@ export class UserGroundHost {
883
1166
  bytes: manifest.chunks[index]?.bytes,
884
1167
  })),
885
1168
  }, ["private.entity-content.data-batch"]), true, retryAttempts);
886
- return decodeContentWireBytes(binaryWireFrameBytes(frame), frame.content_encoding, Number(frame.bytes));
1169
+ requests += 1;
1170
+ const decoded = await decodeContentWireBytes(binaryWireFrameBytes(frame), frame.content_encoding, Number(frame.bytes));
1171
+ downloadedBytes += Number(frame.wire_bytes ?? frame.bytes);
1172
+ return decoded;
887
1173
  },
888
1174
  putChunk: async (_candidate, _manifest, index, bytes) => {
889
1175
  if (!cache.target)
@@ -902,8 +1188,35 @@ export class UserGroundHost {
902
1188
  return { local: sealed, bytes: sealed.bytes, contentHash: sealed.contentHash };
903
1189
  },
904
1190
  }, artifact.kind === "file" ? { concurrency: FILE_BATCH_DOWNLOAD_CONCURRENCY } : {});
1191
+ this.stage({
1192
+ primitive: "download",
1193
+ stage: "immutable-content",
1194
+ status: "completed",
1195
+ ...identifiers,
1196
+ durationMs: performance.now() - started,
1197
+ measurements: {
1198
+ journey: trace?.journey ?? "bootstrap",
1199
+ kind: artifact.kind,
1200
+ declaredBytes: receipt.manifest.bytes,
1201
+ downloadedBytes,
1202
+ downloadedChunks: receipt.downloadedChunks,
1203
+ reusedChunks: receipt.reusedChunks,
1204
+ requests,
1205
+ },
1206
+ });
905
1207
  return receipt.local;
906
1208
  }
1209
+ catch (error) {
1210
+ this.stage({
1211
+ primitive: "download",
1212
+ stage: "immutable-content",
1213
+ status: "failed",
1214
+ ...identifiers,
1215
+ durationMs: performance.now() - started,
1216
+ measurements: { journey: trace?.journey ?? "bootstrap", kind: artifact.kind, requests },
1217
+ });
1218
+ throw error;
1219
+ }
907
1220
  finally {
908
1221
  await cache.target?.close();
909
1222
  }
@@ -911,6 +1224,7 @@ export class UserGroundHost {
911
1224
  ensureWatchers(identity, authoritativeBaselineRootIds = []) {
912
1225
  if (this.closing)
913
1226
  return this.watchHost.health();
1227
+ const started = performance.now();
914
1228
  const root = this.userRoot(identity);
915
1229
  const wanted = new Map([[root, {
916
1230
  rootId: `user:${identity.userId}`,
@@ -935,6 +1249,18 @@ export class UserGroundHost {
935
1249
  if (health.state !== "healthy" || health.contentHandles < 1) {
936
1250
  throw new Error(`Watch coverage is degraded: ${health.reason ?? "no content coverage"}`);
937
1251
  }
1252
+ this.stage({
1253
+ primitive: "watch",
1254
+ stage: "topology-reconcile",
1255
+ status: "completed",
1256
+ durationMs: performance.now() - started,
1257
+ measurements: {
1258
+ logicalRoots: health.logicalRoots,
1259
+ contentHandles: health.contentHandles,
1260
+ addressHandles: health.addressHandles,
1261
+ authoritativeBaselines: authoritativeBaselineRootIds.length,
1262
+ },
1263
+ });
938
1264
  return health;
939
1265
  }
940
1266
  scheduleRescan(retryDelayMs) {
@@ -1030,7 +1356,15 @@ export class UserGroundHost {
1030
1356
  }
1031
1357
  }
1032
1358
  async syncLocalChanges(identity) {
1359
+ const waitStarted = performance.now();
1360
+ this.stage({ primitive: "detect", stage: "ground-wait", status: "started" });
1033
1361
  const release = await this.acquireGroundEffect();
1362
+ this.stage({
1363
+ primitive: "detect",
1364
+ stage: "ground-wait",
1365
+ status: "completed",
1366
+ durationMs: performance.now() - waitStarted,
1367
+ });
1034
1368
  try {
1035
1369
  await this.syncLocalChangesUnlocked(identity);
1036
1370
  }
@@ -1042,6 +1376,17 @@ export class UserGroundHost {
1042
1376
  if (!this.cloudState)
1043
1377
  throw new Error("cloud state is unavailable for user-ground Watch");
1044
1378
  const observations = this.watchHost.observations();
1379
+ const passStarted = performance.now();
1380
+ this.stage({
1381
+ primitive: "detect",
1382
+ stage: "pass",
1383
+ status: "started",
1384
+ measurements: {
1385
+ roots: observations.length,
1386
+ completeRoots: observations.filter(({ suspicion }) => suspicion.paths === null).length,
1387
+ namedPaths: observations.reduce((count, { suspicion }) => count + (suspicion.paths?.length ?? 0), 0),
1388
+ },
1389
+ });
1045
1390
  let widenedRoots = new Set();
1046
1391
  if (this.namedDetect) {
1047
1392
  // The named lane is an optimization over the same settled evidence.
@@ -1052,7 +1397,7 @@ export class UserGroundHost {
1052
1397
  if (named?.handled) {
1053
1398
  for (const evidence of named.evidence) {
1054
1399
  const observation = observations.find((candidate) => candidate.directory === evidence.directory);
1055
- this.options.onDetectScan?.({
1400
+ this.reportDetectScan({
1056
1401
  rootId: observation?.rootId ?? evidence.directory,
1057
1402
  directory: evidence.directory,
1058
1403
  scope: "paths",
@@ -1070,6 +1415,17 @@ export class UserGroundHost {
1070
1415
  this.watchHost.settle(observations);
1071
1416
  this.applyRail?.localStateChanged();
1072
1417
  this.recordRail?.localRecordsAvailable();
1418
+ this.stage({
1419
+ primitive: "detect",
1420
+ stage: "pass",
1421
+ status: "completed",
1422
+ durationMs: performance.now() - passStarted,
1423
+ measurements: {
1424
+ lane: "named",
1425
+ roots: named.evidence.length,
1426
+ records: named.evidence.reduce((count, evidence) => count + evidence.records, 0),
1427
+ },
1428
+ });
1073
1429
  return;
1074
1430
  }
1075
1431
  widenedRoots = new Set(named?.widenedRoots ?? []);
@@ -1085,7 +1441,7 @@ export class UserGroundHost {
1085
1441
  userRoot: this.userRoot(identity),
1086
1442
  database: this.databasePath(identity),
1087
1443
  cacheDir: this.cacheDir(identity),
1088
- onScan: this.options.onDetectScan,
1444
+ onScan: (evidence) => this.reportDetectScan(evidence),
1089
1445
  suspicions: new Map(observations.map((observation) => [observation.directory, widenedRoots.has(observation.directory)
1090
1446
  ? { ...observation.suspicion, paths: null }
1091
1447
  : observation.suspicion])),
@@ -1113,6 +1469,7 @@ export class UserGroundHost {
1113
1469
  proposal,
1114
1470
  }))
1115
1471
  : []);
1472
+ const recordCommitStarted = performance.now();
1116
1473
  commitDetectedState(this.databasePath(identity), {
1117
1474
  acceptedMaterializedRows,
1118
1475
  materializedRemovals,
@@ -1120,10 +1477,40 @@ export class UserGroundHost {
1120
1477
  notebookRemovals,
1121
1478
  proposals: detectedRecords,
1122
1479
  });
1480
+ const recordCommitDuration = performance.now() - recordCommitStarted;
1481
+ if (detectedRecords.length === 0) {
1482
+ this.stage({
1483
+ primitive: "record",
1484
+ stage: "sqlite-transaction",
1485
+ status: "completed",
1486
+ durationMs: recordCommitDuration,
1487
+ measurements: { records: 0 },
1488
+ });
1489
+ }
1490
+ else {
1491
+ for (const record of detectedRecords) {
1492
+ this.stage({
1493
+ primitive: "record",
1494
+ stage: "outbox-committed",
1495
+ status: "completed",
1496
+ entityId: record.proposal.entityId,
1497
+ mutationId: record.mutationId,
1498
+ durationMs: recordCommitDuration,
1499
+ measurements: { records: detectedRecords.length },
1500
+ });
1501
+ }
1502
+ }
1123
1503
  this.namedDetect?.refreshEnrollmentPolicy();
1124
1504
  this.watchHost.settle(observations);
1125
1505
  this.applyRail?.localStateChanged();
1126
1506
  this.recordRail?.localRecordsAvailable();
1507
+ this.stage({
1508
+ primitive: "detect",
1509
+ stage: "pass",
1510
+ status: "completed",
1511
+ durationMs: performance.now() - passStarted,
1512
+ measurements: { lane: "reconciliation", records: detectedRecords.length },
1513
+ });
1127
1514
  }
1128
1515
  async publishPendingSnapshotsBeforeLookup(identity, resourceId) {
1129
1516
  const pending = readSnapshotPublications(this.databasePath(identity));
@@ -1145,7 +1532,7 @@ export class UserGroundHost {
1145
1532
  /** Register is the deliberate cold graph-publication boundary. Ordinary
1146
1533
  * Watch detection stops at a durable exact record and never rebuilds this
1147
1534
  * resource snapshot. */
1148
- async publishMaterializedSnapshot(identity, publicationRootIds) {
1535
+ async publishMaterializedSnapshot(identity, publicationRootIds, trace) {
1149
1536
  const state = this.cloudState;
1150
1537
  if (!state)
1151
1538
  throw new Error("cloud state is unavailable for graph publication");
@@ -1169,7 +1556,7 @@ export class UserGroundHost {
1169
1556
  const identical = pending.find((entry) => entry.resourceId === state.resourceId
1170
1557
  && entry.snapshotChecksum === checksum);
1171
1558
  if (identical) {
1172
- await this.publishPendingSnapshots(identity);
1559
+ await this.publishPendingSnapshots(identity, trace);
1173
1560
  return;
1174
1561
  }
1175
1562
  if (pending.length > 0) {
@@ -1207,19 +1594,29 @@ export class UserGroundHost {
1207
1594
  replacementRootIds: replacement.rootIds,
1208
1595
  replacementRecords: replacement.records,
1209
1596
  });
1210
- await this.publishPendingSnapshots(identity);
1597
+ await this.publishPendingSnapshots(identity, trace);
1211
1598
  }
1212
- async publishPendingSnapshots(identity) {
1599
+ async publishPendingSnapshots(identity, trace) {
1213
1600
  for (const pending of readSnapshotPublications(this.databasePath(identity))) {
1214
1601
  const snapshot = snapshotFromRecords(JSON.parse(pending.snapshotJson).records || []);
1215
1602
  const replacement = rootReplacementFromRecords(pending.replacementRootIds, pending.replacementRecords);
1216
1603
  try {
1217
- await this.uploadSnapshotContent(identity, pending.resourceId, pending.uploadRecords);
1604
+ await this.uploadSnapshotContent(identity, pending.resourceId, pending.uploadRecords, trace);
1218
1605
  }
1219
1606
  catch (error) {
1220
1607
  throw pipelineStageError("cloud upload", error);
1221
1608
  }
1222
1609
  let frame;
1610
+ const graphCommitStarted = performance.now();
1611
+ if (trace?.journey === "register") {
1612
+ this.stage({
1613
+ primitive: "register",
1614
+ stage: "cloud-graph-commit",
1615
+ status: "started",
1616
+ ...(trace.workspaceId ? { workspaceId: trace.workspaceId } : {}),
1617
+ ...(trace.mutationId ? { mutationId: trace.mutationId } : {}),
1618
+ });
1619
+ }
1223
1620
  try {
1224
1621
  frame = await this.wire.request({
1225
1622
  type: "shared.mutation.submit",
@@ -1237,8 +1634,27 @@ export class UserGroundHost {
1237
1634
  }, ["shared.mutation.ack"]);
1238
1635
  }
1239
1636
  catch (error) {
1637
+ if (trace?.journey === "register") {
1638
+ this.stage({
1639
+ primitive: "register",
1640
+ stage: "cloud-graph-commit",
1641
+ status: "failed",
1642
+ ...(trace.workspaceId ? { workspaceId: trace.workspaceId } : {}),
1643
+ durationMs: performance.now() - graphCommitStarted,
1644
+ });
1645
+ }
1240
1646
  throw pipelineStageError("cloud graph commit", error);
1241
1647
  }
1648
+ if (trace?.journey === "register") {
1649
+ this.stage({
1650
+ primitive: "register",
1651
+ stage: "cloud-graph-commit",
1652
+ status: "completed",
1653
+ ...(trace.workspaceId ? { workspaceId: trace.workspaceId } : {}),
1654
+ durationMs: performance.now() - graphCommitStarted,
1655
+ measurements: { version: Number(frame.version) },
1656
+ });
1657
+ }
1242
1658
  this.cloudState = {
1243
1659
  resourceId: pending.resourceId,
1244
1660
  authorityEpoch: Number(frame.authority_epoch),