chuk-artifacts 0.1.1__py3-none-any.whl → 0.1.3__py3-none-any.whl
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.
- chuk_artifacts/admin.py +24 -18
- chuk_artifacts/core.py +94 -120
- chuk_artifacts/metadata.py +139 -240
- chuk_artifacts/presigned.py +59 -23
- chuk_artifacts/session/__init__.py +0 -0
- chuk_artifacts/session/session_manager.py +196 -0
- chuk_artifacts/{session_operations.py → session/session_operations.py} +46 -125
- chuk_artifacts/store.py +353 -267
- {chuk_artifacts-0.1.1.dist-info → chuk_artifacts-0.1.3.dist-info}/METADATA +200 -191
- {chuk_artifacts-0.1.1.dist-info → chuk_artifacts-0.1.3.dist-info}/RECORD +13 -11
- {chuk_artifacts-0.1.1.dist-info → chuk_artifacts-0.1.3.dist-info}/WHEEL +0 -0
- {chuk_artifacts-0.1.1.dist-info → chuk_artifacts-0.1.3.dist-info}/licenses/LICENSE +0 -0
- {chuk_artifacts-0.1.1.dist-info → chuk_artifacts-0.1.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,196 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
# chuk_artifacts/session/session_manager.py
|
3
|
+
"""
|
4
|
+
Clean session manager for grid architecture.
|
5
|
+
|
6
|
+
Simple rules:
|
7
|
+
- Always have a session (auto-allocate if needed)
|
8
|
+
- Grid paths: grid/{sandbox_id}/{session_id}/{artifact_id}
|
9
|
+
- No legacy compatibility, clean implementation
|
10
|
+
"""
|
11
|
+
|
12
|
+
from __future__ import annotations
|
13
|
+
|
14
|
+
import uuid
|
15
|
+
import time
|
16
|
+
import json
|
17
|
+
import asyncio
|
18
|
+
import logging
|
19
|
+
from datetime import datetime, timedelta
|
20
|
+
from typing import Optional, Dict, Any, List, AsyncContextManager, Callable
|
21
|
+
from dataclasses import dataclass, asdict
|
22
|
+
|
23
|
+
from ..exceptions import SessionError, ArtifactStoreError
|
24
|
+
|
25
|
+
logger = logging.getLogger(__name__)
|
26
|
+
|
27
|
+
_DEFAULT_SESSION_TTL_HOURS = 24
|
28
|
+
|
29
|
+
|
30
|
+
@dataclass
|
31
|
+
class SessionMetadata:
|
32
|
+
"""Session metadata for grid operations."""
|
33
|
+
session_id: str
|
34
|
+
sandbox_id: str
|
35
|
+
user_id: Optional[str] = None
|
36
|
+
created_at: str = None
|
37
|
+
expires_at: str = None
|
38
|
+
status: str = "active"
|
39
|
+
artifact_count: int = 0
|
40
|
+
total_bytes: int = 0
|
41
|
+
|
42
|
+
def __post_init__(self):
|
43
|
+
if self.created_at is None:
|
44
|
+
self.created_at = datetime.utcnow().isoformat() + "Z"
|
45
|
+
|
46
|
+
def is_expired(self) -> bool:
|
47
|
+
"""Check if session has expired."""
|
48
|
+
if not self.expires_at:
|
49
|
+
return False
|
50
|
+
expires = datetime.fromisoformat(self.expires_at.replace("Z", ""))
|
51
|
+
return datetime.utcnow() > expires
|
52
|
+
|
53
|
+
def to_dict(self) -> Dict[str, Any]:
|
54
|
+
return asdict(self)
|
55
|
+
|
56
|
+
@classmethod
|
57
|
+
def from_dict(cls, data: Dict[str, Any]) -> 'SessionMetadata':
|
58
|
+
return cls(**data)
|
59
|
+
|
60
|
+
|
61
|
+
class SessionManager:
|
62
|
+
"""Simple session manager for grid architecture."""
|
63
|
+
|
64
|
+
def __init__(
|
65
|
+
self,
|
66
|
+
sandbox_id: str,
|
67
|
+
session_factory: Callable[[], AsyncContextManager],
|
68
|
+
default_ttl_hours: int = _DEFAULT_SESSION_TTL_HOURS,
|
69
|
+
):
|
70
|
+
self.sandbox_id = sandbox_id
|
71
|
+
self.session_factory = session_factory
|
72
|
+
self.default_ttl_hours = default_ttl_hours
|
73
|
+
|
74
|
+
# Simple in-memory cache
|
75
|
+
self._session_cache: Dict[str, SessionMetadata] = {}
|
76
|
+
self._cache_lock = asyncio.Lock()
|
77
|
+
|
78
|
+
logger.info(f"SessionManager initialized for sandbox: {sandbox_id}")
|
79
|
+
|
80
|
+
async def allocate_session(
|
81
|
+
self,
|
82
|
+
session_id: Optional[str] = None,
|
83
|
+
user_id: Optional[str] = None,
|
84
|
+
ttl_hours: Optional[int] = None,
|
85
|
+
) -> str:
|
86
|
+
"""Allocate or validate a session."""
|
87
|
+
ttl_hours = ttl_hours or self.default_ttl_hours
|
88
|
+
|
89
|
+
if session_id:
|
90
|
+
# Validate existing session
|
91
|
+
metadata = await self._get_session_metadata(session_id)
|
92
|
+
if metadata and not metadata.is_expired():
|
93
|
+
await self._touch_session(session_id)
|
94
|
+
return session_id
|
95
|
+
|
96
|
+
# Create new session
|
97
|
+
if not session_id:
|
98
|
+
session_id = self._generate_session_id(user_id)
|
99
|
+
|
100
|
+
expires_at = (datetime.utcnow() + timedelta(hours=ttl_hours)).isoformat() + "Z"
|
101
|
+
|
102
|
+
metadata = SessionMetadata(
|
103
|
+
session_id=session_id,
|
104
|
+
sandbox_id=self.sandbox_id,
|
105
|
+
user_id=user_id,
|
106
|
+
expires_at=expires_at,
|
107
|
+
)
|
108
|
+
|
109
|
+
await self._store_session_metadata(metadata)
|
110
|
+
|
111
|
+
async with self._cache_lock:
|
112
|
+
self._session_cache[session_id] = metadata
|
113
|
+
|
114
|
+
logger.info(f"Session allocated: {session_id} (user: {user_id})")
|
115
|
+
return session_id
|
116
|
+
|
117
|
+
async def validate_session(self, session_id: str) -> bool:
|
118
|
+
"""Check if session is valid."""
|
119
|
+
try:
|
120
|
+
metadata = await self._get_session_metadata(session_id)
|
121
|
+
if metadata and not metadata.is_expired():
|
122
|
+
await self._touch_session(session_id)
|
123
|
+
return True
|
124
|
+
return False
|
125
|
+
except Exception:
|
126
|
+
return False
|
127
|
+
|
128
|
+
async def get_session_info(self, session_id: str) -> Optional[Dict[str, Any]]:
|
129
|
+
"""Get session information."""
|
130
|
+
metadata = await self._get_session_metadata(session_id)
|
131
|
+
return metadata.to_dict() if metadata else None
|
132
|
+
|
133
|
+
def get_canonical_prefix(self, session_id: str) -> str:
|
134
|
+
"""Get grid path prefix."""
|
135
|
+
return f"grid/{self.sandbox_id}/{session_id}/"
|
136
|
+
|
137
|
+
def generate_artifact_key(self, session_id: str, artifact_id: str) -> str:
|
138
|
+
"""Generate grid artifact key."""
|
139
|
+
return f"grid/{self.sandbox_id}/{session_id}/{artifact_id}"
|
140
|
+
|
141
|
+
def _generate_session_id(self, user_id: Optional[str] = None) -> str:
|
142
|
+
"""Generate session ID."""
|
143
|
+
timestamp = int(time.time())
|
144
|
+
unique = uuid.uuid4().hex[:8]
|
145
|
+
|
146
|
+
if user_id:
|
147
|
+
safe_user = "".join(c for c in user_id if c.isalnum())[:8]
|
148
|
+
return f"sess-{safe_user}-{timestamp}-{unique}"
|
149
|
+
else:
|
150
|
+
return f"sess-{timestamp}-{unique}"
|
151
|
+
|
152
|
+
async def _get_session_metadata(self, session_id: str) -> Optional[SessionMetadata]:
|
153
|
+
"""Get session metadata."""
|
154
|
+
# Check cache
|
155
|
+
async with self._cache_lock:
|
156
|
+
if session_id in self._session_cache:
|
157
|
+
return self._session_cache[session_id]
|
158
|
+
|
159
|
+
# Query session provider
|
160
|
+
try:
|
161
|
+
session_ctx_mgr = self.session_factory()
|
162
|
+
async with session_ctx_mgr as session:
|
163
|
+
raw_data = await session.get(f"session:{session_id}")
|
164
|
+
if raw_data:
|
165
|
+
data = json.loads(raw_data)
|
166
|
+
metadata = SessionMetadata.from_dict(data)
|
167
|
+
|
168
|
+
# Cache it
|
169
|
+
async with self._cache_lock:
|
170
|
+
self._session_cache[session_id] = metadata
|
171
|
+
|
172
|
+
return metadata
|
173
|
+
except Exception as e:
|
174
|
+
logger.warning(f"Failed to get session {session_id}: {e}")
|
175
|
+
|
176
|
+
return None
|
177
|
+
|
178
|
+
async def _store_session_metadata(self, metadata: SessionMetadata) -> None:
|
179
|
+
"""Store session metadata."""
|
180
|
+
try:
|
181
|
+
session_ctx_mgr = self.session_factory()
|
182
|
+
async with session_ctx_mgr as session:
|
183
|
+
key = f"session:{metadata.session_id}"
|
184
|
+
ttl_seconds = int((datetime.fromisoformat(metadata.expires_at.replace("Z", "")) - datetime.utcnow()).total_seconds())
|
185
|
+
data = json.dumps(metadata.to_dict())
|
186
|
+
|
187
|
+
await session.setex(key, ttl_seconds, data)
|
188
|
+
except Exception as e:
|
189
|
+
raise SessionError(f"Session storage failed: {e}") from e
|
190
|
+
|
191
|
+
async def _touch_session(self, session_id: str) -> None:
|
192
|
+
"""Update last accessed time."""
|
193
|
+
metadata = await self._get_session_metadata(session_id)
|
194
|
+
if metadata:
|
195
|
+
# Simple touch - could update last_accessed if we add that field
|
196
|
+
await self._store_session_metadata(metadata)
|
@@ -1,8 +1,7 @@
|
|
1
1
|
# -*- coding: utf-8 -*-
|
2
|
-
# chuk_artifacts/session_operations.py
|
2
|
+
# chuk_artifacts/session/session_operations.py
|
3
3
|
"""
|
4
4
|
Session-based file operations with strict session isolation.
|
5
|
-
NO cross-session operations allowed for security.
|
6
5
|
"""
|
7
6
|
|
8
7
|
from __future__ import annotations
|
@@ -11,8 +10,8 @@ import uuid, hashlib, json, logging
|
|
11
10
|
from datetime import datetime
|
12
11
|
from typing import Any, Dict, Optional, Union, List
|
13
12
|
|
14
|
-
from
|
15
|
-
from
|
13
|
+
from ..base import BaseOperations
|
14
|
+
from ..exceptions import (
|
16
15
|
ArtifactStoreError, ArtifactNotFoundError, ArtifactExpiredError,
|
17
16
|
ProviderError, SessionError
|
18
17
|
)
|
@@ -36,27 +35,6 @@ class SessionOperations(BaseOperations):
|
|
36
35
|
) -> Dict[str, Any]:
|
37
36
|
"""
|
38
37
|
Move a file within the SAME session or rename it.
|
39
|
-
|
40
|
-
Parameters
|
41
|
-
----------
|
42
|
-
artifact_id : str
|
43
|
-
Source artifact ID
|
44
|
-
new_filename : str, optional
|
45
|
-
New filename (renames the file)
|
46
|
-
new_session_id : str, optional
|
47
|
-
Target session (BLOCKED - always raises error if different)
|
48
|
-
new_meta : dict, optional
|
49
|
-
Additional metadata to merge
|
50
|
-
|
51
|
-
Returns
|
52
|
-
-------
|
53
|
-
dict
|
54
|
-
Updated artifact metadata
|
55
|
-
|
56
|
-
Raises
|
57
|
-
------
|
58
|
-
ArtifactStoreError
|
59
|
-
If trying to move across sessions (always blocked)
|
60
38
|
"""
|
61
39
|
self._check_closed()
|
62
40
|
|
@@ -86,7 +64,7 @@ class SessionOperations(BaseOperations):
|
|
86
64
|
|
87
65
|
if updates:
|
88
66
|
# Use the metadata operations to update
|
89
|
-
from
|
67
|
+
from ..metadata import MetadataOperations
|
90
68
|
metadata_ops = MetadataOperations(self._artifact_store)
|
91
69
|
return await metadata_ops.update_metadata(artifact_id, **updates)
|
92
70
|
|
@@ -96,12 +74,14 @@ class SessionOperations(BaseOperations):
|
|
96
74
|
raise
|
97
75
|
except Exception as e:
|
98
76
|
logger.error(
|
99
|
-
"File move failed",
|
77
|
+
"File move failed for artifact %s: %s",
|
78
|
+
artifact_id,
|
79
|
+
str(e),
|
100
80
|
extra={
|
101
81
|
"artifact_id": artifact_id,
|
102
|
-
"
|
82
|
+
"new_file_name": new_filename, # FIXED: Renamed from 'new_filename'
|
103
83
|
"new_session_id": new_session_id,
|
104
|
-
"
|
84
|
+
"operation": "move_file"
|
105
85
|
}
|
106
86
|
)
|
107
87
|
raise ProviderError(f"Move operation failed: {e}") from e
|
@@ -117,29 +97,6 @@ class SessionOperations(BaseOperations):
|
|
117
97
|
) -> str:
|
118
98
|
"""
|
119
99
|
Copy a file WITHIN THE SAME SESSION only.
|
120
|
-
|
121
|
-
Parameters
|
122
|
-
----------
|
123
|
-
artifact_id : str
|
124
|
-
Source artifact ID
|
125
|
-
new_filename : str, optional
|
126
|
-
Filename for the copy (defaults to original + "_copy")
|
127
|
-
target_session_id : str, optional
|
128
|
-
Target session (BLOCKED - must be same as source session)
|
129
|
-
new_meta : dict, optional
|
130
|
-
Additional metadata to merge
|
131
|
-
summary : str, optional
|
132
|
-
New summary for the copy
|
133
|
-
|
134
|
-
Returns
|
135
|
-
-------
|
136
|
-
str
|
137
|
-
New artifact ID of the copy
|
138
|
-
|
139
|
-
Raises
|
140
|
-
------
|
141
|
-
ArtifactStoreError
|
142
|
-
If trying to copy across sessions (always blocked)
|
143
100
|
"""
|
144
101
|
self._check_closed()
|
145
102
|
|
@@ -180,7 +137,7 @@ class SessionOperations(BaseOperations):
|
|
180
137
|
copy_meta["copy_within_session"] = original_session
|
181
138
|
|
182
139
|
# Store the copy using core operations
|
183
|
-
from
|
140
|
+
from ..core import CoreStorageOperations
|
184
141
|
core_ops = CoreStorageOperations(self._artifact_store)
|
185
142
|
|
186
143
|
new_artifact_id = await core_ops.store(
|
@@ -193,12 +150,15 @@ class SessionOperations(BaseOperations):
|
|
193
150
|
)
|
194
151
|
|
195
152
|
logger.info(
|
196
|
-
"File copied within session",
|
153
|
+
"File copied within session: %s -> %s",
|
154
|
+
artifact_id,
|
155
|
+
new_artifact_id,
|
197
156
|
extra={
|
198
157
|
"source_artifact_id": artifact_id,
|
199
158
|
"new_artifact_id": new_artifact_id,
|
200
159
|
"session": copy_session,
|
201
|
-
"security_level": "same_session_only"
|
160
|
+
"security_level": "same_session_only",
|
161
|
+
"operation": "copy_file"
|
202
162
|
}
|
203
163
|
)
|
204
164
|
|
@@ -208,12 +168,14 @@ class SessionOperations(BaseOperations):
|
|
208
168
|
raise
|
209
169
|
except Exception as e:
|
210
170
|
logger.error(
|
211
|
-
"File copy failed",
|
171
|
+
"File copy failed for artifact %s: %s",
|
172
|
+
artifact_id,
|
173
|
+
str(e),
|
212
174
|
extra={
|
213
175
|
"artifact_id": artifact_id,
|
214
|
-
"
|
176
|
+
"new_file_name": new_filename, # FIXED: Renamed from 'new_filename'
|
215
177
|
"target_session_id": target_session_id,
|
216
|
-
"
|
178
|
+
"operation": "copy_file"
|
217
179
|
}
|
218
180
|
)
|
219
181
|
raise ProviderError(f"Copy operation failed: {e}") from e
|
@@ -227,9 +189,6 @@ class SessionOperations(BaseOperations):
|
|
227
189
|
) -> Union[str, bytes]:
|
228
190
|
"""
|
229
191
|
Read file content directly.
|
230
|
-
|
231
|
-
Note: This operation inherently respects session boundaries since
|
232
|
-
you can only read files you have artifact IDs for.
|
233
192
|
"""
|
234
193
|
self._check_closed()
|
235
194
|
|
@@ -249,8 +208,10 @@ class SessionOperations(BaseOperations):
|
|
249
208
|
raise
|
250
209
|
except Exception as e:
|
251
210
|
logger.error(
|
252
|
-
"File read failed",
|
253
|
-
|
211
|
+
"File read failed for artifact %s: %s",
|
212
|
+
artifact_id,
|
213
|
+
str(e),
|
214
|
+
extra={"artifact_id": artifact_id, "operation": "read_file"}
|
254
215
|
)
|
255
216
|
raise ProviderError(f"Read operation failed: {e}") from e
|
256
217
|
|
@@ -268,35 +229,6 @@ class SessionOperations(BaseOperations):
|
|
268
229
|
) -> str:
|
269
230
|
"""
|
270
231
|
Write content to a new file or overwrite existing WITHIN THE SAME SESSION.
|
271
|
-
|
272
|
-
Parameters
|
273
|
-
----------
|
274
|
-
content : str or bytes
|
275
|
-
Content to write
|
276
|
-
filename : str
|
277
|
-
Filename for the new file
|
278
|
-
mime : str, optional
|
279
|
-
MIME type (default: text/plain)
|
280
|
-
summary : str, optional
|
281
|
-
File summary
|
282
|
-
session_id : str, optional
|
283
|
-
Session for the file
|
284
|
-
meta : dict, optional
|
285
|
-
Additional metadata
|
286
|
-
encoding : str, optional
|
287
|
-
Text encoding for string content (default: utf-8)
|
288
|
-
overwrite_artifact_id : str, optional
|
289
|
-
If provided, overwrite this existing artifact (must be in same session)
|
290
|
-
|
291
|
-
Returns
|
292
|
-
-------
|
293
|
-
str
|
294
|
-
Artifact ID of the written file
|
295
|
-
|
296
|
-
Raises
|
297
|
-
------
|
298
|
-
ArtifactStoreError
|
299
|
-
If trying to overwrite a file in a different session
|
300
232
|
"""
|
301
233
|
self._check_closed()
|
302
234
|
|
@@ -325,7 +257,7 @@ class SessionOperations(BaseOperations):
|
|
325
257
|
session_id = session_id or existing_session
|
326
258
|
|
327
259
|
# Delete old version (within same session)
|
328
|
-
from
|
260
|
+
from ..metadata import MetadataOperations
|
329
261
|
metadata_ops = MetadataOperations(self._artifact_store)
|
330
262
|
await metadata_ops.delete(overwrite_artifact_id)
|
331
263
|
|
@@ -333,7 +265,7 @@ class SessionOperations(BaseOperations):
|
|
333
265
|
pass # Original doesn't exist, proceed with new creation
|
334
266
|
|
335
267
|
# Store new content using core operations
|
336
|
-
from
|
268
|
+
from ..core import CoreStorageOperations
|
337
269
|
core_ops = CoreStorageOperations(self._artifact_store)
|
338
270
|
|
339
271
|
write_meta = {**(meta or {})}
|
@@ -351,28 +283,35 @@ class SessionOperations(BaseOperations):
|
|
351
283
|
meta=write_meta
|
352
284
|
)
|
353
285
|
|
286
|
+
# FIXED: Use separate variables for logging to avoid 'filename' conflict
|
354
287
|
logger.info(
|
355
|
-
"File written successfully",
|
288
|
+
"File written successfully: %s (artifact_id: %s)",
|
289
|
+
filename,
|
290
|
+
artifact_id,
|
356
291
|
extra={
|
357
292
|
"artifact_id": artifact_id,
|
358
|
-
"
|
293
|
+
"file_name": filename, # FIXED: Renamed from 'filename'
|
359
294
|
"bytes": len(data),
|
360
295
|
"overwrite": bool(overwrite_artifact_id),
|
361
296
|
"session_id": session_id,
|
362
|
-
"security_level": "session_isolated"
|
297
|
+
"security_level": "session_isolated",
|
298
|
+
"operation": "write_file"
|
363
299
|
}
|
364
300
|
)
|
365
301
|
|
366
302
|
return artifact_id
|
367
303
|
|
368
304
|
except Exception as e:
|
305
|
+
# FIXED: Use separate variables for logging to avoid 'filename' conflict
|
369
306
|
logger.error(
|
370
|
-
"File write failed",
|
307
|
+
"File write failed for %s: %s",
|
308
|
+
filename,
|
309
|
+
str(e),
|
371
310
|
extra={
|
372
|
-
"
|
311
|
+
"file_name": filename, # FIXED: Renamed from 'filename'
|
373
312
|
"overwrite_artifact_id": overwrite_artifact_id,
|
374
313
|
"session_id": session_id,
|
375
|
-
"
|
314
|
+
"operation": "write_file"
|
376
315
|
}
|
377
316
|
)
|
378
317
|
raise ProviderError(f"Write operation failed: {e}") from e
|
@@ -385,52 +324,34 @@ class SessionOperations(BaseOperations):
|
|
385
324
|
) -> List[Dict[str, Any]]:
|
386
325
|
"""
|
387
326
|
List files in a directory-like structure within a session.
|
388
|
-
|
389
|
-
This operation is inherently session-safe since it requires
|
390
|
-
explicit session_id parameter.
|
391
327
|
"""
|
392
328
|
try:
|
393
|
-
from
|
329
|
+
from ..metadata import MetadataOperations
|
394
330
|
metadata_ops = MetadataOperations(self._artifact_store)
|
395
331
|
return await metadata_ops.list_by_prefix(session_id, directory_prefix, limit)
|
396
332
|
except Exception as e:
|
397
333
|
logger.error(
|
398
|
-
"Directory listing failed",
|
334
|
+
"Directory listing failed for session %s: %s",
|
335
|
+
session_id,
|
336
|
+
str(e),
|
399
337
|
extra={
|
400
338
|
"session_id": session_id,
|
401
339
|
"directory_prefix": directory_prefix,
|
402
|
-
"
|
340
|
+
"operation": "get_directory_contents"
|
403
341
|
}
|
404
342
|
)
|
405
343
|
raise ProviderError(f"Directory listing failed: {e}") from e
|
406
344
|
|
407
345
|
async def _retrieve_data(self, artifact_id: str) -> bytes:
|
408
346
|
"""Helper to retrieve artifact data using core operations."""
|
409
|
-
from
|
347
|
+
from ..core import CoreStorageOperations
|
410
348
|
core_ops = CoreStorageOperations(self._artifact_store)
|
411
349
|
return await core_ops.retrieve(artifact_id)
|
412
350
|
|
413
|
-
#
|
351
|
+
# Session security validation helper
|
414
352
|
async def _validate_session_access(self, artifact_id: str, expected_session_id: str = None) -> Dict[str, Any]:
|
415
353
|
"""
|
416
354
|
Validate that an artifact belongs to the expected session.
|
417
|
-
|
418
|
-
Parameters
|
419
|
-
----------
|
420
|
-
artifact_id : str
|
421
|
-
Artifact to check
|
422
|
-
expected_session_id : str, optional
|
423
|
-
Expected session ID (if None, just returns the session)
|
424
|
-
|
425
|
-
Returns
|
426
|
-
-------
|
427
|
-
dict
|
428
|
-
Artifact metadata
|
429
|
-
|
430
|
-
Raises
|
431
|
-
------
|
432
|
-
ArtifactStoreError
|
433
|
-
If artifact belongs to different session
|
434
355
|
"""
|
435
356
|
record = await self._get_record(artifact_id)
|
436
357
|
actual_session = record.get("session_id")
|