@openclaw/diffs 2026.7.2-beta.1 → 2026.7.2-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -10,8 +10,9 @@ import { normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "opencl
10
10
  import crypto from "node:crypto";
11
11
  import fs$1 from "node:fs/promises";
12
12
  import { fileURLToPath } from "node:url";
13
+ import { gunzip, gzip } from "node:zlib";
13
14
  import { MAX_DATE_TIMESTAMP_MS, timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
14
- import { root, safeEqualSecret, writeExternalFileWithinRoot } from "openclaw/plugin-sdk/security-runtime";
15
+ import { safeEqualSecret, writeExternalFileWithinRoot } from "openclaw/plugin-sdk/security-runtime";
15
16
  import { optionalFiniteNumberSchema, stringEnum } from "openclaw/plugin-sdk/channel-actions";
16
17
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
17
18
  import { readFiniteNumberParam } from "openclaw/plugin-sdk/param-readers";
@@ -472,19 +473,20 @@ function createDiffsHttpHandler(params) {
472
473
  respondText(res, 404, "Diff not found");
473
474
  return true;
474
475
  }
475
- if (!await params.store.getArtifact(id, token)) {
476
- recordRemoteFailure(viewerFailureLimiter, access);
477
- respondText(res, 404, "Diff not found or expired");
478
- return true;
479
- }
480
476
  try {
481
- const html = await params.store.readHtml(id);
477
+ const viewer = await params.store.readAuthorizedViewer(id, token);
478
+ if (!viewer) {
479
+ recordRemoteFailure(viewerFailureLimiter, access);
480
+ respondText(res, 404, "Diff not found or expired");
481
+ return true;
482
+ }
482
483
  resetRemoteFailures(viewerFailureLimiter, access);
483
484
  res.statusCode = 200;
484
485
  setSharedHeaders(res, "text/html; charset=utf-8");
485
486
  res.setHeader("content-security-policy", VIEWER_CONTENT_SECURITY_POLICY);
487
+ res.setHeader("content-length", String(viewer.html.byteLength));
486
488
  if (req.method === "HEAD") res.end();
487
- else res.end(html);
489
+ else res.end(Buffer.from(viewer.html));
488
490
  return true;
489
491
  } catch (error) {
490
492
  recordRemoteFailure(viewerFailureLimiter, access);
@@ -634,134 +636,167 @@ const DEFAULT_TTL_MS = 1800 * 1e3;
634
636
  const MAX_TTL_MS = 360 * 60 * 1e3;
635
637
  const SWEEP_FALLBACK_AGE_MS = 1440 * 60 * 1e3;
636
638
  const DEFAULT_CLEANUP_INTERVAL_MS = 300 * 1e3;
639
+ const MAX_DECODED_HTML_BYTES = 64 * 1024 * 1024;
640
+ const ARTIFACT_ID_ATTEMPTS = 8;
637
641
  const VIEWER_PREFIX = "/plugins/diffs/view";
642
+ const EMPTY_BLOB = /* @__PURE__ */ new Uint8Array();
643
+ function isBlobLimitError(error) {
644
+ return typeof error === "object" && error !== null && "code" in error && error.code === "PLUGIN_BLOB_LIMIT_EXCEEDED";
645
+ }
638
646
  var DiffArtifactStore = class {
639
647
  constructor(params) {
648
+ this.renderingFileIds = /* @__PURE__ */ new Set();
640
649
  this.cleanupInFlight = null;
641
650
  this.nextCleanupAt = 0;
642
651
  this.rootDir = path.resolve(params.rootDir);
652
+ this.blobStore = params.blobStore;
643
653
  this.logger = params.logger;
644
654
  this.cleanupIntervalMs = params.cleanupIntervalMs === void 0 ? DEFAULT_CLEANUP_INTERVAL_MS : Math.max(0, Math.floor(params.cleanupIntervalMs));
645
655
  }
646
656
  async createArtifact(params) {
647
- await this.ensureRoot();
648
- const id = crypto.randomBytes(10).toString("hex");
657
+ const html = Buffer.from(params.html, "utf8");
658
+ if (html.byteLength > MAX_DECODED_HTML_BYTES) throw new Error(`Diff viewer HTML exceeds ${MAX_DECODED_HTML_BYTES} bytes.`);
659
+ const compressedHtml = await gzipAsync(html);
649
660
  const token = crypto.randomBytes(24).toString("hex");
650
- const artifactDir = this.artifactDir(id);
651
- const htmlPath = path.join(artifactDir, "viewer.html");
652
661
  const ttlMs = normalizeTtlMs$1(params.ttlMs);
653
- const createdAt = /* @__PURE__ */ new Date();
654
- const createdAtIso = createdAt.toISOString();
655
- const expiresAt = resolveExpiresAtIso(createdAt.getTime(), ttlMs);
656
- const meta = {
657
- id,
658
- token,
662
+ const metadata = {
663
+ version: 1,
664
+ kind: "viewer",
665
+ encoding: "gzip",
666
+ tokenHash: hashToken(token),
659
667
  title: params.title,
660
668
  inputKind: params.inputKind,
661
669
  fileCount: params.fileCount,
662
- createdAt: createdAtIso,
663
- expiresAt,
664
- viewerPath: `${VIEWER_PREFIX}/${id}/${token}`,
665
- htmlPath,
670
+ decodedBytes: html.byteLength,
666
671
  ...params.context ? { context: params.context } : {}
667
672
  };
668
- const root = await this.artifactRoot();
669
- await root.mkdir(id);
670
- await root.write(path.posix.join(id, "viewer.html"), params.html);
671
- await this.writeMeta(meta);
673
+ const entry = await this.registerUnique(compressedHtml, metadata, ttlMs);
672
674
  this.scheduleCleanup();
673
- return meta;
675
+ return viewerEntryToMeta(entry, token);
674
676
  }
675
- async getArtifact(id, token) {
676
- const meta = await this.readMeta(id);
677
- if (!meta) return null;
678
- if (!safeEqualSecret(token, meta.token)) return null;
679
- if (isExpired(meta)) {
680
- await this.deleteArtifact(id);
677
+ async readAuthorizedViewer(id, token) {
678
+ if (!DIFF_ARTIFACT_ID_PATTERN.test(id) || !DIFF_ARTIFACT_TOKEN_PATTERN.test(token)) return null;
679
+ const entry = await this.blobStore.lookup(id);
680
+ if (!entry) {
681
+ const expired = await this.blobStore.deleteExpiredKey(id);
682
+ if (expired) await this.deleteExpiredFile(expired);
681
683
  return null;
682
684
  }
683
- return meta;
684
- }
685
- async readHtml(id) {
686
- const meta = await this.readMeta(id);
687
- if (!meta) throw new Error(`Diff artifact not found: ${id}`);
688
- const htmlPath = this.normalizeStoredPath(meta.htmlPath, "htmlPath");
689
- return await (await this.artifactRoot()).readText(this.relativeStoredPath(htmlPath));
690
- }
691
- async updateFilePath(id, filePath) {
692
- const meta = await this.readMeta(id);
693
- if (!meta) throw new Error(`Diff artifact not found: ${id}`);
694
- const normalizedFilePath = this.normalizeStoredPath(filePath, "filePath");
695
- const next = {
696
- ...meta,
697
- filePath: normalizedFilePath,
698
- imagePath: normalizedFilePath
685
+ if (!isViewerMetadata(entry.metadata)) return null;
686
+ if (!safeEqualSecret(hashToken(token), entry.metadata.tokenHash)) return null;
687
+ const html = await gunzipAsync(entry.bytes, MAX_DECODED_HTML_BYTES);
688
+ if (html.byteLength !== entry.metadata.decodedBytes) throw new Error(`Diff artifact ${id} decoded size does not match its metadata.`);
689
+ return {
690
+ artifact: viewerEntryToMeta(entry, token),
691
+ html
699
692
  };
700
- await this.writeMeta(next);
701
- return next;
702
- }
703
- async updateImagePath(id, imagePath) {
704
- return this.updateFilePath(id, imagePath);
705
- }
706
- allocateFilePath(id, format = "png") {
707
- return path.join(this.artifactDir(id), `preview.${format}`);
708
693
  }
709
694
  async createStandaloneFileArtifact(params = {}) {
710
- await this.ensureRoot();
711
- const id = crypto.randomBytes(10).toString("hex");
712
- const artifactDir = this.artifactDir(id);
713
695
  const format = params.format ?? "png";
714
- const filePath = path.join(artifactDir, `preview.${format}`);
715
696
  const ttlMs = normalizeTtlMs$1(params.ttlMs);
716
- const createdAt = /* @__PURE__ */ new Date();
717
- const meta = {
718
- kind: "standalone_file",
719
- id,
720
- createdAt: createdAt.toISOString(),
721
- expiresAt: resolveExpiresAtIso(createdAt.getTime(), ttlMs),
722
- filePath: this.normalizeStoredPath(filePath, "filePath"),
697
+ const metadata = {
698
+ version: 1,
699
+ kind: "rendered_file",
700
+ format,
723
701
  ...params.context ? { context: params.context } : {}
724
702
  };
725
- await (await this.artifactRoot()).mkdir(id);
726
- await this.writeStandaloneMeta(meta);
727
- this.scheduleCleanup();
728
- return {
729
- id,
730
- filePath: meta.filePath,
731
- expiresAt: meta.expiresAt,
732
- ...meta.context ? { context: meta.context } : {}
733
- };
703
+ for (let attempt = 0; attempt < ARTIFACT_ID_ATTEMPTS; attempt += 1) {
704
+ const id = crypto.randomBytes(10).toString("hex");
705
+ if (!await this.registerIfAbsentWithCleanup(id, EMPTY_BLOB, metadata, ttlMs)) continue;
706
+ const artifactDir = this.artifactDir(id);
707
+ try {
708
+ await fs$1.mkdir(this.rootDir, { recursive: true });
709
+ await fs$1.mkdir(artifactDir);
710
+ const entry = await this.blobStore.lookup(id);
711
+ if (!entry || !isRenderedFileMetadata(entry.metadata)) throw new Error(`Diff file artifact expired before materialization: ${id}`);
712
+ this.renderingFileIds.add(id);
713
+ this.scheduleCleanup();
714
+ return {
715
+ id,
716
+ filePath: path.join(artifactDir, `preview.${format}`),
717
+ expiresAt: resolveEntryExpiresAt(entry),
718
+ ...params.context ? { context: params.context } : {}
719
+ };
720
+ } catch (error) {
721
+ await this.blobStore.delete(id).catch(() => false);
722
+ await fs$1.rm(artifactDir, {
723
+ recursive: true,
724
+ force: true
725
+ }).catch(() => {});
726
+ if (isFileExists(error)) continue;
727
+ throw error;
728
+ }
729
+ }
730
+ throw new Error("Failed to allocate a unique diff file artifact id.");
734
731
  }
735
- allocateImagePath(id, format = "png") {
736
- return this.allocateFilePath(id, format);
732
+ async completeFileArtifact(id) {
733
+ try {
734
+ const entry = await this.blobStore.lookup(id);
735
+ if (!entry || !isRenderedFileMetadata(entry.metadata)) {
736
+ await fs$1.rm(this.artifactDir(id), {
737
+ recursive: true,
738
+ force: true
739
+ }).catch(() => {});
740
+ throw new Error(`Diff file artifact expired during rendering: ${id}`);
741
+ }
742
+ } finally {
743
+ this.renderingFileIds.delete(id);
744
+ }
745
+ }
746
+ async deleteFileArtifact(id) {
747
+ this.renderingFileIds.delete(id);
748
+ await this.blobStore.delete(id).catch(() => false);
749
+ await fs$1.rm(this.artifactDir(id), {
750
+ recursive: true,
751
+ force: true
752
+ }).catch(() => {});
737
753
  }
738
754
  scheduleCleanup() {
739
755
  this.maybeCleanupExpired();
740
756
  }
741
757
  async cleanupExpired() {
742
- const entries = await (await this.artifactRoot()).list("", { withFileTypes: true }).catch(() => []);
758
+ const expired = await this.blobStore.deleteExpired();
759
+ await Promise.all(expired.map(async (entry) => await this.deleteExpiredFile(entry)));
760
+ const entries = await fs$1.readdir(this.rootDir, { withFileTypes: true }).catch((error) => {
761
+ if (isFileNotFound(error)) return [];
762
+ throw error;
763
+ });
743
764
  const now = Date.now();
744
- await Promise.all(entries.filter((entry) => entry.isDirectory).map(async (entry) => {
745
- const id = entry.name;
746
- const meta = await this.readMeta(id);
747
- if (meta) {
748
- if (isExpired(meta)) await this.deleteArtifact(id);
749
- return;
750
- }
751
- const standaloneMeta = await this.readStandaloneMeta(id);
752
- if (standaloneMeta) {
753
- if (isExpired(standaloneMeta)) await this.deleteArtifact(id);
754
- return;
755
- }
756
- if (now - entry.mtimeMs > SWEEP_FALLBACK_AGE_MS) await this.deleteArtifact(id);
765
+ await Promise.all(entries.filter((entry) => entry.isDirectory() && DIFF_ARTIFACT_ID_PATTERN.test(entry.name)).map(async (entry) => {
766
+ if (this.renderingFileIds.has(entry.name) || await this.blobStore.lookup(entry.name)) return;
767
+ const artifactDir = this.artifactDir(entry.name);
768
+ const stats = await fs$1.stat(artifactDir).catch(() => null);
769
+ if (stats && now - stats.mtimeMs > SWEEP_FALLBACK_AGE_MS) await fs$1.rm(artifactDir, {
770
+ recursive: true,
771
+ force: true
772
+ }).catch(() => {});
757
773
  }));
758
774
  }
759
- async ensureRoot() {
760
- await fs$1.mkdir(this.rootDir, { recursive: true });
775
+ async registerUnique(bytes, metadata, ttlMs) {
776
+ for (let attempt = 0; attempt < ARTIFACT_ID_ATTEMPTS; attempt += 1) {
777
+ const id = crypto.randomBytes(10).toString("hex");
778
+ if (!await this.registerIfAbsentWithCleanup(id, bytes, metadata, ttlMs)) continue;
779
+ const entry = await this.blobStore.lookup(id);
780
+ if (entry) return entry;
781
+ }
782
+ throw new Error("Failed to allocate a unique diff artifact id.");
761
783
  }
762
- async artifactRoot() {
763
- await this.ensureRoot();
764
- return await root(this.rootDir);
784
+ async registerIfAbsentWithCleanup(id, bytes, metadata, ttlMs) {
785
+ try {
786
+ return await this.blobStore.registerIfAbsent(id, bytes, metadata, { ttlMs });
787
+ } catch (error) {
788
+ if (!isBlobLimitError(error)) throw error;
789
+ await this.cleanupExpired();
790
+ return await this.blobStore.registerIfAbsent(id, bytes, metadata, { ttlMs });
791
+ }
792
+ }
793
+ async deleteExpiredFile(entry) {
794
+ if (!isRenderedFileMetadata(entry.metadata) || this.renderingFileIds.has(entry.key)) return;
795
+ if (await this.blobStore.lookup(entry.key)) return;
796
+ await fs$1.rm(this.artifactDir(entry.key), {
797
+ recursive: true,
798
+ force: true
799
+ }).catch(() => {});
765
800
  }
766
801
  maybeCleanupExpired() {
767
802
  const now = Date.now();
@@ -776,104 +811,85 @@ var DiffArtifactStore = class {
776
811
  this.cleanupInFlight = cleanupPromise;
777
812
  }
778
813
  artifactDir(id) {
779
- return this.resolveWithinRoot(id);
780
- }
781
- async writeMeta(meta) {
782
- await this.writeJsonMeta(meta.id, "meta.json", meta);
783
- }
784
- async readMeta(id) {
785
- const parsed = await this.readJsonMeta(id, "meta.json", "diff artifact");
786
- if (!parsed) return null;
787
- return parsed;
788
- }
789
- async writeStandaloneMeta(meta) {
790
- await this.writeJsonMeta(meta.id, "file-meta.json", meta);
791
- }
792
- async readStandaloneMeta(id) {
793
- const parsed = await this.readJsonMeta(id, "file-meta.json", "standalone diff");
794
- if (!parsed) return null;
795
- try {
796
- const value = parsed;
797
- if (value.kind !== "standalone_file" || typeof value.id !== "string" || typeof value.createdAt !== "string" || typeof value.expiresAt !== "string" || typeof value.filePath !== "string") return null;
798
- return {
799
- kind: value.kind,
800
- id: value.id,
801
- createdAt: value.createdAt,
802
- expiresAt: value.expiresAt,
803
- filePath: this.normalizeStoredPath(value.filePath, "filePath"),
804
- ...value.context ? { context: normalizeArtifactContext(value.context) } : {}
805
- };
806
- } catch (error) {
807
- this.logger?.warn(`Failed to normalize standalone diff metadata for ${id}: ${String(error)}`);
808
- return null;
809
- }
810
- }
811
- async writeJsonMeta(id, fileName, data) {
812
- await (await this.artifactRoot()).writeJson(path.posix.join(id, fileName), data, { space: 2 });
813
- }
814
- async readJsonMeta(id, fileName, context) {
815
- try {
816
- const raw = await (await this.artifactRoot()).readText(path.posix.join(id, fileName));
817
- return JSON.parse(raw);
818
- } catch (error) {
819
- if (isFileNotFound(error)) return null;
820
- this.logger?.warn(`Failed to read ${context} metadata for ${id}: ${String(error)}`);
821
- return null;
822
- }
823
- }
824
- async deleteArtifact(id) {
825
- await fs$1.rm(this.artifactDir(id), {
826
- recursive: true,
827
- force: true
828
- }).catch(() => {});
829
- }
830
- resolveWithinRoot(...parts) {
831
- const candidate = path.resolve(this.rootDir, ...parts);
832
- this.assertWithinRoot(candidate);
833
- return candidate;
834
- }
835
- normalizeStoredPath(rawPath, label) {
836
- const candidate = path.isAbsolute(rawPath) ? path.resolve(rawPath) : path.resolve(this.rootDir, rawPath);
837
- this.assertWithinRoot(candidate, label);
838
- return candidate;
839
- }
840
- relativeStoredPath(storedPath) {
841
- return path.relative(this.rootDir, this.normalizeStoredPath(storedPath, "path")).split(path.sep).join(path.posix.sep);
842
- }
843
- assertWithinRoot(candidate, label = "path") {
844
- const relative = path.relative(this.rootDir, candidate);
845
- if (relative === "" || !relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative)) return;
846
- throw new Error(`Diff artifact ${label} escapes store root: ${candidate}`);
814
+ if (!DIFF_ARTIFACT_ID_PATTERN.test(id)) throw new Error(`Invalid diff artifact id: ${id}`);
815
+ return path.join(this.rootDir, id);
847
816
  }
848
817
  };
818
+ function viewerEntryToMeta(entry, token) {
819
+ if (!isViewerMetadata(entry.metadata)) throw new Error(`Diff artifact ${entry.key} is not a viewer.`);
820
+ return {
821
+ id: entry.key,
822
+ token,
823
+ createdAt: timestampMsToIsoString(entry.createdAt) ?? "1970-01-01T00:00:00.000Z",
824
+ expiresAt: resolveEntryExpiresAt(entry),
825
+ title: entry.metadata.title,
826
+ inputKind: entry.metadata.inputKind,
827
+ fileCount: entry.metadata.fileCount,
828
+ viewerPath: `${VIEWER_PREFIX}/${entry.key}/${token}`,
829
+ ...entry.metadata.context ? { context: entry.metadata.context } : {}
830
+ };
831
+ }
832
+ function resolveEntryExpiresAt(entry) {
833
+ return timestampMsToIsoString(entry.expiresAt ?? MAX_DATE_TIMESTAMP_MS) ?? timestampMsToIsoString(MAX_DATE_TIMESTAMP_MS) ?? "1970-01-01T00:00:00.000Z";
834
+ }
835
+ function hashToken(token) {
836
+ return crypto.createHash("sha256").update(token).digest("hex");
837
+ }
849
838
  function normalizeTtlMs$1(value) {
850
- if (!Number.isFinite(value) || value === void 0) return DEFAULT_TTL_MS;
851
- const rounded = Math.floor(value);
852
- if (rounded <= 0) return DEFAULT_TTL_MS;
853
- return Math.min(rounded, MAX_TTL_MS);
839
+ const rounded = value === void 0 || !Number.isFinite(value) ? 0 : Math.floor(value);
840
+ const requestedTtlMs = rounded > 0 ? rounded : DEFAULT_TTL_MS;
841
+ const remainingDateRangeMs = Math.floor(MAX_DATE_TIMESTAMP_MS - Date.now());
842
+ return Math.min(requestedTtlMs, MAX_TTL_MS, Math.max(1, remainingDateRangeMs));
843
+ }
844
+ function isViewerMetadata(value) {
845
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
846
+ const metadata = value;
847
+ return metadata.version === 1 && metadata.kind === "viewer" && metadata.encoding === "gzip" && typeof metadata.tokenHash === "string" && /^[0-9a-f]{64}$/u.test(metadata.tokenHash) && typeof metadata.title === "string" && (metadata.inputKind === "before_after" || metadata.inputKind === "patch") && Number.isSafeInteger(metadata.fileCount) && typeof metadata.fileCount === "number" && metadata.fileCount >= 0 && Number.isSafeInteger(metadata.decodedBytes) && typeof metadata.decodedBytes === "number" && metadata.decodedBytes >= 0 && metadata.decodedBytes <= MAX_DECODED_HTML_BYTES && isArtifactContext(metadata.context);
848
+ }
849
+ function isRenderedFileMetadata(value) {
850
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
851
+ const metadata = value;
852
+ return metadata.version === 1 && metadata.kind === "rendered_file" && (metadata.format === "png" || metadata.format === "pdf") && isArtifactContext(metadata.context);
853
+ }
854
+ function isArtifactContext(value) {
855
+ if (value === void 0) return true;
856
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
857
+ const context = value;
858
+ const allowed = /* @__PURE__ */ new Set([
859
+ "agentId",
860
+ "sessionId",
861
+ "messageChannel",
862
+ "agentAccountId"
863
+ ]);
864
+ return Object.entries(context).every(([key, entry]) => allowed.has(key) && (entry === void 0 || typeof entry === "string"));
865
+ }
866
+ async function gzipAsync(input) {
867
+ return await new Promise((resolve, reject) => {
868
+ gzip(input, (error, result) => {
869
+ if (error) {
870
+ reject(error);
871
+ return;
872
+ }
873
+ resolve(result);
874
+ });
875
+ });
854
876
  }
855
- function resolveExpiresAtIso(createdAtMs, ttlMs) {
856
- return timestampMsToIsoString(createdAtMs + ttlMs) ?? timestampMsToIsoString(MAX_DATE_TIMESTAMP_MS) ?? "1970-01-01T00:00:00.000Z";
877
+ async function gunzipAsync(input, maxOutputLength) {
878
+ return await new Promise((resolve, reject) => {
879
+ gunzip(input, { maxOutputLength }, (error, result) => {
880
+ if (error) {
881
+ reject(error);
882
+ return;
883
+ }
884
+ resolve(result);
885
+ });
886
+ });
857
887
  }
858
- function isExpired(meta) {
859
- const expiresAt = Date.parse(meta.expiresAt);
860
- if (!Number.isFinite(expiresAt)) return true;
861
- return Date.now() >= expiresAt;
888
+ function isFileExists(error) {
889
+ return error instanceof Error && "code" in error && error.code === "EEXIST";
862
890
  }
863
891
  function isFileNotFound(error) {
864
- const code = error instanceof Error && "code" in error ? error.code : void 0;
865
- return code === "ENOENT" || code === "not-found";
866
- }
867
- function normalizeArtifactContext(value) {
868
- if (!value || typeof value !== "object" || Array.isArray(value)) return;
869
- const raw = value;
870
- const context = {
871
- agentId: normalizeOptionalString(raw.agentId),
872
- sessionId: normalizeOptionalString(raw.sessionId),
873
- messageChannel: normalizeOptionalString(raw.messageChannel),
874
- agentAccountId: normalizeOptionalString(raw.agentAccountId)
875
- };
876
- return Object.values(context).some((entry) => entry !== void 0) ? context : void 0;
892
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
877
893
  }
878
894
  //#endregion
879
895
  //#region extensions/diffs/src/browser.ts
@@ -1483,11 +1499,6 @@ const bundledLanguagesBase = Object.fromEntries(bundledLanguagesInfo.map((langua
1483
1499
  function getBundledLanguageAliases(language) {
1484
1500
  return "aliases" in language ? language.aliases : [];
1485
1501
  }
1486
- const bundledLanguagesAlias = Object.fromEntries(bundledLanguagesInfo.flatMap((language) => getBundledLanguageAliases(language).map((alias) => [alias, language.import])));
1487
- ({
1488
- ...bundledLanguagesBase,
1489
- ...bundledLanguagesAlias
1490
- });
1491
1502
  //#endregion
1492
1503
  //#region extensions/diffs/src/language-hints.ts
1493
1504
  const BASE_DIFF_VIEWER_LANGUAGE_HINTS = [
@@ -2309,12 +2320,12 @@ function createDiffsTool(params) {
2309
2320
  const artifactFile = await renderDiffArtifactFile({
2310
2321
  screenshotter,
2311
2322
  store: params.store,
2312
- artifactId: artifact.id,
2313
2323
  html: requireRenderedHtml(rendered.imageHtml, "image"),
2314
2324
  theme,
2315
- image
2325
+ image,
2326
+ ttlMs,
2327
+ context: artifactContext
2316
2328
  });
2317
- await params.store.updateFilePath(artifact.id, artifactFile.path);
2318
2329
  return {
2319
2330
  content: [{
2320
2331
  type: "text",
@@ -2386,34 +2397,44 @@ function buildFileArtifactMessage(params) {
2386
2397
  return lines.join("\n");
2387
2398
  }
2388
2399
  async function renderDiffArtifactFile(params) {
2389
- const standaloneArtifact = params.artifactId ? void 0 : await params.store.createStandaloneFileArtifact({
2400
+ const fileArtifact = await params.store.createStandaloneFileArtifact({
2390
2401
  format: params.image.format,
2391
2402
  ttlMs: params.ttlMs,
2392
2403
  context: params.context
2393
2404
  });
2394
- const outputPath = params.artifactId ? params.store.allocateFilePath(params.artifactId, params.image.format) : standaloneArtifact.filePath;
2395
- await params.screenshotter.screenshotHtml({
2396
- html: params.html,
2397
- outputPath,
2398
- theme: params.theme,
2399
- image: params.image
2400
- });
2401
- return {
2402
- path: outputPath,
2403
- bytes: (await fs$1.stat(outputPath)).size,
2404
- ...standaloneArtifact?.id ? { artifactId: standaloneArtifact.id } : {},
2405
- ...standaloneArtifact?.expiresAt ? { expiresAt: standaloneArtifact.expiresAt } : {}
2406
- };
2405
+ try {
2406
+ await params.screenshotter.screenshotHtml({
2407
+ html: params.html,
2408
+ outputPath: fileArtifact.filePath,
2409
+ theme: params.theme,
2410
+ image: params.image
2411
+ });
2412
+ const stats = await fs$1.stat(fileArtifact.filePath);
2413
+ await params.store.completeFileArtifact(fileArtifact.id);
2414
+ return {
2415
+ path: fileArtifact.filePath,
2416
+ bytes: stats.size,
2417
+ artifactId: fileArtifact.id,
2418
+ expiresAt: fileArtifact.expiresAt
2419
+ };
2420
+ } catch (error) {
2421
+ await params.store.deleteFileArtifact(fileArtifact.id);
2422
+ throw error;
2423
+ }
2407
2424
  }
2408
2425
  function buildArtifactContext(context) {
2409
2426
  if (!context) return;
2427
+ const agentId = normalizeOptionalString(context.agentId);
2428
+ const sessionId = normalizeOptionalString(context.sessionId);
2429
+ const messageChannel = normalizeOptionalString(context.messageChannel);
2430
+ const agentAccountId = normalizeOptionalString(context.agentAccountId);
2410
2431
  const artifactContext = {
2411
- agentId: normalizeOptionalString(context.agentId),
2412
- sessionId: normalizeOptionalString(context.sessionId),
2413
- messageChannel: normalizeOptionalString(context.messageChannel),
2414
- agentAccountId: normalizeOptionalString(context.agentAccountId)
2432
+ ...agentId ? { agentId } : {},
2433
+ ...sessionId ? { sessionId } : {},
2434
+ ...messageChannel ? { messageChannel } : {},
2435
+ ...agentAccountId ? { agentAccountId } : {}
2415
2436
  };
2416
- return Object.values(artifactContext).some((value) => value !== void 0) ? artifactContext : void 0;
2437
+ return Object.keys(artifactContext).length > 0 ? artifactContext : void 0;
2417
2438
  }
2418
2439
  function normalizeDiffInput(params) {
2419
2440
  const patch = params.patch?.trim();
@@ -2483,9 +2504,20 @@ var PluginToolInputError = class extends Error {
2483
2504
  //#endregion
2484
2505
  //#region extensions/diffs/src/plugin.ts
2485
2506
  const DIFFS_LANGUAGE_PACK_PLUGIN_ID = "diffs-language-pack";
2507
+ const DIFF_ARTIFACT_NAMESPACE = "diff-artifacts";
2508
+ const DIFF_ARTIFACT_MAX_ENTRIES = 2048;
2509
+ const DIFF_ARTIFACT_MAX_BYTES_PER_ENTRY = 32 * 1024 * 1024;
2510
+ const DIFF_ARTIFACT_MAX_BYTES_PER_NAMESPACE = 256 * 1024 * 1024;
2486
2511
  function registerDiffsPlugin(api) {
2487
2512
  const store = new DiffArtifactStore({
2488
2513
  rootDir: path.join(resolvePreferredOpenClawTmpDir(), "openclaw-diffs"),
2514
+ blobStore: api.runtime.state.openBlobStore({
2515
+ namespace: DIFF_ARTIFACT_NAMESPACE,
2516
+ maxEntries: DIFF_ARTIFACT_MAX_ENTRIES,
2517
+ maxBytesPerEntry: DIFF_ARTIFACT_MAX_BYTES_PER_ENTRY,
2518
+ maxBytesPerNamespace: DIFF_ARTIFACT_MAX_BYTES_PER_NAMESPACE,
2519
+ overflowPolicy: "reject-new"
2520
+ }),
2489
2521
  logger: api.logger
2490
2522
  });
2491
2523
  const resolveCurrentPluginConfig = () => resolveLivePluginConfigObject(api.runtime.config?.current ? () => api.runtime.config.current() : void 0, "diffs", api.pluginConfig) ?? {};
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@openclaw/diffs",
3
- "version": "2026.7.2-beta.1",
3
+ "version": "2026.7.2-beta.3",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@openclaw/diffs",
9
- "version": "2026.7.2-beta.1",
9
+ "version": "2026.7.2-beta.3",
10
10
  "dependencies": {
11
11
  "@pierre/diffs": "1.2.12",
12
12
  "@shikijs/langs": "4.3.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/diffs",
3
- "version": "2026.7.2-beta.1",
3
+ "version": "2026.7.2-beta.3",
4
4
  "description": "OpenClaw read-only diff viewer plugin and file renderer for agents.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -25,13 +25,13 @@
25
25
  "minHostVersion": ">=2026.4.30"
26
26
  },
27
27
  "compat": {
28
- "pluginApi": ">=2026.7.2-beta.1"
28
+ "pluginApi": ">=2026.7.2-beta.3"
29
29
  },
30
30
  "assetScripts": {
31
31
  "build": "node ../../scripts/build-diffs-viewer-runtime.mjs curated"
32
32
  },
33
33
  "build": {
34
- "openclawVersion": "2026.7.2-beta.1",
34
+ "openclawVersion": "2026.7.2-beta.3",
35
35
  "staticAssets": [
36
36
  {
37
37
  "source": "./assets/viewer-runtime.js",
@@ -55,7 +55,7 @@
55
55
  "skills/**"
56
56
  ],
57
57
  "peerDependencies": {
58
- "openclaw": ">=2026.7.2-beta.1"
58
+ "openclaw": ">=2026.7.2-beta.3"
59
59
  },
60
60
  "peerDependenciesMeta": {
61
61
  "openclaw": {