mlclaw 0.2.3 → 0.3.1

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.
@@ -10,6 +10,8 @@ export type SyncConfig = {
10
10
  liveDir: string;
11
11
  /** Bucket repo id (`owner/name`), or null when no bucket is configured. */
12
12
  bucket: string | null;
13
+ /** Mounted bucket directory for Space runtimes, or null to use the Hub API. */
14
+ stateMountDir: string | null;
13
15
  /** Path prefix inside the bucket for all sync objects. */
14
16
  bucketPrefix: string;
15
17
  intervalSeconds: number;
@@ -22,9 +24,14 @@ export type SyncConfig = {
22
24
  agentName: string;
23
25
  gatewayLocation: "local" | "space" | "unknown";
24
26
  runtimeImage: string;
27
+ /** UID/GID used by the secret-free snapshot traversal worker. */
28
+ snapshotUid?: number;
29
+ snapshotGid?: number;
30
+ /** Root-owned runtime state overlaid by the trusted snapshot supervisor. */
31
+ protectedStateDir?: string;
25
32
  };
26
33
 
27
- const DEFAULT_LIVE_DIR = "/tmp/openclaw-live";
34
+ const DEFAULT_LIVE_DIR = "/home/node/.local/share/mlclaw/live";
28
35
  export const DEFAULT_BUCKET_PREFIX = "openclaw-state";
29
36
  const DEFAULT_INTERVAL_SECONDS = 60;
30
37
  const DEFAULT_HANDOFF_POLL_SECONDS = 5;
@@ -37,9 +44,12 @@ function positiveIntFromEnv(value: string | undefined, fallback: number): number
37
44
 
38
45
  export function resolveSyncConfig(env: NodeJS.ProcessEnv = process.env): SyncConfig {
39
46
  const runId = env.MLCLAW_RUN_ID?.trim() || randomUUID();
47
+ const snapshotUid = nonNegativeIntFromEnv(env.MLCLAW_OPENCLAW_UID);
48
+ const snapshotGid = nonNegativeIntFromEnv(env.MLCLAW_OPENCLAW_GID);
40
49
  return {
41
50
  liveDir: env.OPENCLAW_LIVE_DIR?.trim() || DEFAULT_LIVE_DIR,
42
51
  bucket: env.OPENCLAW_HF_STATE_BUCKET?.trim() || null,
52
+ stateMountDir: env.MLCLAW_STATE_MOUNT_DIR?.trim() || null,
43
53
  bucketPrefix: normalizeBucketPrefix(env.OPENCLAW_HF_STATE_PREFIX),
44
54
  intervalSeconds: positiveIntFromEnv(env.HF_STATE_SYNC_INTERVAL_SECONDS, DEFAULT_INTERVAL_SECONDS),
45
55
  handoffPollSeconds: positiveIntFromEnv(env.HF_STATE_SYNC_HANDOFF_POLL_SECONDS, DEFAULT_HANDOFF_POLL_SECONDS),
@@ -47,13 +57,22 @@ export function resolveSyncConfig(env: NodeJS.ProcessEnv = process.env): SyncCon
47
57
  runId,
48
58
  runtimeId: env.MLCLAW_RUNTIME_ID?.trim() || runId,
49
59
  agentName: env.OPENCLAW_AGENT_NAME?.trim() || "openclaw",
50
- gatewayLocation: env.MLCLAW_GATEWAY_LOCATION === "local" || env.MLCLAW_GATEWAY_LOCATION === "space"
51
- ? env.MLCLAW_GATEWAY_LOCATION
52
- : "unknown",
60
+ gatewayLocation:
61
+ env.MLCLAW_GATEWAY_LOCATION === "local" || env.MLCLAW_GATEWAY_LOCATION === "space"
62
+ ? env.MLCLAW_GATEWAY_LOCATION
63
+ : "unknown",
53
64
  runtimeImage: env.MLCLAW_RUNTIME_IMAGE?.trim() || "unknown",
65
+ ...(snapshotUid !== undefined ? { snapshotUid } : {}),
66
+ ...(snapshotGid !== undefined ? { snapshotGid } : {}),
67
+ ...(env.MLCLAW_PROTECTED_STATE_DIR?.trim() ? { protectedStateDir: env.MLCLAW_PROTECTED_STATE_DIR.trim() } : {}),
54
68
  };
55
69
  }
56
70
 
71
+ function nonNegativeIntFromEnv(value: string | undefined): number | undefined {
72
+ const parsed = Number(value);
73
+ return Number.isInteger(parsed) && parsed >= 0 ? parsed : undefined;
74
+ }
75
+
57
76
  /** Remote object path under the configured bucket prefix. */
58
77
  export function remotePath(config: Pick<SyncConfig, "bucketPrefix">, name: string): string {
59
78
  return `${normalizeBucketPrefix(config.bucketPrefix)}/${name.replace(/^\/+/, "")}`;
@@ -0,0 +1,79 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import type { SyncConfig } from "./paths.js";
4
+
5
+ export async function prepareRestore(config: SyncConfig): Promise<void> {
6
+ if (config.snapshotUid === undefined || config.snapshotGid === undefined) {
7
+ throw new Error("restore preparation requires MLCLAW_OPENCLAW_UID and MLCLAW_OPENCLAW_GID");
8
+ }
9
+ const liveParent = path.dirname(config.liveDir);
10
+ await fs.mkdir(liveParent, { recursive: true });
11
+ await fs.chown(liveParent, config.snapshotUid, config.snapshotGid);
12
+
13
+ if (!config.stateMountDir) {
14
+ return;
15
+ }
16
+ await makeTraversableDirectory(config.stateMountDir);
17
+ const prefixRoot = confinedPath(config.stateMountDir, config.bucketPrefix);
18
+ let prefixPart = config.stateMountDir;
19
+ for (const part of config.bucketPrefix.split("/").filter(Boolean)) {
20
+ prefixPart = path.join(prefixPart, part);
21
+ await makeTraversableDirectory(prefixPart);
22
+ }
23
+ await makeReadableIfFile(path.join(prefixRoot, "manifest.json"));
24
+ const snapshotsDir = path.join(prefixRoot, "snapshots");
25
+ await makeTraversableDirectory(snapshotsDir);
26
+ let entries;
27
+ try {
28
+ entries = await fs.readdir(snapshotsDir, { withFileTypes: true });
29
+ } catch (err) {
30
+ if (isNotFound(err)) {
31
+ return;
32
+ }
33
+ throw err;
34
+ }
35
+ for (const entry of entries) {
36
+ if (entry.isFile()) {
37
+ await fs.chmod(path.join(snapshotsDir, entry.name), 0o644);
38
+ }
39
+ }
40
+ }
41
+
42
+ async function makeTraversableDirectory(directory: string): Promise<void> {
43
+ try {
44
+ const stat = await fs.lstat(directory);
45
+ if (stat.isDirectory()) {
46
+ await fs.chmod(directory, 0o711);
47
+ }
48
+ } catch (err) {
49
+ if (!isNotFound(err)) {
50
+ throw err;
51
+ }
52
+ }
53
+ }
54
+
55
+ function confinedPath(root: string, relative: string): string {
56
+ const resolvedRoot = path.resolve(root);
57
+ const resolved = path.resolve(resolvedRoot, relative);
58
+ if (resolved !== resolvedRoot && !resolved.startsWith(`${resolvedRoot}${path.sep}`)) {
59
+ throw new Error(`invalid state prefix outside mounted bucket: ${relative}`);
60
+ }
61
+ return resolved;
62
+ }
63
+
64
+ async function makeReadableIfFile(file: string): Promise<void> {
65
+ try {
66
+ const stat = await fs.lstat(file);
67
+ if (stat.isFile()) {
68
+ await fs.chmod(file, 0o644);
69
+ }
70
+ } catch (err) {
71
+ if (!isNotFound(err)) {
72
+ throw err;
73
+ }
74
+ }
75
+ }
76
+
77
+ function isNotFound(err: unknown): boolean {
78
+ return err instanceof Error && "code" in err && err.code === "ENOENT";
79
+ }
@@ -18,6 +18,15 @@ export type SnapshotOutcome =
18
18
  | { kind: "skipped"; reason: "no-bucket" | "empty-state" }
19
19
  | { kind: "failed"; detail: string };
20
20
 
21
+ export type StagedArchiveOutcome =
22
+ | { kind: "staged"; databaseCount: number }
23
+ | { kind: "corrupt-database"; database: string; detail: string };
24
+
25
+ export type StageArchive = (params: {
26
+ liveDir: string;
27
+ archivePath: string;
28
+ }) => Promise<StagedArchiveOutcome>;
29
+
21
30
  // Object names must be unique across overlapping containers (run-id suffix)
22
31
  // AND within one run (counter): a final snapshot can land in the same second
23
32
  // as the last interval snapshot, and an overwritten tarball behind a distinct
@@ -61,6 +70,7 @@ export async function runSnapshot(params: {
61
70
  hub: BucketHub;
62
71
  bootTime: string;
63
72
  now?: () => Date;
73
+ stageArchive?: StageArchive;
64
74
  }): Promise<SnapshotOutcome> {
65
75
  const { config, hub } = params;
66
76
  if (!config.bucket) {
@@ -74,8 +84,13 @@ export async function runSnapshot(params: {
74
84
 
75
85
  const workDir = await fs.mkdtemp(path.join(os.tmpdir(), "hf-state-snapshot-"));
76
86
  try {
77
- const stagingDir = path.join(workDir, "stage");
78
- const staged = await stageLiveDir(config.liveDir, stagingDir);
87
+ const now = (params.now ?? (() => new Date()))();
88
+ const id = snapshotId(now, config.runId);
89
+ const archiveName = `state-${id}.tar.zst`;
90
+ const archivePath = path.join(workDir, archiveName);
91
+ const staged = params.stageArchive
92
+ ? await params.stageArchive({ liveDir: config.liveDir, archivePath })
93
+ : await stageArchiveInProcess(config.liveDir, path.join(workDir, "stage"), archivePath);
79
94
  if (staged.kind === "corrupt-database") {
80
95
  return {
81
96
  kind: "failed",
@@ -83,12 +98,6 @@ export async function runSnapshot(params: {
83
98
  };
84
99
  }
85
100
 
86
- const now = (params.now ?? (() => new Date()))();
87
- const id = snapshotId(now, config.runId);
88
- const archiveName = `state-${id}.tar.zst`;
89
- const archivePath = path.join(workDir, archiveName);
90
- await createTarZst(stagingDir, archivePath);
91
-
92
101
  const entry: SnapshotEntry = {
93
102
  id,
94
103
  path: remotePath(config, `snapshots/${archiveName}`),
@@ -123,7 +132,7 @@ export async function runSnapshot(params: {
123
132
  if (expired.length > 0) {
124
133
  await hub.delete(expired.map((e) => e.path));
125
134
  }
126
- log(`snapshot ${entry.id} uploaded (${entry.sizeBytes} bytes, ${staged.databases.length} dbs)`);
135
+ log(`snapshot ${entry.id} uploaded (${entry.sizeBytes} bytes, ${staged.databaseCount} dbs)`);
127
136
  return { kind: "uploaded", entry };
128
137
  } catch (err) {
129
138
  return { kind: "failed", detail: err instanceof Error ? err.message : String(err) };
@@ -131,3 +140,16 @@ export async function runSnapshot(params: {
131
140
  await fs.rm(workDir, { recursive: true, force: true });
132
141
  }
133
142
  }
143
+
144
+ async function stageArchiveInProcess(
145
+ liveDir: string,
146
+ stagingDir: string,
147
+ archivePath: string,
148
+ ): Promise<StagedArchiveOutcome> {
149
+ const staged = await stageLiveDir(liveDir, stagingDir);
150
+ if (staged.kind === "corrupt-database") {
151
+ return staged;
152
+ }
153
+ await createTarZst(stagingDir, archivePath);
154
+ return { kind: "staged", databaseCount: staged.databases.length };
155
+ }
@@ -0,0 +1,165 @@
1
+ import { spawn } from "node:child_process";
2
+ import { createReadStream, createWriteStream, writeFileSync } from "node:fs";
3
+ import fs from "node:fs/promises";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { pipeline } from "node:stream/promises";
7
+ import { PROTECTED_STATE_DIR_NAME, createTarZst, extractTarZst, stageLiveDir } from "./archive.js";
8
+ import type { SyncConfig } from "./paths.js";
9
+ import type { StageArchive, StagedArchiveOutcome } from "./snapshot.js";
10
+
11
+ type WorkerMessage = StagedArchiveOutcome | { kind: "failed"; detail: string };
12
+
13
+ export function unprivilegedStageArchive(params: { uid: number; gid: number; scriptPath: string }): StageArchive {
14
+ return async ({ liveDir, archivePath }) => {
15
+ const child = spawn(process.execPath, [params.scriptPath, "stage-worker", liveDir], {
16
+ uid: params.uid,
17
+ gid: params.gid,
18
+ env: snapshotWorkerEnvironment(process.env),
19
+ stdio: ["ignore", "pipe", "inherit", "pipe"],
20
+ });
21
+ if (!child.stdout || !child.stdio[3]) {
22
+ child.kill("SIGKILL");
23
+ throw new Error("snapshot staging worker pipes are unavailable");
24
+ }
25
+ const archiveOutput = createWriteStream(archivePath, { flags: "wx", mode: 0o600 });
26
+ const metadata = collect(child.stdio[3] as NodeJS.ReadableStream);
27
+ const archive = pipeline(child.stdout, archiveOutput);
28
+ const exitCode = await new Promise<number>((resolve, reject) => {
29
+ child.once("error", reject);
30
+ child.once("exit", (code, signal) => resolve(code ?? (signal ? 128 : 1)));
31
+ });
32
+ await archive;
33
+ const message = parseWorkerMessage(await metadata);
34
+ if (exitCode !== 0 || message.kind === "failed") {
35
+ throw new Error(
36
+ message.kind === "failed" ? message.detail : `snapshot staging worker exited with code ${exitCode}`,
37
+ );
38
+ }
39
+ return message;
40
+ };
41
+ }
42
+
43
+ export function protectedStageArchive(params: {
44
+ base: StageArchive;
45
+ sourceDir: string;
46
+ archiveName: string;
47
+ }): StageArchive {
48
+ return async (request) => {
49
+ const outcome = await params.base(request);
50
+ if (outcome.kind !== "staged") {
51
+ return outcome;
52
+ }
53
+ const workDir = await fs.mkdtemp(path.join(os.tmpdir(), "hf-state-protected-stage-"));
54
+ try {
55
+ const stagingDir = path.join(workDir, "stage");
56
+ await extractTarZst(request.archivePath, stagingDir);
57
+ const destination = path.join(stagingDir, params.archiveName);
58
+ await fs.cp(params.sourceDir, destination, {
59
+ recursive: true,
60
+ force: false,
61
+ preserveTimestamps: true,
62
+ filter: (source) => includeProtectedSnapshotPath(params.sourceDir, source),
63
+ });
64
+ await fs.chmod(destination, 0o700);
65
+ await fs.rm(request.archivePath, { force: true });
66
+ await createTarZst(stagingDir, request.archivePath);
67
+ await fs.chmod(request.archivePath, 0o600);
68
+ return outcome;
69
+ } finally {
70
+ await fs.rm(workDir, { recursive: true, force: true });
71
+ }
72
+ };
73
+ }
74
+
75
+ export function includeProtectedSnapshotPath(sourceDir: string, source: string): boolean {
76
+ const relative = path.relative(sourceDir, source);
77
+ return relative !== "hf-broker/mirrors" && !relative.startsWith(`hf-broker/mirrors${path.sep}`);
78
+ }
79
+
80
+ export function trustedStageArchive(config: SyncConfig, scriptPath: string | undefined): StageArchive | undefined {
81
+ const canStageAsOpenClaw =
82
+ process.getuid?.() === 0 &&
83
+ Boolean(scriptPath) &&
84
+ config.snapshotUid !== undefined &&
85
+ config.snapshotGid !== undefined;
86
+ if (!canStageAsOpenClaw) {
87
+ if (config.protectedStateDir) {
88
+ throw new Error("protected runtime state requires root snapshot staging with an OpenClaw UID and GID");
89
+ }
90
+ return undefined;
91
+ }
92
+
93
+ let stageArchive = unprivilegedStageArchive({
94
+ uid: config.snapshotUid as number,
95
+ gid: config.snapshotGid as number,
96
+ scriptPath: scriptPath as string,
97
+ });
98
+ if (config.protectedStateDir) {
99
+ stageArchive = protectedStageArchive({
100
+ base: stageArchive,
101
+ sourceDir: config.protectedStateDir,
102
+ archiveName: PROTECTED_STATE_DIR_NAME,
103
+ });
104
+ }
105
+ return stageArchive;
106
+ }
107
+
108
+ export async function runStageWorker(liveDir: string): Promise<number> {
109
+ const workDir = await fs.mkdtemp(path.join(os.tmpdir(), "hf-state-stage-worker-"));
110
+ try {
111
+ const stagingDir = path.join(workDir, "stage");
112
+ const archivePath = path.join(workDir, "snapshot.tar.zst");
113
+ const staged = await stageLiveDir(liveDir, stagingDir, { excludeProtectedState: true });
114
+ if (staged.kind === "corrupt-database") {
115
+ writeWorkerMessage(staged);
116
+ return 0;
117
+ }
118
+ await createTarZst(stagingDir, archivePath);
119
+ writeWorkerMessage({ kind: "staged", databaseCount: staged.databases.length });
120
+ await pipeline(createReadStream(archivePath), process.stdout);
121
+ return 0;
122
+ } catch (err) {
123
+ writeWorkerMessage({ kind: "failed", detail: err instanceof Error ? err.message : String(err) });
124
+ return 1;
125
+ } finally {
126
+ await fs.rm(workDir, { recursive: true, force: true });
127
+ }
128
+ }
129
+
130
+ export function snapshotWorkerEnvironment(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
131
+ return {
132
+ HOME: "/home/node",
133
+ PATH: env.PATH,
134
+ TMPDIR: env.TMPDIR,
135
+ };
136
+ }
137
+
138
+ function writeWorkerMessage(message: WorkerMessage): void {
139
+ writeFileSync(3, `${JSON.stringify(message)}\n`);
140
+ }
141
+
142
+ async function collect(stream: NodeJS.ReadableStream | null): Promise<string> {
143
+ if (!stream) {
144
+ throw new Error("snapshot staging worker metadata pipe is unavailable");
145
+ }
146
+ const chunks: Buffer[] = [];
147
+ for await (const chunk of stream) {
148
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
149
+ }
150
+ return Buffer.concat(chunks).toString("utf8");
151
+ }
152
+
153
+ function parseWorkerMessage(raw: string): WorkerMessage {
154
+ const parsed = JSON.parse(raw) as WorkerMessage;
155
+ if (parsed.kind === "staged" && Number.isInteger(parsed.databaseCount) && parsed.databaseCount >= 0) {
156
+ return parsed;
157
+ }
158
+ if (parsed.kind === "corrupt-database" && parsed.database && parsed.detail) {
159
+ return parsed;
160
+ }
161
+ if (parsed.kind === "failed" && parsed.detail) {
162
+ return parsed;
163
+ }
164
+ throw new Error("snapshot staging worker returned invalid metadata");
165
+ }
@@ -6,6 +6,7 @@ import { setTimeout as delay } from "node:timers/promises";
6
6
  import type { BucketHub } from "./hub.js";
7
7
  import { type SyncConfig, log, logError, remotePath } from "./paths.js";
8
8
  import { runSnapshot, type SnapshotOutcome } from "./snapshot.js";
9
+ import { trustedStageArchive } from "./stage-worker.js";
9
10
 
10
11
  const LEASE_HEARTBEAT_MS = 60_000;
11
12
  const HANDOFF_REQUEST_TTL_MS = 10 * 60 * 1000;
@@ -17,17 +18,15 @@ const HANDOFF_REQUEST_TTL_MS = 10 * 60 * 1000;
17
18
  * is lost. If the platform hard-kills us first, the interval loop has already
18
19
  * bounded the loss the same way.
19
20
  */
20
- export async function supervise(params: {
21
- config: SyncConfig;
22
- hub: BucketHub;
23
- command: string[];
24
- }): Promise<number> {
21
+ export async function supervise(params: { config: SyncConfig; hub: BucketHub; command: string[] }): Promise<number> {
25
22
  const { config, hub, command } = params;
26
23
  const [binary, ...args] = command;
27
24
  if (!binary) {
28
25
  throw new Error("supervise: missing child command");
29
26
  }
30
27
  const bootTime = new Date().toISOString();
28
+ const scriptPath = process.argv[1];
29
+ const stageArchive = trustedStageArchive(config, scriptPath);
31
30
  let lastSnapshotId: string | undefined;
32
31
  const handoffState: { request: RuntimeHandoffRequest | null } = { request: null };
33
32
 
@@ -104,7 +103,10 @@ export async function supervise(params: {
104
103
  }
105
104
  };
106
105
 
107
- const child: ChildProcess = spawn(binary, args, { stdio: "inherit" });
106
+ const child: ChildProcess = spawn(binary, args, {
107
+ stdio: "inherit",
108
+ env: supervisedChildEnvironment(process.env),
109
+ });
108
110
  const childExit = new Promise<number>((resolve) => {
109
111
  child.on("exit", (code, signal) => resolve(code ?? (signal ? 128 : 1)));
110
112
  child.on("error", (err) => {
@@ -117,7 +119,12 @@ export async function supervise(params: {
117
119
  let inFlight: Promise<SnapshotOutcome> | null = null;
118
120
  const runOnce = async (label: string): Promise<SnapshotOutcome> => {
119
121
  try {
120
- const outcome = await runSnapshot({ config, hub, bootTime });
122
+ const outcome = await runSnapshot({
123
+ config,
124
+ hub,
125
+ bootTime,
126
+ ...(stageArchive ? { stageArchive } : {}),
127
+ });
121
128
  if (outcome.kind === "failed") {
122
129
  logError(`${label}: snapshot failed: ${outcome.detail}`);
123
130
  } else if (outcome.kind === "uploaded") {
@@ -162,7 +169,9 @@ export async function supervise(params: {
162
169
  void snapshotLoop;
163
170
 
164
171
  const heartbeatLoop = (async () => {
165
- await writeLease().catch((err) => logError(`initial lease failed: ${err instanceof Error ? err.message : String(err)}`));
172
+ await writeLease().catch((err) =>
173
+ logError(`initial lease failed: ${err instanceof Error ? err.message : String(err)}`),
174
+ );
166
175
  while (!stopping) {
167
176
  await delay(LEASE_HEARTBEAT_MS);
168
177
  if (stopping) {
@@ -224,10 +233,14 @@ export async function supervise(params: {
224
233
  if (handoffState.request) {
225
234
  const request = handoffState.request;
226
235
  if (finalOutcome.kind !== "uploaded") {
227
- throw new Error(`handoff ${request.requestId} final snapshot did not upload: ${snapshotFailureDetail(finalOutcome)}`);
236
+ throw new Error(
237
+ `handoff ${request.requestId} final snapshot did not upload: ${snapshotFailureDetail(finalOutcome)}`,
238
+ );
228
239
  }
229
240
  await writeHandoffAck(request).catch((err) => {
230
- throw new Error(`handoff ${request.requestId} snapshot completed but ack failed: ${err instanceof Error ? err.message : String(err)}`);
241
+ throw new Error(
242
+ `handoff ${request.requestId} snapshot completed but ack failed: ${err instanceof Error ? err.message : String(err)}`,
243
+ );
231
244
  });
232
245
  log(`handoff ${request.requestId} acknowledged`);
233
246
  return 0;
@@ -235,6 +248,12 @@ export async function supervise(params: {
235
248
  return exitCode;
236
249
  }
237
250
 
251
+ export function supervisedChildEnvironment(source: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
252
+ const env = { ...source };
253
+ delete env.MLCLAW_STATE_HF_TOKEN;
254
+ return env;
255
+ }
256
+
238
257
  function snapshotFailureDetail(outcome: SnapshotOutcome): string {
239
258
  switch (outcome.kind) {
240
259
  case "uploaded":
@@ -35,7 +35,7 @@ export class ChunkCache {
35
35
  return;
36
36
  }
37
37
  if (this.map.values().next().value === this.index) {
38
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
38
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- The equality check proves the oldest key exists.
39
39
  this.map.delete(this.map.keys().next().value!);
40
40
  }
41
41
  this.map.set(hash, this.index);
@@ -137,7 +137,7 @@ export class XetBlob extends Blob {
137
137
  fetch: this.fetch,
138
138
  hash: this.hash,
139
139
  refreshUrl: this.refreshUrl,
140
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
140
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Clone is called only after reconstruction URL initialization.
141
141
  reconstructionUrl: this.reconstructionUrl!,
142
142
  size: this.size,
143
143
  });
@@ -258,7 +258,7 @@ export class XetBlob extends Blob {
258
258
  if (termRanges.every((range) => range.data)) {
259
259
  log("all data available for term", term.hash, readBytesToSkip);
260
260
  rangeLoop: for (const range of termRanges) {
261
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
261
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- The preceding every check proves range data is present.
262
262
  for (let chunk of range.data!) {
263
263
  if (readBytesToSkip) {
264
264
  const skipped = Math.min(readBytesToSkip, chunk.byteLength);
@@ -32,7 +32,7 @@ const INTERVAL_BETWEEN_REMOTE_DEDUP = 4_000_000; // 4MB
32
32
  * 0.5 = show progress when uploading the xorb and when processing the file
33
33
  */
34
34
  const PROCESSING_PROGRESS_RATIO = 0.1;
35
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
35
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars -- Kept in sync with the vendored upstream progress constants.
36
36
  const UPLOADING_PROGRESS_RATIO = 1 - PROCESSING_PROGRESS_RATIO;
37
37
 
38
38
  function computeXorbHashHex(chunks: { hash: string; length: number }[]): string {
@@ -703,7 +703,7 @@ async function loadDedupInfoToCache(
703
703
  const chunker = createChunker(TARGET_CHUNK_SIZE);
704
704
  const cache = chunkCache;
705
705
 
706
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
706
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars -- Preserved for parity with upstream deduplication accounting.
707
707
  let dedupedBytes = 0;
708
708
  let chunksProcessed = 0;
709
709
  let totalBytes = 0;