@amalgm/shell 0.1.110 → 0.1.112

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.
@@ -55,6 +55,15 @@ const WALK_CAPTURE_CONCURRENCY = 8;
55
55
  const REGISTER_STALE_CAPTURE_ATTEMPTS = 3;
56
56
  const DOWNLOAD_CONTENT_RETRY_ATTEMPTS = 3;
57
57
  const APPLY_GROUND_WAIT_TIMEOUT_MS = 15_000;
58
+ function loadSnapshotPublication(entry) {
59
+ const snapshotRecords = snapshotFromRecords((JSON.parse(entry.snapshotBlob.toString("utf8")).records ?? [])).records;
60
+ const uploadIds = new Set(entry.uploadUuids);
61
+ return {
62
+ ...entry,
63
+ snapshotRecords,
64
+ uploadRecords: snapshotRecords.filter((record) => uploadIds.has(record.uuid)),
65
+ };
66
+ }
58
67
  const GROUND_ROW_COLUMNS = [
59
68
  "uuid", "resource_id", "root_uuid", "type", "parent_uuid", "name", "status", "version",
60
69
  "payload_version", "transport_version", "relative_path", "absolute_path", "device_number",
@@ -196,6 +205,11 @@ export class UserGroundHost {
196
205
  stage(input) {
197
206
  emitFilesStage(this.options.onFilesStage, input);
198
207
  }
208
+ /** Whether a counter worth real time on the critical path may be computed. */
209
+ measuring() {
210
+ return this.options.filesTraceActive?.()
211
+ ?? (this.options.onFilesStage !== undefined || this.options.onDetectScan !== undefined);
212
+ }
199
213
  reportDetectScan(evidence) {
200
214
  this.options.onDetectScan?.(evidence);
201
215
  this.stage({
@@ -213,7 +227,19 @@ export class UserGroundHost {
213
227
  gitInspections: evidence.gitInspections,
214
228
  repositoryCaptures: evidence.repositoryCaptures,
215
229
  records: evidence.records ?? 0,
216
- recordBytes: evidence.recordBytes ?? 0,
230
+ ...(evidence.recordBytes === undefined ? {} : { recordBytes: evidence.recordBytes }),
231
+ // Where the scan actually went. `captureBusyMs / captureMs` is the
232
+ // capture pool's occupancy; at ~1 the pool ran one file at a time.
233
+ walkMs: Math.round(evidence.walkMs ?? 0),
234
+ gitMs: Math.round(evidence.gitMs ?? 0),
235
+ captureMs: Math.round(evidence.captureMs ?? 0),
236
+ captureBusyMs: Math.round(evidence.captureBusyMs ?? 0),
237
+ captureBytes: evidence.captureBytes ?? 0,
238
+ repoIdentityMs: Math.round(evidence.repositoryIdentityMs ?? 0),
239
+ repoCaptureMs: Math.round(evidence.repositoryCaptureMs ?? 0),
240
+ repoSealMs: Math.round(evidence.repositorySealMs ?? 0),
241
+ reconcileMs: Math.round(evidence.reconcileMs ?? 0),
242
+ persistMs: Math.round(evidence.persistMs ?? 0),
217
243
  },
218
244
  });
219
245
  }
@@ -611,6 +637,7 @@ export class UserGroundHost {
611
637
  database: this.databasePath(identity),
612
638
  cacheDir: this.cacheDir(identity),
613
639
  onScan: (evidence) => this.reportDetectScan(evidence),
640
+ measuring: this.measuring(),
614
641
  registrationWorkspaceId: workspaceId,
615
642
  suspicions: new Map(observations.map((observation) => [observation.directory, observation.suspicion])),
616
643
  });
@@ -1128,6 +1155,7 @@ export class UserGroundHost {
1128
1155
  await reconcileGround({
1129
1156
  identity, userRoot, database, cacheDir,
1130
1157
  onScan: (evidence) => this.reportDetectScan(evidence),
1158
+ measuring: this.measuring(),
1131
1159
  });
1132
1160
  return localValue(identity, userRoot, database);
1133
1161
  },
@@ -1842,6 +1870,7 @@ export class UserGroundHost {
1842
1870
  database: this.databasePath(identity),
1843
1871
  cacheDir: this.cacheDir(identity),
1844
1872
  onScan: (evidence) => this.reportDetectScan(evidence),
1873
+ measuring: this.measuring(),
1845
1874
  suspicions: reconcileSuspicions,
1846
1875
  coreScan,
1847
1876
  });
@@ -1939,7 +1968,7 @@ export class UserGroundHost {
1939
1968
  headVersion: first.baseVersion,
1940
1969
  checksum: "",
1941
1970
  deliveryWatermark: first.deliveryWatermark,
1942
- records: snapshotFromRecords(JSON.parse(first.snapshotJson).records || []).records,
1971
+ records: loadSnapshotPublication(first).snapshotRecords,
1943
1972
  };
1944
1973
  try {
1945
1974
  await this.publishPendingSnapshots(identity);
@@ -1990,45 +2019,65 @@ export class UserGroundHost {
1990
2019
  const state = this.cloudState;
1991
2020
  if (!state)
1992
2021
  throw new Error("cloud state is unavailable for graph publication");
1993
- const officialRecords = recordsAtDeliveryPrefix(state.records, readAcceptedResults(this.databasePath(identity), state.resourceId, state.deliveryWatermark, deliveryWatermark));
1994
- const localRecords = recordsRetainingMissingEvidence(readGroundRowsForRoots(this.databasePath(identity), "entities", publicationRootIds)
2022
+ // Graph preparation is the leg between a committed local ground and the
2023
+ // first wire frame. It is pure CPU and SQLite, so it is measured by its
2024
+ // own parts: whoever reads this ledger must see which part is slow.
2025
+ const preparationStarted = performance.now();
2026
+ const leg = {};
2027
+ const timed = (name, work) => {
2028
+ const started = performance.now();
2029
+ const value = work();
2030
+ leg[`${name}Ms`] = performance.now() - started;
2031
+ return value;
2032
+ };
2033
+ if (trace?.journey === "register") {
2034
+ this.stage({
2035
+ primitive: "register",
2036
+ stage: "graph-preparation",
2037
+ status: "started",
2038
+ ...(trace.workspaceId ? { workspaceId: trace.workspaceId } : {}),
2039
+ });
2040
+ }
2041
+ const officialRecords = timed("officialRows", () => recordsAtDeliveryPrefix(state.records, readAcceptedResults(this.databasePath(identity), state.resourceId, state.deliveryWatermark, deliveryWatermark)));
2042
+ const localRecords = timed("groundRows", () => recordsRetainingMissingEvidence(readGroundRowsForRoots(this.databasePath(identity), "entities", publicationRootIds)
1995
2043
  .map(portableRecord), readGroundRowsForRoots(this.databasePath(identity), "detection_notebook", publicationRootIds)
1996
- .map(portableRecord));
1997
- const merged = mergeMaterializedRoots(officialRecords, localRecords);
1998
- const traveling = travelingRecords(merged, sha256Hex);
2044
+ .map(portableRecord)));
2045
+ const merged = timed("merge", () => mergeMaterializedRoots(officialRecords, localRecords));
2046
+ const traveling = timed("project", () => travelingRecords(merged, sha256Hex));
1999
2047
  const impossibleGitLeaf = traveling.find((record) => (record.type === "file.text" || record.type === "file.binary" || record.type === "link")
2000
2048
  && record.payloadVersion?.startsWith("git:"));
2001
2049
  if (impossibleGitLeaf) {
2002
2050
  const parent = merged.find((record) => record.uuid === impossibleGitLeaf.parentUUID);
2003
2051
  throw new Error(`repository-owned leaf escaped its rail: ${impossibleGitLeaf.name} parent=${parent?.type ?? "missing"}`);
2004
2052
  }
2005
- const snapshot = snapshotFromRecords(traveling);
2006
- const snapshotJson = stableJson(snapshot);
2007
- const checksum = sha256Hex(snapshotJson);
2053
+ const snapshot = timed("normalize", () => snapshotFromRecords(traveling));
2054
+ // ONE canonical serialization: these exact bytes are the journal blob
2055
+ // and the checksum's input; they are never re-derived or re-parsed on
2056
+ // the in-process path.
2057
+ const snapshotBlob = timed("serialize", () => Buffer.from(stableJson(snapshot), "utf8"));
2058
+ const checksum = timed("checksum", () => sha256Hex(snapshotBlob));
2008
2059
  const localIds = new Set(localRecords.map((record) => record.uuid));
2009
2060
  // Graph membership is never proof of sealed cargo: a record in the cloud
2010
2061
  // graph may name content sealed only under a prior content contract,
2011
2062
  // whose namespace the current readers never touch. Only the authority's
2012
- // (resource, hash, contract) seal evidence may skip an upload — and the
2013
- // question is asked even when the graph itself is unchanged, so Register
2014
- // always re-establishes cargo closure before returning.
2015
- const cargo = snapshot.records.flatMap((record) => {
2016
- if (!localIds.has(record.uuid))
2017
- return [];
2018
- const artifact = artifactForRecord(record);
2019
- return artifact ? [{ record, contentHash: artifact.contentHash }] : [];
2020
- });
2021
- const sealedArtifacts = await this.sealedContentHashes(state.resourceId, [...new Set(cargo.map((entry) => entry.contentHash))]);
2022
- const uploadRecords = cargo
2023
- .filter((entry) => !sealedArtifacts.has(entry.contentHash))
2024
- .map((entry) => entry.record);
2063
+ // (resource, hash, contract) seal evidence may skip an upload — the
2064
+ // sealed-first upload asks that exact question itself and moves only
2065
+ // what is unsealed, so Register always re-establishes cargo closure
2066
+ // before returning, even when the graph itself is unchanged.
2067
+ const cargoRecords = snapshot.records.filter((record) => localIds.has(record.uuid) && artifactForRecord(record) !== null);
2025
2068
  if (checksum === state.checksum && deliveryWatermark === state.deliveryWatermark) {
2069
+ this.finishPreparation(trace, preparationStarted, leg, {
2070
+ records: snapshot.records.length,
2071
+ snapshotBytes: snapshotBlob.byteLength,
2072
+ cargoRecords: cargoRecords.length,
2073
+ graphUnchanged: true,
2074
+ });
2026
2075
  // The graph is already committed; only cargo can be missing. Repair
2027
2076
  // uploads need no journal row — a crash simply re-runs the same
2028
2077
  // repair on the next Register.
2029
- if (uploadRecords.length > 0) {
2078
+ if (cargoRecords.length > 0) {
2030
2079
  try {
2031
- await this.uploadSnapshotContent(identity, state.resourceId, uploadRecords, trace, manifestHints);
2080
+ await this.uploadSnapshotContent(identity, state.resourceId, cargoRecords, trace, manifestHints);
2032
2081
  }
2033
2082
  catch (error) {
2034
2083
  throw pipelineStageError("cloud upload", error);
@@ -2036,30 +2085,68 @@ export class UserGroundHost {
2036
2085
  }
2037
2086
  return;
2038
2087
  }
2039
- const replacementRootIds = localRecords
2040
- .filter((record) => record.parentUUID === null)
2041
- .map((record) => record.uuid);
2042
- const replacementRoots = new Set(replacementRootIds);
2043
- const rootOf = recordRootResolver(snapshot.records);
2044
- const replacementRecords = snapshot.records.filter((record) => {
2045
- const root = rootOf(record);
2046
- return root !== null && replacementRoots.has(root);
2088
+ const replacement = timed("replacement", () => {
2089
+ const rootIds = localRecords
2090
+ .filter((record) => record.parentUUID === null)
2091
+ .map((record) => record.uuid);
2092
+ const roots = new Set(rootIds);
2093
+ const rootOf = recordRootResolver(snapshot.records);
2094
+ return rootReplacementFromRecords(rootIds, snapshot.records.filter((record) => {
2095
+ const root = rootOf(record);
2096
+ return root !== null && roots.has(root);
2097
+ }));
2047
2098
  });
2048
- const replacement = rootReplacementFromRecords(replacementRootIds, replacementRecords);
2049
- supersedeSnapshotJournal(this.databasePath(identity), {
2099
+ // The operation's canonical bytes: staged, hashed, and submitted as-is.
2100
+ const operationBlob = timed("operation", () => Buffer.from(stableJson(replacement), "utf8"));
2101
+ const entry = {
2050
2102
  mutationId: randomUUID(),
2051
2103
  resourceId: state.resourceId,
2052
2104
  authorityEpoch: state.authorityEpoch,
2053
2105
  baseVersion: state.headVersion,
2054
2106
  snapshotChecksum: checksum,
2055
- snapshotJson,
2056
2107
  deliveryWatermark,
2057
- uploadRecords,
2108
+ operationKind: replacement.kind,
2058
2109
  replacementRootIds: replacement.rootIds,
2059
- replacementRecords: replacement.records,
2110
+ uploadUuids: cargoRecords.map((record) => record.uuid),
2111
+ snapshotBlob,
2112
+ operationBlob,
2060
2113
  status: "pending",
2114
+ submitted: false,
2115
+ };
2116
+ timed("journal", () => supersedeSnapshotJournal(this.databasePath(identity), entry));
2117
+ this.finishPreparation(trace, preparationStarted, leg, {
2118
+ records: snapshot.records.length,
2119
+ snapshotBytes: snapshotBlob.byteLength,
2120
+ operationBytes: operationBlob.byteLength,
2121
+ operationRoots: replacement.rootIds.length,
2122
+ cargoRecords: cargoRecords.length,
2123
+ graphUnchanged: false,
2124
+ });
2125
+ // Publish the freshly journaled intent directly: the records are already
2126
+ // in memory and the operation bytes already exist — nothing is re-read,
2127
+ // re-parsed, or re-serialized between prepare and the wire.
2128
+ await this.publishLoadedSnapshot(identity, {
2129
+ ...entry,
2130
+ snapshotRecords: snapshot.records,
2131
+ uploadRecords: cargoRecords,
2132
+ }, trace, manifestHints);
2133
+ }
2134
+ /** Close the graph-preparation stage with every leg it spent, so a slow
2135
+ * preparation names the slow part instead of one opaque number. */
2136
+ finishPreparation(trace, startedAt, legs, counters) {
2137
+ if (trace?.journey !== "register")
2138
+ return;
2139
+ this.stage({
2140
+ primitive: "register",
2141
+ stage: "graph-preparation",
2142
+ status: "completed",
2143
+ durationMs: performance.now() - startedAt,
2144
+ ...(trace.workspaceId ? { workspaceId: trace.workspaceId } : {}),
2145
+ measurements: {
2146
+ ...counters,
2147
+ ...Object.fromEntries(Object.entries(legs).map(([name, value]) => [name, Math.round(value)])),
2148
+ },
2061
2149
  });
2062
- await this.publishPendingSnapshots(identity, trace, manifestHints);
2063
2150
  }
2064
2151
  /** Which of these heads does the authority hold sealed under the current
2065
2152
  * content contract? Batched so a whole workspace costs a handful of
@@ -2091,87 +2178,111 @@ export class UserGroundHost {
2091
2178
  // snapshot superseding it can publish this resource again.
2092
2179
  if (pending.status === "rejected")
2093
2180
  continue;
2094
- const snapshot = snapshotFromRecords(JSON.parse(pending.snapshotJson).records || []);
2095
- const replacement = rootReplacementFromRecords(pending.replacementRootIds, pending.replacementRecords);
2096
- try {
2097
- await this.uploadSnapshotContent(identity, pending.resourceId, pending.uploadRecords, trace, manifestHints);
2098
- }
2099
- catch (error) {
2100
- // An unsubmitted pending row is a local intent the authority has
2101
- // never seen. When its captured ground moved before upload, the
2102
- // intent is simply stale: delete it and let the caller re-derive
2103
- // one from current ground. A submitted row never lands here — its
2104
- // upload sealed completely before the first submit frame left.
2105
- if (staleCapture(error) && !pending.submitted) {
2106
- deleteJournalEntry(this.databasePath(identity), pending.mutationId);
2107
- }
2108
- throw pipelineStageError("cloud upload", error);
2181
+ await this.publishLoadedSnapshot(identity, loadSnapshotPublication(pending), trace, manifestHints);
2182
+ }
2183
+ }
2184
+ async publishLoadedSnapshot(identity, publication, trace, manifestHints) {
2185
+ try {
2186
+ await this.uploadSnapshotContent(identity, publication.resourceId, publication.uploadRecords, trace, manifestHints);
2187
+ }
2188
+ catch (error) {
2189
+ // An unsubmitted pending row is a local intent the authority has
2190
+ // never seen. When its captured ground moved before upload, the
2191
+ // intent is simply stale: delete it and let the caller re-derive
2192
+ // one from current ground. A submitted row never lands here — its
2193
+ // upload sealed completely before the first submit frame left.
2194
+ if (staleCapture(error) && !publication.submitted) {
2195
+ deleteJournalEntry(this.databasePath(identity), publication.mutationId);
2109
2196
  }
2110
- let frame;
2111
- const graphCommitStarted = performance.now();
2197
+ throw pipelineStageError("cloud upload", error);
2198
+ }
2199
+ let frame;
2200
+ const graphCommitStarted = performance.now();
2201
+ if (trace?.journey === "register") {
2202
+ this.stage({
2203
+ primitive: "register",
2204
+ stage: "cloud-graph-commit",
2205
+ status: "started",
2206
+ ...(trace.workspaceId ? { workspaceId: trace.workspaceId } : {}),
2207
+ ...(trace.mutationId ? { mutationId: trace.mutationId } : {}),
2208
+ });
2209
+ }
2210
+ const legMs = {};
2211
+ const local = (name, work) => {
2212
+ const at = performance.now();
2213
+ const value = work();
2214
+ legMs[`${name}Ms`] = performance.now() - at;
2215
+ return value;
2216
+ };
2217
+ if (!publication.submitted) {
2218
+ local("markSubmitted", () => markSnapshotJournalSubmitted(this.databasePath(identity), publication.mutationId));
2219
+ }
2220
+ try {
2221
+ frame = await submitMutationOperation(this.wire, {
2222
+ resourceId: publication.resourceId,
2223
+ authorityEpoch: publication.authorityEpoch,
2224
+ mutationId: publication.mutationId,
2225
+ deviceId: identity.deviceId,
2226
+ baseVersion: publication.baseVersion,
2227
+ contract: ENTITY_CLOUD_CONTRACT,
2228
+ schemaVersion: ENTITY_CLOUD_SCHEMA_VERSION,
2229
+ operationKind: publication.operationKind,
2230
+ deliveryWatermark: publication.deliveryWatermark,
2231
+ }, publication.operationBlob, (leg, durationMs) => { legMs[`${leg}Ms`] = durationMs; });
2232
+ }
2233
+ catch (error) {
2112
2234
  if (trace?.journey === "register") {
2113
2235
  this.stage({
2114
2236
  primitive: "register",
2115
2237
  stage: "cloud-graph-commit",
2116
- status: "started",
2238
+ status: "failed",
2117
2239
  ...(trace.workspaceId ? { workspaceId: trace.workspaceId } : {}),
2118
- ...(trace.mutationId ? { mutationId: trace.mutationId } : {}),
2240
+ durationMs: performance.now() - graphCommitStarted,
2119
2241
  });
2120
2242
  }
2121
- if (!pending.submitted) {
2122
- markSnapshotJournalSubmitted(this.databasePath(identity), pending.mutationId);
2123
- }
2124
- try {
2125
- frame = await submitMutationOperation(this.wire, {
2126
- resourceId: pending.resourceId,
2127
- authorityEpoch: pending.authorityEpoch,
2128
- mutationId: pending.mutationId,
2129
- deviceId: identity.deviceId,
2130
- baseVersion: pending.baseVersion,
2131
- contract: ENTITY_CLOUD_CONTRACT,
2132
- schemaVersion: ENTITY_CLOUD_SCHEMA_VERSION,
2133
- operationKind: replacement.kind,
2134
- deliveryWatermark: pending.deliveryWatermark,
2135
- }, replacement);
2136
- }
2137
- catch (error) {
2138
- if (trace?.journey === "register") {
2139
- this.stage({
2140
- primitive: "register",
2141
- stage: "cloud-graph-commit",
2142
- status: "failed",
2143
- ...(trace.workspaceId ? { workspaceId: trace.workspaceId } : {}),
2144
- durationMs: performance.now() - graphCommitStarted,
2145
- });
2146
- }
2147
- // A non-retryable error frame is the authority's answer: this exact
2148
- // mutation is refused forever. Settle the row so it is never
2149
- // replayed and a newer snapshot may supersede it. Every other
2150
- // failure leaves the outcome unknown and the row pending.
2151
- if (error instanceof WireRequestError && error.retryable === false) {
2152
- markSnapshotJournalRejected(this.databasePath(identity), pending.mutationId);
2153
- }
2154
- throw pipelineStageError("cloud graph commit", error);
2243
+ // A non-retryable error frame is the authority's answer: this exact
2244
+ // mutation is refused forever. Settle the row so it is never
2245
+ // replayed and a newer snapshot may supersede it. Every other
2246
+ // failure leaves the outcome unknown and the row pending.
2247
+ if (error instanceof WireRequestError && error.retryable === false) {
2248
+ markSnapshotJournalRejected(this.databasePath(identity), publication.mutationId);
2155
2249
  }
2156
- if (trace?.journey === "register") {
2157
- this.stage({
2158
- primitive: "register",
2159
- stage: "cloud-graph-commit",
2160
- status: "completed",
2161
- ...(trace.workspaceId ? { workspaceId: trace.workspaceId } : {}),
2162
- durationMs: performance.now() - graphCommitStarted,
2163
- measurements: { version: Number(frame.version) },
2164
- });
2250
+ throw pipelineStageError("cloud graph commit", error);
2251
+ }
2252
+ this.cloudState = {
2253
+ resourceId: publication.resourceId,
2254
+ authorityEpoch: Number(frame.authority_epoch),
2255
+ headVersion: Number(frame.version),
2256
+ checksum: publication.snapshotChecksum,
2257
+ deliveryWatermark: Number(frame.delivery_watermark),
2258
+ records: publication.snapshotRecords,
2259
+ };
2260
+ local("clearJournal", () => deleteJournalEntry(this.databasePath(identity), publication.mutationId));
2261
+ if (trace?.journey === "register") {
2262
+ // The authority reports where its side of the commit went; carrying
2263
+ // its legs here gives one ledger line the whole story: local journal
2264
+ // marks, wire time, gateway assemble/validate, and the database RPC.
2265
+ for (const [wire, leg] of [
2266
+ ["assemble_ms", "serverAssembleMs"],
2267
+ ["validate_ms", "serverValidateMs"],
2268
+ ["database_ms", "serverDatabaseMs"],
2269
+ ]) {
2270
+ if (typeof frame[wire] === "number")
2271
+ legMs[leg] = frame[wire];
2165
2272
  }
2166
- this.cloudState = {
2167
- resourceId: pending.resourceId,
2168
- authorityEpoch: Number(frame.authority_epoch),
2169
- headVersion: Number(frame.version),
2170
- checksum: pending.snapshotChecksum,
2171
- deliveryWatermark: Number(frame.delivery_watermark),
2172
- records: snapshot.records,
2173
- };
2174
- deleteJournalEntry(this.databasePath(identity), pending.mutationId);
2273
+ this.stage({
2274
+ primitive: "register",
2275
+ stage: "cloud-graph-commit",
2276
+ status: "completed",
2277
+ ...(trace.workspaceId ? { workspaceId: trace.workspaceId } : {}),
2278
+ durationMs: performance.now() - graphCommitStarted,
2279
+ measurements: {
2280
+ version: Number(frame.version),
2281
+ operationBytes: publication.operationBlob.byteLength,
2282
+ ...Object.fromEntries(Object.entries(legMs)
2283
+ .map(([name, value]) => [name, Math.round(value)])),
2284
+ },
2285
+ });
2175
2286
  }
2176
2287
  }
2177
2288
  }
@@ -2264,12 +2375,142 @@ function assertHealthyWatch(evidence, rootId) {
2264
2375
  * file over a dozen times; the file itself is never deleted by the host, so
2265
2376
  * every later open needs only the connection and its pragmas. */
2266
2377
  const migratedDatabases = new Set();
2267
- function initializeDatabase(file) {
2378
+ /** The journal's mutable facts and its immutable payload live in separate
2379
+ * tables on purpose: SQLite rewrites a row's entire record — overflow pages
2380
+ * included — on any column update, so flipping submitted_at on a row that
2381
+ * carried a register-scale snapshot cost a full multi-megabyte rewrite
2382
+ * through the WAL at synchronous=FULL (measured ~7.5s of the production
2383
+ * cloud-commit stage on the 1-GB machine). The blob row is written exactly
2384
+ * once and read only on restart replay; every status transition touches only
2385
+ * the small row. */
2386
+ const SNAPSHOT_PUBLICATIONS_SCHEMA = `
2387
+ CREATE TABLE IF NOT EXISTS snapshot_publications (
2388
+ sequence INTEGER PRIMARY KEY AUTOINCREMENT,
2389
+ mutation_id TEXT NOT NULL UNIQUE,
2390
+ resource_id TEXT NOT NULL,
2391
+ meta_json TEXT NOT NULL,
2392
+ created_at INTEGER NOT NULL,
2393
+ status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending', 'rejected')),
2394
+ submitted_at INTEGER
2395
+ );
2396
+ CREATE TABLE IF NOT EXISTS snapshot_publication_blobs (
2397
+ mutation_id TEXT PRIMARY KEY,
2398
+ snapshot_blob BLOB NOT NULL,
2399
+ operation_blob BLOB NOT NULL
2400
+ );`;
2401
+ /** Converts an old-shape publication journal (one nested publication_json
2402
+ * text per row) into the blob journal, atomically, preserving every row's
2403
+ * meaning. A pending row may be a submitted mutation whose outcome exists
2404
+ * only at the authority — dropping it would violate the retry law that
2405
+ * recovers a lost acknowledgement — so conversion is structural: the nested
2406
+ * snapshot text becomes the snapshot blob verbatim, the stored replacement
2407
+ * becomes the operation blob, and semantic judgment stays where it always
2408
+ * was, with the reader (an unlawful row still blocks registration rather
2409
+ * than being silently replaced).
2410
+ *
2411
+ * The transform runs entirely inside SQLite. A production journal row is a
2412
+ * register-scale graph serialized three times over — the exact text whose
2413
+ * repeated JavaScript materialization blew the 512 MB heap — so no part of
2414
+ * it may ever become a JavaScript value here: json_extract lifts the nested
2415
+ * snapshot text straight into its blob, and the meta and operation rows are
2416
+ * json_object over subtrees, all in engine memory outside the V8 heap.
2417
+ *
2418
+ * Tables older than the three-state journal carry no status or submitted_at;
2419
+ * their rows were written before the pre-submit durability mark existed, so
2420
+ * the only safe reading is "possibly submitted": keep them pending and
2421
+ * marked submitted, which replays the same mutation id — always idempotent —
2422
+ * and never supersedes. A row whose publication_json does not parse fails
2423
+ * the open loudly with the journal intact; nothing is ever deleted on a
2424
+ * conversion failure. */
2425
+ function convertLegacySnapshotJournal(database, columns) {
2426
+ database.transaction(() => {
2427
+ database.exec("ALTER TABLE snapshot_publications RENAME TO snapshot_publications_legacy");
2428
+ database.exec(SNAPSHOT_PUBLICATIONS_SCHEMA);
2429
+ database.exec(`
2430
+ INSERT INTO snapshot_publications(
2431
+ mutation_id, resource_id, meta_json, created_at, status, submitted_at
2432
+ )
2433
+ SELECT
2434
+ mutation_id,
2435
+ resource_id,
2436
+ json_object(
2437
+ 'authorityEpoch', json_extract(publication_json, '$.authorityEpoch'),
2438
+ 'baseVersion', json_extract(publication_json, '$.baseVersion'),
2439
+ 'deliveryWatermark', json_extract(publication_json, '$.deliveryWatermark'),
2440
+ 'snapshotChecksum', json_extract(publication_json, '$.snapshotChecksum'),
2441
+ 'operationKind', 'entity.roots.replace',
2442
+ 'replacementRootIds', json(json_extract(publication_json, '$.replacementRootIds')),
2443
+ 'uploadUuids', coalesce((
2444
+ SELECT json_group_array(json_extract(uploaded.value, '$.uuid'))
2445
+ FROM json_each(publication_json, '$.uploadRecords') AS uploaded
2446
+ ), json_array())
2447
+ ),
2448
+ created_at,
2449
+ ${columns.has("status") ? "CASE status WHEN 'rejected' THEN 'rejected' ELSE 'pending' END" : "'pending'"},
2450
+ ${columns.has("submitted_at") ? "submitted_at" : "created_at"}
2451
+ FROM snapshot_publications_legacy ORDER BY sequence
2452
+ `);
2453
+ database.exec(`
2454
+ INSERT INTO snapshot_publication_blobs(mutation_id, snapshot_blob, operation_blob)
2455
+ SELECT
2456
+ mutation_id,
2457
+ CAST(json_extract(publication_json, '$.snapshotJson') AS BLOB),
2458
+ CAST(json_object(
2459
+ 'kind', 'entity.roots.replace',
2460
+ 'rootIds', json(json_extract(publication_json, '$.replacementRootIds')),
2461
+ 'records', json(json_extract(publication_json, '$.replacementRecords'))
2462
+ ) AS BLOB)
2463
+ FROM snapshot_publications_legacy ORDER BY sequence
2464
+ `);
2465
+ database.exec("DROP TABLE snapshot_publications_legacy");
2466
+ })();
2467
+ }
2468
+ /** Converts the single-table blob journal (blobs stored on the publication
2469
+ * row itself) into the split journal, atomically and entirely inside SQLite —
2470
+ * the same engine-memory law as the nested-text conversion above. Every
2471
+ * field already has its final meaning; the blobs simply move to their
2472
+ * write-once table so status transitions stop rewriting them. */
2473
+ function splitSnapshotJournalBlobs(database) {
2474
+ database.transaction(() => {
2475
+ database.exec("ALTER TABLE snapshot_publications RENAME TO snapshot_publications_legacy");
2476
+ database.exec(SNAPSHOT_PUBLICATIONS_SCHEMA);
2477
+ database.exec(`
2478
+ INSERT INTO snapshot_publications(
2479
+ mutation_id, resource_id, meta_json, created_at, status, submitted_at
2480
+ )
2481
+ SELECT mutation_id, resource_id, meta_json, created_at, status, submitted_at
2482
+ FROM snapshot_publications_legacy ORDER BY sequence
2483
+ `);
2484
+ database.exec(`
2485
+ INSERT INTO snapshot_publication_blobs(mutation_id, snapshot_blob, operation_blob)
2486
+ SELECT mutation_id, snapshot_blob, operation_blob
2487
+ FROM snapshot_publications_legacy ORDER BY sequence
2488
+ `);
2489
+ database.exec("DROP TABLE snapshot_publications_legacy");
2490
+ })();
2491
+ }
2492
+ /** The single opener and schema-migration entry for a user ground database.
2493
+ * Exported so the upgrade path is provable in isolation — the journal
2494
+ * conversion law test opens a legacy file through this exact entry inside a
2495
+ * heap-capped child process. */
2496
+ export function initializeDatabase(file) {
2268
2497
  ensurePrivateDir(dirname(file));
2269
2498
  const database = new Database(file);
2270
2499
  database.pragma("busy_timeout = 5000");
2271
2500
  database.pragma("journal_mode = WAL");
2272
2501
  database.pragma("synchronous = FULL");
2502
+ // The legacy probe runs on every open, before the migration memo: a journal
2503
+ // row may be a submitted publication whose outcome exists only at the
2504
+ // authority, so an old-shape table must convert — never drop — no matter
2505
+ // how this file arrived at this process. One PRAGMA against the in-memory
2506
+ // catalog costs nothing next to the connection open itself.
2507
+ const publicationColumns = new Set(database.prepare("PRAGMA table_info(snapshot_publications)").all().map(({ name }) => name));
2508
+ if (publicationColumns.has("publication_json")) {
2509
+ convertLegacySnapshotJournal(database, publicationColumns);
2510
+ }
2511
+ else if (publicationColumns.has("snapshot_blob")) {
2512
+ splitSnapshotJournalBlobs(database);
2513
+ }
2273
2514
  if (migratedDatabases.has(file))
2274
2515
  return database;
2275
2516
  database.exec(`
@@ -2361,15 +2602,7 @@ function initializeDatabase(file) {
2361
2602
  authority_id TEXT PRIMARY KEY,
2362
2603
  delivery_sequence INTEGER NOT NULL CHECK(delivery_sequence >= 0)
2363
2604
  );
2364
- CREATE TABLE IF NOT EXISTS snapshot_publications (
2365
- sequence INTEGER PRIMARY KEY AUTOINCREMENT,
2366
- mutation_id TEXT NOT NULL UNIQUE,
2367
- resource_id TEXT NOT NULL,
2368
- publication_json TEXT NOT NULL,
2369
- created_at INTEGER NOT NULL,
2370
- status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending', 'rejected')),
2371
- submitted_at INTEGER
2372
- );
2605
+ ${SNAPSHOT_PUBLICATIONS_SCHEMA}
2373
2606
  CREATE TABLE IF NOT EXISTS workspace_add_intents (
2374
2607
  workspace_uuid TEXT PRIMARY KEY,
2375
2608
  destination_path TEXT NOT NULL UNIQUE,
@@ -2405,13 +2638,6 @@ function initializeDatabase(file) {
2405
2638
  if (!inboxColumns.has("resolved_at")) {
2406
2639
  database.exec("ALTER TABLE record_inbox ADD COLUMN resolved_at INTEGER");
2407
2640
  }
2408
- const publicationColumns = new Set(database.prepare("PRAGMA table_info(snapshot_publications)").all().map(({ name }) => name));
2409
- if (!publicationColumns.has("status")) {
2410
- database.exec("ALTER TABLE snapshot_publications ADD COLUMN status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending', 'rejected'))");
2411
- }
2412
- if (!publicationColumns.has("submitted_at")) {
2413
- database.exec("ALTER TABLE snapshot_publications ADD COLUMN submitted_at INTEGER");
2414
- }
2415
2641
  database.exec("DROP INDEX IF EXISTS record_inbox_delivery");
2416
2642
  database.exec("DROP TABLE IF EXISTS mutation_journal");
2417
2643
  migratedDatabases.add(file);
@@ -2861,34 +3087,46 @@ function readSnapshotPublications(file) {
2861
3087
  const database = initializeDatabase(file);
2862
3088
  try {
2863
3089
  return database.prepare(`
2864
- SELECT mutation_id AS mutationId, resource_id AS resourceId,
2865
- publication_json AS publicationJson, status,
2866
- submitted_at AS submittedAt
2867
- FROM snapshot_publications ORDER BY sequence
3090
+ SELECT publications.mutation_id AS mutationId,
3091
+ publications.resource_id AS resourceId,
3092
+ publications.meta_json AS metaJson,
3093
+ blobs.snapshot_blob AS snapshotBlob,
3094
+ blobs.operation_blob AS operationBlob,
3095
+ publications.status,
3096
+ publications.submitted_at AS submittedAt
3097
+ FROM snapshot_publications AS publications
3098
+ LEFT JOIN snapshot_publication_blobs AS blobs USING (mutation_id)
3099
+ ORDER BY publications.sequence
2868
3100
  `).all().map((row) => {
2869
- const publication = JSON.parse(row.publicationJson);
2870
- if (!Array.isArray(publication.uploadRecords)) {
2871
- throw new Error(`snapshot publication ${row.mutationId} has no exact upload subset`);
3101
+ // A publication without its payload is damage, never a skippable row —
3102
+ // silently dropping it would let a new publication supersede a
3103
+ // submitted mutation whose outcome exists only at the authority.
3104
+ if (!row.snapshotBlob || !row.operationBlob) {
3105
+ throw new Error(`snapshot publication ${row.mutationId} has no journaled payload`);
2872
3106
  }
2873
- if (!Array.isArray(publication.replacementRootIds)
2874
- || !Array.isArray(publication.replacementRecords)) {
3107
+ const meta = JSON.parse(row.metaJson);
3108
+ // No lawful publication carries zero roots — rootReplacementFromRecords
3109
+ // refuses them at build time, so a rootless row here is damage.
3110
+ if (!Array.isArray(meta.replacementRootIds) || meta.replacementRootIds.length === 0
3111
+ || !Array.isArray(meta.uploadUuids) || meta.operationKind !== "entity.roots.replace") {
2875
3112
  throw new Error(`snapshot publication ${row.mutationId} has no exact root replacement`);
2876
3113
  }
2877
- const deliveryWatermark = Number(publication.deliveryWatermark);
3114
+ const deliveryWatermark = Number(meta.deliveryWatermark);
2878
3115
  if (!Number.isSafeInteger(deliveryWatermark) || deliveryWatermark < 0) {
2879
3116
  throw new Error(`snapshot publication ${row.mutationId} has no delivery watermark`);
2880
3117
  }
2881
3118
  return {
2882
3119
  mutationId: row.mutationId,
2883
3120
  resourceId: row.resourceId,
2884
- authorityEpoch: Number(publication.authorityEpoch),
2885
- baseVersion: Number(publication.baseVersion),
3121
+ authorityEpoch: Number(meta.authorityEpoch),
3122
+ baseVersion: Number(meta.baseVersion),
2886
3123
  deliveryWatermark,
2887
- snapshotChecksum: String(publication.snapshotChecksum),
2888
- snapshotJson: String(publication.snapshotJson),
2889
- uploadRecords: snapshotFromRecords(publication.uploadRecords).records,
2890
- replacementRootIds: publication.replacementRootIds.map(String),
2891
- replacementRecords: snapshotFromRecords(publication.replacementRecords).records,
3124
+ snapshotChecksum: String(meta.snapshotChecksum),
3125
+ operationKind: meta.operationKind,
3126
+ replacementRootIds: meta.replacementRootIds.map(String),
3127
+ uploadUuids: meta.uploadUuids.map(String),
3128
+ snapshotBlob: row.snapshotBlob,
3129
+ operationBlob: row.operationBlob,
2892
3130
  status: row.status === "rejected" ? "rejected" : "pending",
2893
3131
  submitted: row.submittedAt !== null,
2894
3132
  };
@@ -3055,22 +3293,30 @@ function supersedeSnapshotJournal(file, entry) {
3055
3293
  if (unresolved) {
3056
3294
  throw new Error("a submitted publication with an unknown outcome must resolve before supersession");
3057
3295
  }
3296
+ database.prepare(`
3297
+ DELETE FROM snapshot_publication_blobs WHERE mutation_id IN (
3298
+ SELECT mutation_id FROM snapshot_publications WHERE resource_id = ?
3299
+ )
3300
+ `).run(entry.resourceId);
3058
3301
  database.prepare("DELETE FROM snapshot_publications WHERE resource_id = ?")
3059
3302
  .run(entry.resourceId);
3060
3303
  database.prepare(`
3061
3304
  INSERT INTO snapshot_publications(
3062
- mutation_id, resource_id, publication_json, created_at
3305
+ mutation_id, resource_id, meta_json, created_at
3063
3306
  ) VALUES (?, ?, ?, ?)
3064
3307
  `).run(entry.mutationId, entry.resourceId, stableJson({
3065
3308
  authorityEpoch: entry.authorityEpoch,
3066
3309
  baseVersion: entry.baseVersion,
3067
3310
  deliveryWatermark: entry.deliveryWatermark,
3068
3311
  snapshotChecksum: entry.snapshotChecksum,
3069
- snapshotJson: entry.snapshotJson,
3070
- uploadRecords: entry.uploadRecords,
3312
+ operationKind: entry.operationKind,
3071
3313
  replacementRootIds: entry.replacementRootIds,
3072
- replacementRecords: entry.replacementRecords,
3314
+ uploadUuids: entry.uploadUuids,
3073
3315
  }), Date.now());
3316
+ database.prepare(`
3317
+ INSERT INTO snapshot_publication_blobs(mutation_id, snapshot_blob, operation_blob)
3318
+ VALUES (?, ?, ?)
3319
+ `).run(entry.mutationId, entry.snapshotBlob, entry.operationBlob);
3074
3320
  })();
3075
3321
  }
3076
3322
  finally {
@@ -3140,7 +3386,10 @@ function captureChurn(error) {
3140
3386
  function deleteJournalEntry(file, mutationId) {
3141
3387
  const database = initializeDatabase(file);
3142
3388
  try {
3143
- database.prepare("DELETE FROM snapshot_publications WHERE mutation_id = ?").run(mutationId);
3389
+ database.transaction(() => {
3390
+ database.prepare("DELETE FROM snapshot_publication_blobs WHERE mutation_id = ?").run(mutationId);
3391
+ database.prepare("DELETE FROM snapshot_publications WHERE mutation_id = ?").run(mutationId);
3392
+ })();
3144
3393
  }
3145
3394
  finally {
3146
3395
  database.close();
@@ -3520,7 +3769,7 @@ function materializedWorkspaceBindings(bindingDir) {
3520
3769
  return bindings.sort((left, right) => left.workspaceId.localeCompare(right.workspaceId));
3521
3770
  }
3522
3771
  async function reconcileGround(input) {
3523
- const { identity, userRoot, database, cacheDir, suspicions, coreScan, registrationWorkspaceId, onScan, } = input;
3772
+ const { identity, userRoot, database, cacheDir, suspicions, coreScan, registrationWorkspaceId, onScan, measuring = false, } = input;
3524
3773
  const resourceId = privateEntityResourceId(identity.userId, sha256Hex);
3525
3774
  const bindingDir = workspaceBindingDir(userRoot, identity.deviceId);
3526
3775
  const boundRootEntries = materializedWorkspaceBindings(bindingDir);
@@ -3615,9 +3864,24 @@ async function reconcileGround(input) {
3615
3864
  contentReads: 0,
3616
3865
  gitInspections: 0,
3617
3866
  repositoryCaptures: 0,
3867
+ walkMs: 0,
3868
+ captureMs: 0,
3869
+ captureBusyMs: 0,
3870
+ captureBytes: 0,
3871
+ gitMs: 0,
3872
+ repositoryIdentityMs: 0,
3873
+ repositoryCaptureMs: 0,
3874
+ repositorySealMs: 0,
3875
+ reconcileMs: 0,
3876
+ persistMs: 0,
3618
3877
  };
3619
3878
  const started = performance.now();
3620
- const result = await reconcileRoot({ ...parameters, evidence: counters, materializedUUIDs });
3879
+ const result = await reconcileRoot({
3880
+ ...parameters,
3881
+ evidence: counters,
3882
+ measureEvidence: measuring,
3883
+ materializedUUIDs,
3884
+ });
3621
3885
  if (result.transferFailure !== null) {
3622
3886
  transferFailures.push({ rootId: parameters.rootUUID, reason: result.transferFailure });
3623
3887
  }
@@ -3655,7 +3919,14 @@ async function reconcileGround(input) {
3655
3919
  durationMs: performance.now() - started,
3656
3920
  plannedRows: proposals.length,
3657
3921
  records: proposals.length,
3658
- recordBytes: proposals.reduce((bytes, proposal) => bytes + encodedRecordBytes(parameters.resourceId, proposal), 0),
3922
+ // Encoding every proposal to learn its wire size costs one record
3923
+ // construction per entity — real time on the critical path of a cold
3924
+ // Register. It is evidence, so it is paid only when read.
3925
+ ...(measuring
3926
+ ? {
3927
+ recordBytes: proposals.reduce((bytes, proposal) => bytes + encodedRecordBytes(parameters.resourceId, proposal), 0),
3928
+ }
3929
+ : {}),
3659
3930
  });
3660
3931
  // Each root commits its local projection before Detect yields. Native
3661
3932
  // callbacks can therefore preserve new suspicion between large roots.
@@ -3754,7 +4025,7 @@ async function reconcileGround(input) {
3754
4025
  };
3755
4026
  }
3756
4027
  async function reconcileRoot(input) {
3757
- const { identity, resourceId, rootUUID, rootName, rootPath, rootType, database, cacheDir, policy, bindingDir, suspects = null, evidence, plansSendRecords, largeManifests, } = input;
4028
+ const { identity, resourceId, rootUUID, rootName, rootPath, rootType, database, cacheDir, policy, bindingDir, suspects = null, evidence, measureEvidence = false, plansSendRecords, largeManifests, } = input;
3758
4029
  // Prior rows reach this scan from every binding this root encloses. A row's
3759
4030
  // stored relativePath is relative to the root that last committed it, which
3760
4031
  // is a different frame when an enclosing boundary was just registered
@@ -3927,6 +4198,33 @@ async function reconcileRoot(input) {
3927
4198
  // its own entry in place; every task is awaited before entries are read.
3928
4199
  const captureTasks = [];
3929
4200
  const captureGate = concurrencyGate(WALK_CAPTURE_CONCURRENCY);
4201
+ // The scan's three costs are metadata traversal, content capture, and Git
4202
+ // inspection. They interleave, so each is accumulated where it is spent
4203
+ // rather than inferred from the total afterwards.
4204
+ let captureOpenedAt = 0;
4205
+ let captureClosedAt = 0;
4206
+ const spent = (work, into) => {
4207
+ if (!measureEvidence || !evidence)
4208
+ return work();
4209
+ const at = performance.now();
4210
+ try {
4211
+ return work();
4212
+ }
4213
+ finally {
4214
+ evidence[into] += performance.now() - at;
4215
+ }
4216
+ };
4217
+ const spentAsync = async (work, into) => {
4218
+ if (!measureEvidence || !evidence)
4219
+ return work();
4220
+ const at = performance.now();
4221
+ try {
4222
+ return await work();
4223
+ }
4224
+ finally {
4225
+ evidence[into] += performance.now() - at;
4226
+ }
4227
+ };
3930
4228
  const visit = async (directory, parentUUID, parentType, base = "", activeRepository = parentType === "repo.git"
3931
4229
  ? {
3932
4230
  root: directory,
@@ -3934,8 +4232,8 @@ async function reconcileRoot(input) {
3934
4232
  evidence: directory === rootPath ? rootRepositoryEvidence : null,
3935
4233
  }
3936
4234
  : null, forceObserve = rootRailChanged) => {
3937
- const children = readdirSync(directory, { withFileTypes: true })
3938
- .sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
4235
+ const children = spent(() => readdirSync(directory, { withFileTypes: true })
4236
+ .sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0), "walkMs");
3939
4237
  for (const child of children) {
3940
4238
  // Every materialization boundary reserves its root .amalgm/ directory
3941
4239
  // for machine intents, staging, and recovery. This is structural host
@@ -3951,7 +4249,7 @@ async function reconcileRoot(input) {
3951
4249
  if (!policy(relativePath))
3952
4250
  continue;
3953
4251
  const absolutePath = join(directory, child.name);
3954
- const stats = lstatSync(absolutePath);
4252
+ const stats = spent(() => lstatSync(absolutePath), "walkMs");
3955
4253
  evidence && (evidence.entries += 1);
3956
4254
  // The walk is synchronous metadata work. Yield periodically so the
3957
4255
  // wire, Receive, and Apply keep running while a large root is proved.
@@ -3982,7 +4280,7 @@ async function reconcileRoot(input) {
3982
4280
  if (observe && repositoryPath && activeRepository && !activeRepository.evidence) {
3983
4281
  evidence && (evidence.gitInspections += 1);
3984
4282
  try {
3985
- activeRepository.evidence = inspectGitRegistration(activeRepository.root, activeRepository.evidencePaths);
4283
+ activeRepository.evidence = spent(() => inspectGitRegistration(activeRepository.root, activeRepository.evidencePaths), "gitMs");
3986
4284
  }
3987
4285
  catch {
3988
4286
  // Classification already settled from the marker. If Git cannot
@@ -4075,11 +4373,19 @@ async function reconcileRoot(input) {
4075
4373
  });
4076
4374
  if (deferredCapture) {
4077
4375
  const entry = entries[entries.length - 1];
4376
+ if (measureEvidence && evidence && captureOpenedAt === 0)
4377
+ captureOpenedAt = performance.now();
4078
4378
  captureTasks.push(captureGate(async () => {
4379
+ const taskStarted = measureEvidence ? performance.now() : 0;
4079
4380
  let current = stats;
4080
4381
  for (let attempt = 1;; attempt++) {
4081
4382
  try {
4082
4383
  const captured = await captureContentFile(absolutePath, current, activeRepository || !plansSendRecords ? null : cacheDir);
4384
+ if (measureEvidence && evidence) {
4385
+ evidence.captureBusyMs += performance.now() - taskStarted;
4386
+ evidence.captureBytes += captured.bytes;
4387
+ captureClosedAt = performance.now();
4388
+ }
4083
4389
  entry.type = classifyFile({ binary: captured.binary });
4084
4390
  entry.payloadVersion = captured.contentHash;
4085
4391
  entry.contentVerifiedAtMs = Date.now();
@@ -4125,6 +4431,10 @@ async function reconcileRoot(input) {
4125
4431
  };
4126
4432
  await visit(rootPath, rootUUID, rootType);
4127
4433
  await Promise.all(captureTasks);
4434
+ if (measureEvidence && evidence && captureOpenedAt !== 0) {
4435
+ evidence.captureMs += captureClosedAt - captureOpenedAt;
4436
+ }
4437
+ const reconcileStarted = measureEvidence ? performance.now() : 0;
4128
4438
  // When every entry sits at its stored address with its stored type and
4129
4439
  // physical fingerprint (honoring any fixed UUID), path-first claiming makes
4130
4440
  // the multi-pass reconciliation the identity map — resolve directly. Any
@@ -4158,6 +4468,8 @@ async function reconcileRoot(input) {
4158
4468
  inode: entry.inode,
4159
4469
  ...(entry.fixedUUID ? { fixedUUID: entry.fixedUUID } : {}),
4160
4470
  })));
4471
+ if (measureEvidence && evidence)
4472
+ evidence.reconcileMs += performance.now() - reconcileStarted;
4161
4473
  const resolvedByProvisional = new Map();
4162
4474
  for (const entry of entries) {
4163
4475
  const resolved = resolvedIdentity.get(entry.relativePath) ?? entry.uuid;
@@ -4224,13 +4536,13 @@ async function reconcileRoot(input) {
4224
4536
  contentVerifiedAtMs: entry.contentVerifiedAtMs,
4225
4537
  };
4226
4538
  });
4227
- const persistCurrentRows = () => {
4539
+ const persistCurrentRows = () => spent(() => {
4228
4540
  const rows = rowsFromEntries();
4229
4541
  if (input.suspicion === undefined) {
4230
4542
  persistRows(database, identity, resourceId, rootUUID, rows, existingRows);
4231
4543
  }
4232
4544
  return rows;
4233
- };
4545
+ }, "persistMs");
4234
4546
  // Seal nested repositories first so their independent rows are complete
4235
4547
  // before the enclosing graph is persisted. Their state is not an input to
4236
4548
  // the enclosing identity map; only their address boundary is.
@@ -4358,7 +4670,7 @@ async function reconcileRoot(input) {
4358
4670
  }
4359
4671
  }
4360
4672
  const fingerprintBefore = gitEvidenceFingerprint(repository.absolutePath);
4361
- let repoIdentity = await refreshRepositoryIdentity();
4673
+ let repoIdentity = await spentAsync(refreshRepositoryIdentity, "repositoryIdentityMs");
4362
4674
  /* The rows and the transport identity are derived from the same fresh
4363
4675
  * worktree evidence. A later Watch ring for one of these dirty paths is
4364
4676
  * therefore ordinary equality, not a transport-only echo. */
@@ -4396,8 +4708,8 @@ async function reconcileRoot(input) {
4396
4708
  let captured = null;
4397
4709
  let coherent = false;
4398
4710
  for (let attempt = 0; attempt < 3; attempt += 1) {
4399
- captured = captureRepository(repository.absolutePath, repoIdentity, prior, previousIdentity);
4400
- const after = await refreshRepositoryIdentity();
4711
+ captured = spent(() => captureRepository(repository.absolutePath, repoIdentity, prior, previousIdentity), "repositoryCaptureMs");
4712
+ const after = await spentAsync(refreshRepositoryIdentity, "repositoryIdentityMs");
4401
4713
  if (sameGitIdentity(after, repoIdentity)) {
4402
4714
  repoIdentity = [...captured.identity];
4403
4715
  coherent = true;
@@ -4419,15 +4731,18 @@ async function reconcileRoot(input) {
4419
4731
  }
4420
4732
  repository.payloadVersion = captured.stateId;
4421
4733
  repository.transportVersion = captured.transportVersion;
4422
- if (captured.bytes) {
4423
- sealCapturedArtifact(cacheDir, {
4424
- entityId: repository.uuid,
4425
- entityType: "repo.git",
4426
- kind: "git",
4427
- contentHash: captured.transportVersion,
4428
- }, captured.bytes);
4429
- }
4430
- const currentLayout = inspectRepositoryTransportFile(join(cacheDir, `${captured.transportVersion}.bin`));
4734
+ const settled = captured;
4735
+ const currentLayout = spent(() => {
4736
+ if (settled.bytes) {
4737
+ sealCapturedArtifact(cacheDir, {
4738
+ entityId: repository.uuid,
4739
+ entityType: "repo.git",
4740
+ kind: "git",
4741
+ contentHash: settled.transportVersion,
4742
+ }, settled.bytes);
4743
+ }
4744
+ return inspectRepositoryTransportFile(join(cacheDir, `${settled.transportVersion}.bin`));
4745
+ }, "repositorySealMs");
4431
4746
  replayByUuid.set(repository.uuid, {
4432
4747
  kind: "repo.git",
4433
4748
  stateId: currentLayout.stateId,