rol-websocket-channel 1.8.6 → 1.8.8

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.
@@ -4,11 +4,14 @@ import path from 'node:path';
4
4
  import { ensureDir, pathExists, readJsonFile } from '../lib/fs.js';
5
5
  import { ensureInside } from '../lib/paths.js';
6
6
  import { JsonRpcException, JSON_RPC_ERRORS } from '../jsonrpc.js';
7
+ import { findSessionRecord } from './sessions.js';
7
8
  const INLINE_CONTENT_LIMIT_BYTES = 10 * 1024 * 1024;
8
9
  const MIN_ARTIFACT_SIZE_BYTES = 1;
9
10
  const ARTIFACT_MANIFEST_FILE = 'artifacts.json';
10
11
  const MARKDOWN_SCAN_STATE_FILE = 'md-scan-state.json';
12
+ const ARTIFACT_ARCHIVE_STATE_FILE = 'artifact-archive-state.json';
11
13
  const PLUGIN_WORKSPACE_STATE_DIR = path.join('.openclaw', 'rol-websocket-channel');
14
+ const SESSION_ARTIFACTS_ROOT_RELATIVE = 'artifacts/sessions';
12
15
  const IGNORE_EXTENSIONS = new Set(['.tmp', '.part', '.crdownload']);
13
16
  const IGNORE_FILE_NAMES = new Set(['.ds_store', 'thumbs.db', ARTIFACT_MANIFEST_FILE]);
14
17
  const IGNORE_MARKDOWN_FILE_NAMES = new Set([
@@ -83,27 +86,46 @@ const MIME_BY_EXTENSION = {
83
86
  export const listArtifacts = async (params, context) => {
84
87
  const objectParams = expectOptionalObject(params);
85
88
  const refresh = objectParams.refresh !== false;
89
+ const normalizedFilters = normalizeArtifactFilters(objectParams);
90
+ const filters = {
91
+ ...normalizedFilters,
92
+ sessionId: await resolveCanonicalArtifactSessionId(context, normalizedFilters.sessionId)
93
+ };
94
+ const groupBy = normalizeArtifactGroupBy(objectParams.groupBy);
86
95
  const manifest = refresh
87
- ? await refreshArtifactManifest(context)
96
+ ? await refreshArtifactManifest(context, { sessionId: filters.sessionId })
88
97
  : await readOrRefreshArtifactManifest(context);
89
- return {
90
- scope: 'workspace',
91
- count: manifest.items.length,
98
+ const items = manifest.items.filter((item) => matchesArtifactFilters(item, filters));
99
+ const response = {
100
+ scope: filters.sessionId ? 'session' : 'workspace',
101
+ sessionId: filters.sessionId ?? null,
102
+ query: filters.query ?? null,
103
+ count: items.length,
92
104
  manifestPath: manifest.manifestPath,
93
105
  workspaceRoot: manifest.workspacePaths.workspaceRoot,
94
- items: manifest.items.map(artifactToJsonValue)
106
+ items: items.map(artifactToJsonValue)
95
107
  };
108
+ if (groupBy) {
109
+ response.groupBy = groupBy;
110
+ response.groups = buildArtifactGroups(items, groupBy);
111
+ }
112
+ return response;
96
113
  };
97
114
  export const refreshArtifacts = async (params, context) => {
98
115
  const objectParams = expectOptionalObject(params);
99
- const manifest = await refreshArtifactManifest(context);
116
+ const sessionId = await resolveCanonicalArtifactSessionId(context, optionalTrimmedString(objectParams.sessionId));
117
+ const manifest = await refreshArtifactManifest(context, { sessionId });
118
+ const items = sessionId
119
+ ? manifest.items.filter((item) => item.sessionId === sessionId)
120
+ : manifest.items;
100
121
  return {
101
122
  ok: true,
102
- scope: 'workspace',
103
- count: manifest.items.length,
123
+ scope: sessionId ? 'session' : 'workspace',
124
+ sessionId: sessionId ?? null,
125
+ count: items.length,
104
126
  manifestPath: manifest.manifestPath,
105
127
  workspaceRoot: manifest.workspacePaths.workspaceRoot,
106
- items: manifest.items.map(artifactToJsonValue)
128
+ items: items.map(artifactToJsonValue)
107
129
  };
108
130
  };
109
131
  export const getArtifactContent = async (params, context) => {
@@ -129,10 +151,12 @@ export const getArtifactContent = async (params, context) => {
129
151
  export const ensureArtifactUploaded = async (params, context) => {
130
152
  const objectParams = expectObject(params);
131
153
  const artifact = await resolveArtifact(objectParams, context);
154
+ const scope = artifact.sessionId ? 'session' : 'workspace';
132
155
  if (artifact.storageStatus === 'uploaded' && artifact.fileUrl) {
133
156
  return {
134
157
  ok: true,
135
- scope: 'workspace',
158
+ scope,
159
+ sessionId: artifact.sessionId ?? null,
136
160
  uploaded: false,
137
161
  artifactId: artifact.id,
138
162
  objectKey: artifact.objectKey ?? null,
@@ -161,7 +185,8 @@ export const ensureArtifactUploaded = async (params, context) => {
161
185
  });
162
186
  return {
163
187
  ok: true,
164
- scope: 'workspace',
188
+ scope,
189
+ sessionId: artifact.sessionId ?? null,
165
190
  uploaded: true,
166
191
  artifactId: artifact.id,
167
192
  objectKey,
@@ -178,6 +203,9 @@ export const publishArtifact = async (params, context) => {
178
203
  const uploadParams = {
179
204
  artifactId: artifact.id
180
205
  };
206
+ if (artifact.sessionId) {
207
+ uploadParams.sessionId = artifact.sessionId;
208
+ }
181
209
  if (objectParams.presignedPostBody !== undefined) {
182
210
  uploadParams.presignedPostBody = objectParams.presignedPostBody;
183
211
  }
@@ -192,21 +220,32 @@ export const publishArtifact = async (params, context) => {
192
220
  export const findLatestArtifacts = async (params, context) => {
193
221
  const objectParams = expectOptionalObject(params);
194
222
  const refresh = objectParams.refresh !== false;
223
+ const normalizedFilters = normalizeArtifactFindFilters(objectParams);
224
+ const filters = {
225
+ ...normalizedFilters,
226
+ sessionId: await resolveCanonicalArtifactSessionId(context, normalizedFilters.sessionId)
227
+ };
195
228
  const manifest = refresh
196
- ? await refreshArtifactManifest(context)
229
+ ? await refreshArtifactManifest(context, { sessionId: filters.sessionId })
197
230
  : await readOrRefreshArtifactManifest(context);
198
- const filters = normalizeArtifactFindFilters(objectParams);
199
- const items = manifest.items
231
+ const groupBy = normalizeArtifactGroupBy(objectParams.groupBy);
232
+ const matchedItems = manifest.items
200
233
  .filter((item) => matchesArtifactFindFilters(item, filters))
201
234
  .sort(compareArtifactsByRecency)
202
- .slice(0, filters.limit)
203
- .map(artifactToJsonValue);
204
- return {
235
+ .slice(0, filters.limit);
236
+ const response = {
205
237
  ok: true,
206
- scope: 'workspace',
207
- count: items.length,
208
- items
238
+ scope: filters.sessionId ? 'session' : 'workspace',
239
+ sessionId: filters.sessionId ?? null,
240
+ query: filters.query ?? null,
241
+ count: matchedItems.length,
242
+ items: matchedItems.map(artifactToJsonValue)
209
243
  };
244
+ if (groupBy) {
245
+ response.groupBy = groupBy;
246
+ response.groups = buildArtifactGroups(matchedItems, groupBy);
247
+ }
248
+ return response;
210
249
  };
211
250
  export const getArtifactPresignedPost = async (params, context) => {
212
251
  const objectParams = expectObject(params);
@@ -248,11 +287,15 @@ export const markArtifactUploaded = async (params, context) => {
248
287
  throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, 'At least one of objectKey or fileUrl is required');
249
288
  }
250
289
  const manifest = await readOrRefreshArtifactManifest(context);
251
- const artifact = findArtifact(manifest.items, objectParams);
290
+ const sessionId = await resolveCanonicalArtifactSessionId(context, optionalTrimmedString(objectParams.sessionId));
291
+ const artifact = findArtifact(manifest.items, objectParams, {
292
+ sessionId
293
+ });
252
294
  if (!artifact) {
253
295
  throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, 'Artifact not found', {
254
296
  artifactId: objectParams.artifactId,
255
- relativePath: objectParams.relativePath
297
+ relativePath: objectParams.relativePath,
298
+ sessionId
256
299
  });
257
300
  }
258
301
  const updated = await persistUploadedArtifact(manifest.manifestPath, manifest.items, artifact.id, {
@@ -262,7 +305,8 @@ export const markArtifactUploaded = async (params, context) => {
262
305
  });
263
306
  return {
264
307
  ok: true,
265
- scope: 'workspace',
308
+ scope: updated.sessionId ? 'session' : 'workspace',
309
+ sessionId: updated.sessionId ?? null,
266
310
  artifactId: artifact.id,
267
311
  item: artifactToJsonValue(updated)
268
312
  };
@@ -285,7 +329,8 @@ export function shouldIgnoreArtifactFile(fileName) {
285
329
  const ext = path.extname(normalized);
286
330
  return IGNORE_EXTENSIONS.has(ext);
287
331
  }
288
- async function refreshArtifactManifest(context) {
332
+ async function refreshArtifactManifest(context, options) {
333
+ const taggedSessionId = optionalTrimmedString(options?.sessionId);
289
334
  const workspacePaths = await ensureWorkspacePaths(context.openclawRoot);
290
335
  const { workspaceRoot, manifestPath } = workspacePaths;
291
336
  const existing = await readExistingManifest(manifestPath);
@@ -293,28 +338,69 @@ async function refreshArtifactManifest(context) {
293
338
  const loadedMarkdownScanState = await readMarkdownScanState(workspacePaths.markdownScanStatePath);
294
339
  const hadMarkdownScanState = loadedMarkdownScanState !== null;
295
340
  const markdownScanState = loadedMarkdownScanState ?? createMarkdownScanState();
296
- const files = await collectArtifactFiles(workspaceRoot);
341
+ const archiveState = await readArtifactArchiveState(workspacePaths.artifactArchiveStatePath);
342
+ const files = (await collectArtifactFiles(workspaceRoot))
343
+ .sort((left, right) => compareDiscoveredArtifactPaths(workspaceRoot, left, right));
344
+ const discoveredRelativePaths = new Set(files.map((filePath) => path.relative(workspaceRoot, filePath).replace(/\\/g, '/')));
345
+ const emittedRelativePaths = new Set();
297
346
  const items = [];
298
- for (const fullPath of files) {
299
- const stat = await fs.stat(fullPath);
347
+ for (const discoveredFullPath of files) {
348
+ let fullPath = discoveredFullPath;
349
+ let stat = await fs.stat(fullPath);
300
350
  if (!stat.isFile() || stat.size < MIN_ARTIFACT_SIZE_BYTES) {
301
351
  continue;
302
352
  }
303
- const relativePath = path.relative(workspaceRoot, fullPath).replace(/\\/g, '/');
304
- const existingItem = existingByPath.get(relativePath);
353
+ let relativePath = path.relative(workspaceRoot, fullPath).replace(/\\/g, '/');
354
+ let existingItem = existingByPath.get(relativePath);
305
355
  const fileName = path.basename(fullPath);
356
+ const pathSessionId = extractSessionIdFromArtifactRelativePath(relativePath);
306
357
  const isMarkdown = isMarkdownArtifactFile(fileName);
307
358
  if (isMarkdown) {
308
- if (!shouldIncludeMarkdownArtifactFile(relativePath, fileName, stat, markdownScanState, hadMarkdownScanState)) {
359
+ if (!shouldIncludeMarkdownArtifactFile(relativePath, fileName, stat, markdownScanState, hadMarkdownScanState, Boolean(taggedSessionId || pathSessionId))) {
309
360
  continue;
310
361
  }
311
362
  }
312
363
  else if (!shouldIncludeArtifactFile(fileName)) {
313
364
  continue;
314
365
  }
366
+ const archiveEntry = archiveState.files[relativePath];
367
+ if (!pathSessionId) {
368
+ if (archiveEntry && await artifactArchiveEntryIsCurrent(workspaceRoot, archiveEntry, stat)) {
369
+ if (!taggedSessionId) {
370
+ continue;
371
+ }
372
+ const archivedSessionId = await resolveArchiveStateSessionId(context, archiveEntry.sessionId);
373
+ if (archivedSessionId && (archivedSessionId !== taggedSessionId || archiveEntry.sessionId === taggedSessionId)) {
374
+ continue;
375
+ }
376
+ }
377
+ if (taggedSessionId) {
378
+ const archivedRelativePath = await copyArtifactFileToSessionDirectory(workspaceRoot, taggedSessionId, relativePath, fullPath, stat, existingByPath, archiveEntry);
379
+ archiveState.files[relativePath] = {
380
+ sessionId: taggedSessionId,
381
+ targetRelativePath: archivedRelativePath,
382
+ sizeBytes: stat.size,
383
+ mtimeMs: stat.mtimeMs,
384
+ archivedAt: new Date().toISOString()
385
+ };
386
+ if (discoveredRelativePaths.has(archivedRelativePath)) {
387
+ continue;
388
+ }
389
+ fullPath = ensureInside(workspaceRoot, path.join(workspaceRoot, archivedRelativePath));
390
+ relativePath = archivedRelativePath;
391
+ existingItem = existingByPath.get(relativePath);
392
+ stat = await fs.stat(fullPath);
393
+ }
394
+ }
395
+ if (emittedRelativePaths.has(relativePath)) {
396
+ continue;
397
+ }
315
398
  const ext = normalizeExtension(fileName);
316
399
  const createdAt = normalizeTimestamp(stat.birthtime, stat.mtime);
317
- const updatedAt = stat.mtime.toISOString();
400
+ let updatedAt = stat.mtime.toISOString();
401
+ let currentStat = stat;
402
+ const resolvedSessionId = resolveArtifactSessionId(relativePath, existingItem, taggedSessionId);
403
+ emittedRelativePaths.add(relativePath);
318
404
  items.push({
319
405
  id: existingItem?.id ?? buildArtifactId(relativePath),
320
406
  fileName,
@@ -323,9 +409,10 @@ async function refreshArtifactManifest(context) {
323
409
  category: classifyArtifactCategory(fileName),
324
410
  mimeType: MIME_BY_EXTENSION[ext ?? ''] ?? 'application/octet-stream',
325
411
  ext,
326
- sizeBytes: stat.size,
412
+ sizeBytes: currentStat.size,
327
413
  storageStatus: existingItem?.storageStatus ?? 'local_only',
328
414
  source: existingItem?.source ?? 'generated',
415
+ sessionId: resolvedSessionId,
329
416
  previewable: isPreviewableCategory(classifyArtifactCategory(fileName)),
330
417
  createdAt: existingItem?.createdAt ?? createdAt,
331
418
  updatedAt,
@@ -338,6 +425,7 @@ async function refreshArtifactManifest(context) {
338
425
  items.sort((a, b) => a.fileName.localeCompare(b.fileName));
339
426
  markdownScanState.lastScanAt = new Date().toISOString();
340
427
  await writeMarkdownScanState(workspacePaths.markdownScanStatePath, markdownScanState);
428
+ await writeArtifactArchiveState(workspacePaths.artifactArchiveStatePath, archiveState);
341
429
  await writeArtifactManifest(manifestPath, items);
342
430
  return { manifestPath, items, workspacePaths };
343
431
  }
@@ -395,6 +483,30 @@ async function writeMarkdownScanState(statePath, state) {
395
483
  await ensureDir(path.dirname(statePath));
396
484
  await fs.writeFile(statePath, JSON.stringify(state, null, 2), 'utf8');
397
485
  }
486
+ async function readArtifactArchiveState(statePath) {
487
+ if (!(await pathExists(statePath))) {
488
+ return createArtifactArchiveState();
489
+ }
490
+ const parsed = await readJsonFile(statePath);
491
+ if (!isObject(parsed) || !isObject(parsed.files)) {
492
+ return createArtifactArchiveState();
493
+ }
494
+ const files = {};
495
+ for (const [relativePath, value] of Object.entries(parsed.files)) {
496
+ const normalizedRelativePath = normalizeRelativePath(relativePath);
497
+ if (normalizedRelativePath && isArtifactArchiveStateFile(value)) {
498
+ files[normalizedRelativePath] = value;
499
+ }
500
+ }
501
+ return {
502
+ version: 1,
503
+ files
504
+ };
505
+ }
506
+ async function writeArtifactArchiveState(statePath, state) {
507
+ await ensureDir(path.dirname(statePath));
508
+ await fs.writeFile(statePath, JSON.stringify(state, null, 2), 'utf8');
509
+ }
398
510
  async function persistUploadedArtifact(manifestPath, items, artifactId, updates) {
399
511
  const now = new Date().toISOString();
400
512
  let updatedArtifact = null;
@@ -421,19 +533,20 @@ async function persistUploadedArtifact(manifestPath, items, artifactId, updates)
421
533
  }
422
534
  return updatedArtifact;
423
535
  }
424
- async function collectArtifactFiles(rootDir) {
536
+ async function collectArtifactFiles(rootDir, baseDir = rootDir) {
425
537
  const files = [];
426
538
  const entries = await fs.readdir(rootDir, { withFileTypes: true });
427
539
  for (const entry of entries) {
428
- if (entry.isDirectory() && shouldIgnoreArtifactDirectory(entry.name)) {
540
+ const fullPath = path.join(rootDir, entry.name);
541
+ const relativePath = path.relative(baseDir, fullPath).replace(/\\/g, '/');
542
+ if (entry.isDirectory() && shouldIgnoreArtifactDirectory(entry.name, relativePath)) {
429
543
  continue;
430
544
  }
431
545
  if (entry.isFile() && shouldIgnoreArtifactFile(entry.name)) {
432
546
  continue;
433
547
  }
434
- const fullPath = path.join(rootDir, entry.name);
435
548
  if (entry.isDirectory()) {
436
- files.push(...await collectArtifactFiles(fullPath));
549
+ files.push(...await collectArtifactFiles(fullPath, baseDir));
437
550
  continue;
438
551
  }
439
552
  if (entry.isFile()) {
@@ -442,15 +555,47 @@ async function collectArtifactFiles(rootDir) {
442
555
  }
443
556
  return files;
444
557
  }
558
+ function compareDiscoveredArtifactPaths(workspaceRoot, left, right) {
559
+ const leftRelative = path.relative(workspaceRoot, left).replace(/\\/g, '/');
560
+ const rightRelative = path.relative(workspaceRoot, right).replace(/\\/g, '/');
561
+ const leftIsSessionArtifact = Boolean(extractSessionIdFromArtifactRelativePath(leftRelative));
562
+ const rightIsSessionArtifact = Boolean(extractSessionIdFromArtifactRelativePath(rightRelative));
563
+ if (leftIsSessionArtifact !== rightIsSessionArtifact) {
564
+ return leftIsSessionArtifact ? 1 : -1;
565
+ }
566
+ return leftRelative.localeCompare(rightRelative);
567
+ }
568
+ async function artifactArchiveEntryIsCurrent(workspaceRoot, entry, stat) {
569
+ if (entry.sizeBytes !== stat.size || entry.mtimeMs !== stat.mtimeMs) {
570
+ return false;
571
+ }
572
+ const targetFullPath = ensureInside(workspaceRoot, path.join(workspaceRoot, entry.targetRelativePath));
573
+ return await pathExists(targetFullPath);
574
+ }
575
+ async function copyArtifactFileToSessionDirectory(workspaceRoot, sessionId, sourceRelativePath, sourceFullPath, sourceStat, existingByPath, archiveEntry) {
576
+ const targetRelativePath = archiveEntry?.sessionId === sessionId
577
+ ? archiveEntry.targetRelativePath
578
+ : await resolveAvailableSessionArtifactRelativePath(workspaceRoot, sessionId, sourceRelativePath, existingByPath);
579
+ const targetFullPath = ensureInside(workspaceRoot, path.join(workspaceRoot, targetRelativePath));
580
+ await ensureDir(path.dirname(targetFullPath));
581
+ await fs.copyFile(sourceFullPath, targetFullPath);
582
+ await fs.utimes(targetFullPath, new Date(), new Date(sourceStat.mtimeMs));
583
+ return targetRelativePath;
584
+ }
445
585
  async function resolveArtifact(params, context, options) {
586
+ const sessionId = await resolveCanonicalArtifactSessionId(context, optionalTrimmedString(params.sessionId));
587
+ const preparedSelector = options?.refreshManifest && sessionId
588
+ ? await prepareSessionArtifactSelector(params, context, sessionId)
589
+ : params;
446
590
  const manifest = options?.refreshManifest
447
- ? await refreshArtifactManifest(context)
591
+ ? await refreshArtifactManifest(context, { sessionId })
448
592
  : await readOrRefreshArtifactManifest(context);
449
- const artifact = findArtifact(manifest.items, params);
593
+ const artifact = findArtifact(manifest.items, preparedSelector, { sessionId });
450
594
  if (!artifact) {
451
595
  throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, 'Artifact not found', {
452
596
  artifactId: params.artifactId,
453
- relativePath: params.relativePath
597
+ relativePath: params.relativePath,
598
+ sessionId
454
599
  });
455
600
  }
456
601
  const checkedPath = ensureInside(manifest.workspacePaths.workspaceRoot, artifact.localPath);
@@ -464,19 +609,99 @@ async function resolveArtifact(params, context, options) {
464
609
  localPath: checkedPath
465
610
  };
466
611
  }
467
- function findArtifact(items, selector) {
612
+ async function prepareSessionArtifactSelector(params, context, sessionId) {
613
+ const relativePath = normalizeRelativePath(optionalTrimmedString(params.relativePath));
614
+ if (relativePath) {
615
+ const archivedRelativePath = await archiveArtifactFileToSessionDirectory(context, sessionId, relativePath);
616
+ return {
617
+ ...params,
618
+ relativePath: archivedRelativePath ?? relativePath,
619
+ sessionId
620
+ };
621
+ }
622
+ const artifactId = optionalTrimmedString(params.artifactId);
623
+ if (!artifactId) {
624
+ return { ...params, sessionId };
625
+ }
626
+ const manifest = await readOrRefreshArtifactManifest(context);
627
+ const artifact = findArtifact(manifest.items, { artifactId });
628
+ if (!artifact || artifact.sessionId === sessionId) {
629
+ return { ...params, sessionId };
630
+ }
631
+ if (artifact.sessionId) {
632
+ return { ...params, sessionId };
633
+ }
634
+ const archivedRelativePath = await archiveArtifactFileToSessionDirectory(context, sessionId, artifact.relativePath);
635
+ return {
636
+ relativePath: archivedRelativePath ?? artifact.relativePath,
637
+ sessionId
638
+ };
639
+ }
640
+ async function archiveArtifactFileToSessionDirectory(context, sessionId, sourceRelativePath) {
641
+ const normalizedSourcePath = normalizeRelativePath(sourceRelativePath);
642
+ if (!normalizedSourcePath || extractSessionIdFromArtifactRelativePath(normalizedSourcePath)) {
643
+ return normalizedSourcePath;
644
+ }
645
+ const workspacePaths = await ensureWorkspacePaths(context.openclawRoot);
646
+ const sourceFullPath = ensureInside(workspacePaths.workspaceRoot, path.join(workspacePaths.workspaceRoot, normalizedSourcePath));
647
+ if (!(await pathExists(sourceFullPath))) {
648
+ return null;
649
+ }
650
+ const sourceStat = await fs.stat(sourceFullPath);
651
+ if (!sourceStat.isFile()) {
652
+ return null;
653
+ }
654
+ const existing = await readExistingManifest(workspacePaths.manifestPath);
655
+ const existingByPath = new Map(existing.map((item) => [item.relativePath, item]));
656
+ const archiveState = await readArtifactArchiveState(workspacePaths.artifactArchiveStatePath);
657
+ const archiveEntry = archiveState.files[normalizedSourcePath];
658
+ if (archiveEntry &&
659
+ archiveEntry.sessionId === sessionId &&
660
+ await artifactArchiveEntryIsCurrent(workspacePaths.workspaceRoot, archiveEntry, sourceStat)) {
661
+ return archiveEntry.targetRelativePath;
662
+ }
663
+ const targetRelativePath = await copyArtifactFileToSessionDirectory(workspacePaths.workspaceRoot, sessionId, normalizedSourcePath, sourceFullPath, sourceStat, existingByPath, archiveEntry);
664
+ archiveState.files[normalizedSourcePath] = {
665
+ sessionId,
666
+ targetRelativePath,
667
+ sizeBytes: sourceStat.size,
668
+ mtimeMs: sourceStat.mtimeMs,
669
+ archivedAt: new Date().toISOString()
670
+ };
671
+ await writeArtifactArchiveState(workspacePaths.artifactArchiveStatePath, archiveState);
672
+ return targetRelativePath;
673
+ }
674
+ function findArtifact(items, selector, options) {
468
675
  const artifactId = optionalTrimmedString(selector.artifactId);
469
676
  const relativePath = normalizeRelativePath(optionalTrimmedString(selector.relativePath));
677
+ const sessionId = optionalTrimmedString(options?.sessionId);
470
678
  if (!artifactId && !relativePath) {
471
679
  throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, 'Either artifactId or relativePath is required');
472
680
  }
473
681
  return items.find((item) => {
682
+ if (sessionId && item.sessionId !== sessionId) {
683
+ return false;
684
+ }
474
685
  if (artifactId && item.id === artifactId) {
475
686
  return true;
476
687
  }
477
- return Boolean(relativePath && item.relativePath === relativePath);
688
+ return Boolean(relativePath && artifactRelativePathMatches(item, relativePath, sessionId));
478
689
  }) ?? null;
479
690
  }
691
+ function artifactRelativePathMatches(item, relativePath, sessionId) {
692
+ if (item.relativePath === relativePath) {
693
+ return true;
694
+ }
695
+ if (!sessionId) {
696
+ return false;
697
+ }
698
+ const sessionRelativePath = buildSessionArtifactRelativePath(sessionId, relativePath);
699
+ return item.relativePath === sessionRelativePath;
700
+ }
701
+ function buildSessionArtifactRelativePath(sessionId, relativePath) {
702
+ const normalizedRelativePath = normalizeRelativePath(relativePath) ?? path.posix.basename(relativePath);
703
+ return `${SESSION_ARTIFACTS_ROOT_RELATIVE}/${safeSessionDirectoryName(sessionId)}/${normalizedRelativePath}`;
704
+ }
480
705
  function resolveWorkspaceRoot(openclawRoot) {
481
706
  const workspaceRoot = path.join(openclawRoot, 'workspace');
482
707
  return ensureInside(openclawRoot, workspaceRoot);
@@ -485,11 +710,13 @@ async function ensureWorkspacePaths(openclawRoot) {
485
710
  const workspaceRoot = resolveWorkspaceRoot(openclawRoot);
486
711
  const manifestPath = ensureInside(workspaceRoot, path.join(workspaceRoot, ARTIFACT_MANIFEST_FILE));
487
712
  const markdownScanStatePath = ensureInside(workspaceRoot, path.join(workspaceRoot, PLUGIN_WORKSPACE_STATE_DIR, MARKDOWN_SCAN_STATE_FILE));
713
+ const artifactArchiveStatePath = ensureInside(workspaceRoot, path.join(workspaceRoot, PLUGIN_WORKSPACE_STATE_DIR, ARTIFACT_ARCHIVE_STATE_FILE));
488
714
  await ensureDir(workspaceRoot);
489
715
  return {
490
716
  workspaceRoot,
491
717
  manifestPath,
492
- markdownScanStatePath
718
+ markdownScanStatePath,
719
+ artifactArchiveStatePath
493
720
  };
494
721
  }
495
722
  function createMarkdownScanState() {
@@ -500,9 +727,94 @@ function createMarkdownScanState() {
500
727
  files: {}
501
728
  };
502
729
  }
730
+ function createArtifactArchiveState() {
731
+ return {
732
+ version: 1,
733
+ files: {}
734
+ };
735
+ }
503
736
  function buildArtifactId(relativePath) {
504
737
  return `art_${createHash('sha1').update(relativePath).digest('hex').slice(0, 12)}`;
505
738
  }
739
+ async function resolveCanonicalArtifactSessionId(context, sessionIdOrKey) {
740
+ if (!sessionIdOrKey) {
741
+ return null;
742
+ }
743
+ const session = await findSessionRecord(context, sessionIdOrKey).catch(() => null);
744
+ if (session?.sessionId) {
745
+ return session.sessionId;
746
+ }
747
+ if (looksLikeOpenClawSessionKey(sessionIdOrKey)) {
748
+ throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, `Session key not found: ${sessionIdOrKey}`, { sessionKey: sessionIdOrKey });
749
+ }
750
+ return sessionIdOrKey;
751
+ }
752
+ async function resolveArchiveStateSessionId(context, sessionIdOrKey) {
753
+ try {
754
+ return await resolveCanonicalArtifactSessionId(context, sessionIdOrKey);
755
+ }
756
+ catch (error) {
757
+ if (looksLikeOpenClawSessionKey(sessionIdOrKey)) {
758
+ return null;
759
+ }
760
+ throw error;
761
+ }
762
+ }
763
+ function looksLikeOpenClawSessionKey(value) {
764
+ return value.trim().toLowerCase().startsWith('agent:');
765
+ }
766
+ async function resolveAvailableSessionArtifactRelativePath(workspaceRoot, sessionId, sourceRelativePath, existingByPath) {
767
+ const normalizedSourcePath = normalizeRelativePath(sourceRelativePath) ?? path.basename(sourceRelativePath);
768
+ const sessionRoot = `${SESSION_ARTIFACTS_ROOT_RELATIVE}/${safeSessionDirectoryName(sessionId)}`;
769
+ const initialTarget = `${sessionRoot}/${normalizedSourcePath}`;
770
+ const parsed = path.posix.parse(initialTarget);
771
+ let candidate = initialTarget;
772
+ let suffix = 1;
773
+ while (existingByPath.has(candidate) ||
774
+ await pathExists(ensureInside(workspaceRoot, path.join(workspaceRoot, candidate)))) {
775
+ candidate = path.posix.join(parsed.dir, `${parsed.name}-${suffix}${parsed.ext}`);
776
+ suffix += 1;
777
+ }
778
+ return candidate;
779
+ }
780
+ function safeSessionDirectoryName(sessionId) {
781
+ const trimmed = sessionId.trim();
782
+ const sanitized = trimmed
783
+ .replace(/[<>:"/\\|?*\x00-\x1F]/g, '_')
784
+ .replace(/^\.+$/, '_')
785
+ .replace(/[. ]+$/g, '')
786
+ .slice(0, 120);
787
+ if (sanitized) {
788
+ return sanitized;
789
+ }
790
+ return createHash('sha1').update(trimmed).digest('hex').slice(0, 12);
791
+ }
792
+ function extractSessionIdFromArtifactRelativePath(relativePath) {
793
+ const normalized = normalizeRelativePath(relativePath);
794
+ if (!normalized) {
795
+ return null;
796
+ }
797
+ const prefix = `${SESSION_ARTIFACTS_ROOT_RELATIVE}/`;
798
+ if (!normalized.startsWith(prefix)) {
799
+ return null;
800
+ }
801
+ const rest = normalized.slice(prefix.length);
802
+ const sessionId = rest.split('/')[0]?.trim();
803
+ return sessionId || null;
804
+ }
805
+ function resolveArtifactSessionId(relativePath, existingItem, taggedSessionId) {
806
+ if (existingItem) {
807
+ return existingItem.sessionId ?? null;
808
+ }
809
+ const pathSessionId = extractSessionIdFromArtifactRelativePath(relativePath);
810
+ if (!pathSessionId) {
811
+ return taggedSessionId ?? null;
812
+ }
813
+ if (taggedSessionId && pathSessionId === safeSessionDirectoryName(taggedSessionId)) {
814
+ return taggedSessionId;
815
+ }
816
+ return pathSessionId;
817
+ }
506
818
  function normalizeExtension(fileName) {
507
819
  const ext = path.extname(fileName).toLowerCase();
508
820
  return ext || null;
@@ -531,7 +843,12 @@ function normalizeMaxInlineBytes(value) {
531
843
  function isPreviewableCategory(category) {
532
844
  return category === 'image' || category === 'video' || category === 'document';
533
845
  }
534
- function shouldIgnoreArtifactDirectory(dirName) {
846
+ function shouldIgnoreArtifactDirectory(dirName, relativePath) {
847
+ const normalizedRelativePath = relativePath?.replace(/\\/g, '/').toLowerCase();
848
+ if (normalizedRelativePath === SESSION_ARTIFACTS_ROOT_RELATIVE ||
849
+ normalizedRelativePath?.startsWith(`${SESSION_ARTIFACTS_ROOT_RELATIVE}/`)) {
850
+ return false;
851
+ }
535
852
  const normalized = dirName.trim().toLowerCase();
536
853
  return !normalized || normalized.startsWith('.') || IGNORE_DIRECTORY_NAMES.has(normalized);
537
854
  }
@@ -541,7 +858,7 @@ function shouldIncludeArtifactFile(fileName) {
541
858
  }
542
859
  return classifyArtifactCategory(fileName) !== 'other';
543
860
  }
544
- function shouldIncludeMarkdownArtifactFile(relativePath, fileName, stat, state, hadState) {
861
+ function shouldIncludeMarkdownArtifactFile(relativePath, fileName, stat, state, hadState, includeNewFile) {
545
862
  if (shouldIgnoreArtifactFile(fileName)) {
546
863
  return false;
547
864
  }
@@ -558,9 +875,9 @@ function shouldIncludeMarkdownArtifactFile(relativePath, fileName, stat, state,
558
875
  sizeBytes: stat.size,
559
876
  mtimeMs: stat.mtimeMs,
560
877
  firstSeenAt: new Date().toISOString(),
561
- baseline: !hadState
878
+ baseline: !hadState && !includeNewFile
562
879
  };
563
- return hadState;
880
+ return hadState || includeNewFile;
564
881
  }
565
882
  function isMarkdownScanStateFile(value) {
566
883
  if (!value || Array.isArray(value) || typeof value !== 'object') {
@@ -572,6 +889,17 @@ function isMarkdownScanStateFile(value) {
572
889
  typeof objectValue.firstSeenAt === 'string' &&
573
890
  typeof objectValue.baseline === 'boolean');
574
891
  }
892
+ function isArtifactArchiveStateFile(value) {
893
+ if (!value || Array.isArray(value) || typeof value !== 'object') {
894
+ return false;
895
+ }
896
+ const objectValue = value;
897
+ return (typeof objectValue.sessionId === 'string' &&
898
+ typeof objectValue.targetRelativePath === 'string' &&
899
+ typeof objectValue.sizeBytes === 'number' &&
900
+ typeof objectValue.mtimeMs === 'number' &&
901
+ typeof objectValue.archivedAt === 'string');
902
+ }
575
903
  function isArtifactRecord(value) {
576
904
  if (!value || Array.isArray(value) || typeof value !== 'object') {
577
905
  return false;
@@ -594,6 +922,7 @@ function artifactToJsonValue(item) {
594
922
  sizeBytes: item.sizeBytes,
595
923
  storageStatus: item.storageStatus,
596
924
  source: item.source,
925
+ sessionId: item.sessionId ?? null,
597
926
  previewable: item.previewable,
598
927
  createdAt: item.createdAt,
599
928
  updatedAt: item.updatedAt,
@@ -631,13 +960,20 @@ function expectString(value, fieldName) {
631
960
  function optionalTrimmedString(value) {
632
961
  return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
633
962
  }
634
- function normalizeArtifactFindFilters(params) {
963
+ function normalizeArtifactFilters(params) {
635
964
  return {
965
+ sessionId: optionalTrimmedString(params.sessionId),
966
+ query: normalizeOptionalLowercaseString(params.query),
636
967
  category: normalizeArtifactCategory(optionalTrimmedString(params.category)),
637
968
  mimeType: normalizeOptionalLowercaseString(params.mimeType),
638
969
  fileName: normalizeOptionalLowercaseString(params.fileName),
639
970
  relativePathPrefix: normalizeRelativePath(optionalTrimmedString(params.relativePathPrefix)),
640
- createdAfterMs: normalizeOptionalTimestamp(params.createdAfter),
971
+ createdAfterMs: normalizeOptionalTimestamp(params.createdAfter)
972
+ };
973
+ }
974
+ function normalizeArtifactFindFilters(params) {
975
+ return {
976
+ ...normalizeArtifactFilters(params),
641
977
  limit: normalizeArtifactLimit(params.limit)
642
978
  };
643
979
  }
@@ -680,14 +1016,36 @@ function normalizeArtifactLimit(value) {
680
1016
  }
681
1017
  return Math.min(50, Math.floor(value));
682
1018
  }
683
- function matchesArtifactFindFilters(item, filters) {
1019
+ function normalizeArtifactGroupBy(value) {
1020
+ const normalized = normalizeOptionalLowercaseString(value);
1021
+ if (!normalized || normalized === 'none') {
1022
+ return null;
1023
+ }
1024
+ if (normalized === 'session') {
1025
+ return 'session';
1026
+ }
1027
+ if (normalized === 'category' || normalized === 'type') {
1028
+ return 'category';
1029
+ }
1030
+ if (normalized === 'day' || normalized === 'date' || normalized === 'time') {
1031
+ return 'day';
1032
+ }
1033
+ throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, `Unsupported artifact groupBy: ${normalized}`);
1034
+ }
1035
+ function matchesArtifactFilters(item, filters) {
1036
+ if (filters.sessionId && item.sessionId !== filters.sessionId) {
1037
+ return false;
1038
+ }
1039
+ if (filters.query && !artifactMatchesQuery(item, filters.query)) {
1040
+ return false;
1041
+ }
684
1042
  if (filters.category && item.category !== filters.category) {
685
1043
  return false;
686
1044
  }
687
1045
  if (filters.mimeType && item.mimeType.toLowerCase() !== filters.mimeType) {
688
1046
  return false;
689
1047
  }
690
- if (filters.fileName && item.fileName.toLowerCase() !== filters.fileName) {
1048
+ if (filters.fileName && !item.fileName.toLowerCase().includes(filters.fileName)) {
691
1049
  return false;
692
1050
  }
693
1051
  if (filters.relativePathPrefix && !item.relativePath.startsWith(filters.relativePathPrefix)) {
@@ -698,6 +1056,84 @@ function matchesArtifactFindFilters(item, filters) {
698
1056
  }
699
1057
  return true;
700
1058
  }
1059
+ function matchesArtifactFindFilters(item, filters) {
1060
+ return matchesArtifactFilters(item, filters);
1061
+ }
1062
+ function artifactMatchesQuery(item, query) {
1063
+ const values = [
1064
+ item.fileName,
1065
+ item.relativePath,
1066
+ item.category,
1067
+ item.mimeType,
1068
+ item.ext ?? '',
1069
+ item.sessionId ?? ''
1070
+ ];
1071
+ return values.some((value) => value.toLowerCase().includes(query));
1072
+ }
1073
+ function buildArtifactGroups(items, groupBy) {
1074
+ const groups = new Map();
1075
+ for (const item of items) {
1076
+ const { key, label } = resolveArtifactGroup(item, groupBy);
1077
+ const group = groups.get(key) ?? { key, label, items: [] };
1078
+ group.items.push(item);
1079
+ groups.set(key, group);
1080
+ }
1081
+ return Array.from(groups.values())
1082
+ .sort((left, right) => compareArtifactGroups(left, right, groupBy))
1083
+ .map((group) => ({
1084
+ key: group.key,
1085
+ label: group.label,
1086
+ count: group.items.length,
1087
+ items: group.items.map(artifactToJsonValue)
1088
+ }));
1089
+ }
1090
+ function resolveArtifactGroup(item, groupBy) {
1091
+ if (groupBy === 'session') {
1092
+ const sessionId = item.sessionId ?? null;
1093
+ return {
1094
+ key: sessionId ?? 'unassigned',
1095
+ label: sessionId ?? 'Unassigned'
1096
+ };
1097
+ }
1098
+ if (groupBy === 'category') {
1099
+ return {
1100
+ key: item.category,
1101
+ label: item.category
1102
+ };
1103
+ }
1104
+ const timestamp = Date.parse(item.createdAt);
1105
+ if (Number.isNaN(timestamp)) {
1106
+ return {
1107
+ key: 'unknown',
1108
+ label: 'Unknown'
1109
+ };
1110
+ }
1111
+ const day = new Date(timestamp).toISOString().slice(0, 10);
1112
+ return {
1113
+ key: day,
1114
+ label: day
1115
+ };
1116
+ }
1117
+ function compareArtifactGroups(left, right, groupBy) {
1118
+ if (groupBy === 'day') {
1119
+ return right.key.localeCompare(left.key);
1120
+ }
1121
+ if (groupBy === 'category') {
1122
+ return artifactCategorySortIndex(left.key) - artifactCategorySortIndex(right.key);
1123
+ }
1124
+ if (left.key === 'unassigned') {
1125
+ return 1;
1126
+ }
1127
+ if (right.key === 'unassigned') {
1128
+ return -1;
1129
+ }
1130
+ return left.key.localeCompare(right.key);
1131
+ }
1132
+ function artifactCategorySortIndex(value) {
1133
+ const order = ['image', 'video', 'document', 'archive', 'other'];
1134
+ const index = order.indexOf(value);
1135
+ return index === -1 ? order.length : index;
1136
+ }
701
1137
  function compareArtifactsByRecency(left, right) {
702
1138
  const createdDelta = Date.parse(right.createdAt) - Date.parse(left.createdAt);
703
1139
  if (createdDelta !== 0) {