clawgram 2.29.1 → 2.29.2

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.
@@ -132,6 +132,7 @@ async function handleReadAction(ctx) {
132
132
  ? node_path_1.default.join((0, state_dir_1.resolveStateDir)(), "tmp")
133
133
  : node_path_1.default.join(node_os_1.default.tmpdir(), `clawgram-${typeof process.getuid === "function" ? process.getuid() : "user"}`);
134
134
  await (0, media_1.ensurePrivateDir)(mediaRoot);
135
+ await (0, media_1.sweepOrphanMediaDirs)(mediaRoot, Date.now());
135
136
  const sharedFetchDir = node_path_1.default.join(mediaRoot, "clawgram-fetched");
136
137
  const described = (0, media_1.describeMedia)(found.message?.media);
137
138
  // A PDF is not read here: the agent is handed the path for its PDF tool,
@@ -81,6 +81,7 @@ async function readInboundAttachment(params) {
81
81
  if (!media || !message) {
82
82
  return undefined;
83
83
  }
84
+ await (0, media_1.sweepOrphanMediaDirs)(node_os_1.default.tmpdir(), Date.now());
84
85
  let downloaded;
85
86
  try {
86
87
  downloaded = await (0, media_1.downloadInboundMediaToTempFile)({
package/dist/media.js CHANGED
@@ -15,6 +15,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
15
15
  return (mod && mod.__esModule) ? mod : { "default": mod };
16
16
  };
17
17
  Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.ORPHAN_MEDIA_DIR_TTL_MS = void 0;
18
19
  exports.describeMedia = describeMedia;
19
20
  exports.inboundMediaUnderstanding = inboundMediaUnderstanding;
20
21
  exports.fetchMediaUnderstanding = fetchMediaUnderstanding;
@@ -23,6 +24,8 @@ exports.describePrivateDirProblem = describePrivateDirProblem;
23
24
  exports.ensurePrivateDir = ensurePrivateDir;
24
25
  exports.downloadMessageMediaToFile = downloadMessageMediaToFile;
25
26
  exports.pruneFetchedMedia = pruneFetchedMedia;
27
+ exports.pruneOrphanMediaDirs = pruneOrphanMediaDirs;
28
+ exports.sweepOrphanMediaDirs = sweepOrphanMediaDirs;
26
29
  exports.isLocalMediaPath = isLocalMediaPath;
27
30
  exports.assertLocalMediaWithinRoots = assertLocalMediaWithinRoots;
28
31
  exports.loadOutboundMedia = loadOutboundMedia;
@@ -167,17 +170,32 @@ async function downloadInboundMediaToTempFile(params) {
167
170
  return undefined;
168
171
  }
169
172
  const dir = await mkdtemp(join(params.tmpDir, "clawgram-media-"));
170
- const downloaded = await downloadMessageMediaToFile({
171
- client: params.client,
172
- message: params.message,
173
- maxBytes: params.maxBytes,
174
- dir,
175
- fileNameFor: ({ extension }) => `attachment.${extension}`,
176
- });
173
+ // The caller removes only a path it was handed. Every other way out — an
174
+ // empty download, a document, a throw — removes the directory here, or it
175
+ // stays in the temp directory for good (audit r3 V1-10, the inbound path).
176
+ const discard = async () => {
177
+ const { rm } = await import("node:fs/promises");
178
+ await rm(dir, { recursive: true, force: true }).catch(() => undefined);
179
+ };
180
+ let downloaded;
181
+ try {
182
+ downloaded = await downloadMessageMediaToFile({
183
+ client: params.client,
184
+ message: params.message,
185
+ maxBytes: params.maxBytes,
186
+ dir,
187
+ fileNameFor: ({ extension }) => `attachment.${extension}`,
188
+ });
189
+ }
190
+ catch (err) {
191
+ await discard();
192
+ throw err;
193
+ }
177
194
  // This path asks only for `inboundMediaUnderstanding`, so a document cannot
178
195
  // get here. Keep the runtime guard as the public downloader also serves the
179
196
  // explicit `fetch-media` action, which is allowed to request DOCX files.
180
197
  if (!downloaded || downloaded.understanding === "document" || downloaded.understanding === "pdf") {
198
+ await discard();
181
199
  return undefined;
182
200
  }
183
201
  return {
@@ -335,6 +353,57 @@ async function pruneFetchedMedia(dir, maxAgeMs, now) {
335
353
  }
336
354
  return removed;
337
355
  }
356
+ /**
357
+ * A private `clawgram-media-*` directory lives for one call: the inbound
358
+ * reader and `fetch-media mode=read` remove it when they are done. One that is
359
+ * an hour old was left by a crash, or by a version before 2.29.1, which kept a
360
+ * directory on every error branch and every PDF read (audit r3 C2-02, V1-10).
361
+ */
362
+ exports.ORPHAN_MEDIA_DIR_TTL_MS = 60 * 60 * 1000;
363
+ const ORPHAN_SWEEP_EVERY_MS = 10 * 60 * 1000;
364
+ const lastOrphanSweep = new Map();
365
+ /** Removes `clawgram-media-*` directories in `root` older than `maxAgeMs`. */
366
+ async function pruneOrphanMediaDirs(root, maxAgeMs, now) {
367
+ const { readdir, stat, rm } = await import("node:fs/promises");
368
+ const { join } = await import("node:path");
369
+ let entries;
370
+ try {
371
+ entries = await readdir(root);
372
+ }
373
+ catch {
374
+ return 0;
375
+ }
376
+ let removed = 0;
377
+ for (const entry of entries) {
378
+ if (!entry.startsWith("clawgram-media-"))
379
+ continue;
380
+ const path = join(root, entry);
381
+ try {
382
+ const info = await stat(path);
383
+ // In a shared /tmp another account's directory fails `rm` on the
384
+ // sticky bit and is skipped; only our own leftovers go.
385
+ if (info.isDirectory() && now - info.mtimeMs > maxAgeMs) {
386
+ await rm(path, { recursive: true, force: true });
387
+ removed += 1;
388
+ }
389
+ }
390
+ catch {
391
+ // Gone already, or not ours.
392
+ }
393
+ }
394
+ return removed;
395
+ }
396
+ /**
397
+ * `pruneOrphanMediaDirs` at most once per ten minutes per root: it reads the
398
+ * whole temp directory, and it runs on the path of every inbound attachment.
399
+ */
400
+ async function sweepOrphanMediaDirs(root, now) {
401
+ const last = lastOrphanSweep.get(root);
402
+ if (last !== undefined && now - last < ORPHAN_SWEEP_EVERY_MS)
403
+ return 0;
404
+ lastOrphanSweep.set(root, now);
405
+ return pruneOrphanMediaDirs(root, exports.ORPHAN_MEDIA_DIR_TTL_MS, now).catch(() => 0);
406
+ }
338
407
  function extensionFor(media, understanding) {
339
408
  if (understanding === "description") {
340
409
  if (media.mimeType === "image/png")
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "clawgram",
3
- "version": "2.29.1",
3
+ "version": "2.29.2",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "clawgram",
9
- "version": "2.29.1",
9
+ "version": "2.29.2",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "json5": "2.2.3",
@@ -2,7 +2,7 @@
2
2
  "id": "clawgram",
3
3
  "name": "Clawgram",
4
4
  "description": "Clawgram — personal Telegram (MTProto userbot) channel for OpenClaw. Your AI assistant reads and responds as you.",
5
- "version": "2.29.1",
5
+ "version": "2.29.2",
6
6
  "configSchema": {
7
7
  "type": "object",
8
8
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawgram",
3
- "version": "2.29.1",
3
+ "version": "2.29.2",
4
4
  "description": "Clawgram — personal Telegram (MTProto userbot) channel for OpenClaw. Your AI assistant reads and responds as you.",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {