Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/praisonai/praisonai/bots/_unknown_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ async def _handle_pairing_request(
owner_user_id=owner_user_id,
user_name=user_name,
code=code,
channel=channel,
channel=channel_type,
user_id=user_id
)

Expand Down
12 changes: 10 additions & 2 deletions src/praisonai/praisonai/cli/session/unified.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,12 @@ def _get_last_session_path(self) -> Path:
def _acquire_exclusive_lock(self, file_obj) -> None:
if sys.platform == "win32":
import msvcrt
# Lock entire file by using file size (or large value for empty files)
file_obj.seek(0, os.SEEK_END)
file_size = file_obj.tell()
lock_length = max(file_size, 1)
file_obj.seek(0)
msvcrt.locking(file_obj.fileno(), msvcrt.LK_LOCK, 1)
msvcrt.locking(file_obj.fileno(), msvcrt.LK_LOCK, lock_length)
elif _HAS_FCNTL:
fcntl.flock(file_obj.fileno(), fcntl.LOCK_EX)
else:
Expand All @@ -198,8 +202,12 @@ def _acquire_exclusive_lock(self, file_obj) -> None:
def _release_exclusive_lock(self, file_obj) -> None:
if sys.platform == "win32":
import msvcrt
# Use the same lock length as acquisition
file_obj.seek(0, os.SEEK_END)
file_size = file_obj.tell()
lock_length = max(file_size, 1)
file_obj.seek(0)
msvcrt.locking(file_obj.fileno(), msvcrt.LK_UNLCK, 1)
msvcrt.locking(file_obj.fileno(), msvcrt.LK_UNLCK, lock_length)
elif _HAS_FCNTL:
fcntl.flock(file_obj.fileno(), fcntl.LOCK_UN)
Comment on lines 182 to 212

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Windows unlock range mismatch after file resize

_release_exclusive_lock recomputes lock_length from the file's current (post-write) size, but _acquire_exclusive_lock locked the file's pre-write size. msvcrt.locking(LK_UNLCK, n) must receive the exact same byte-count that was passed to LK_LOCK; any mismatch raises IOError: [Errno 13] Permission denied, leaving the file permanently locked for the lifetime of the process. This will reliably trigger whenever _write_json_locked changes the JSON payload size (virtually every merge/save cycle).

Suggested change
# Large constant that covers any realistic session-file size; both
# acquire and release must use the identical value so that
# msvcrt.locking(LK_UNLCK) matches the locked region exactly.
_WIN32_LOCK_LENGTH = 1 << 30 # 1 GiB
def _acquire_exclusive_lock(self, file_obj) -> None:
if sys.platform == "win32":
import msvcrt
file_obj.seek(0)
msvcrt.locking(file_obj.fileno(), msvcrt.LK_LOCK, self._WIN32_LOCK_LENGTH)
elif _HAS_FCNTL:
fcntl.flock(file_obj.fileno(), fcntl.LOCK_EX)
else:
global _WARNED_NO_FCNTL
if not _WARNED_NO_FCNTL:
logger.warning(
"File locking unavailable on this platform (fcntl not available); "
"concurrent writers may corrupt session files."
)
_WARNED_NO_FCNTL = True
def _release_exclusive_lock(self, file_obj) -> None:
if sys.platform == "win32":
import msvcrt
file_obj.seek(0)
msvcrt.locking(file_obj.fileno(), msvcrt.LK_UNLCK, self._WIN32_LOCK_LENGTH)
elif _HAS_FCNTL:
fcntl.flock(file_obj.fileno(), fcntl.LOCK_UN)


Expand Down
10 changes: 5 additions & 5 deletions src/praisonai/tests/integration/bots/test_pairing_agent_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,12 +117,12 @@ async def test_unknown_dm_to_real_agent_after_owner_approval(self):
# Set callback secret via environment variable for consistent signatures
os.environ["PRAISONAI_CALLBACK_SECRET"] = "test-secret-for-e2e"

# Simulate owner approval using real signed callback
# Simulate owner approval using callback payload from the approval DM
keyboard = PairingUIBuilder.create_telegram_keyboard(
user_name="TestUser",
user_name=approval_dm["user_name"],
code=code,
channel="telegram",
user_id="new-user-1",
channel=approval_dm["channel"],
user_id=approval_dm["user_id"],
)
callback_data = keyboard["inline_keyboard"][0][0]["callback_data"] # Get approve button callback

Expand Down Expand Up @@ -191,7 +191,7 @@ async def test_pairing_flow_with_different_channels(self):
assert len(self.adapter.approval_dms) == 1

approval_dm = self.adapter.approval_dms[0]
assert approval_dm["channel"] == "discord-dm-456"
assert approval_dm["channel"] == "discord"
assert approval_dm["user_id"] == "discord-user-1"

@pytest.mark.asyncio
Expand Down
10 changes: 5 additions & 5 deletions src/praisonai/tests/integration/bots/test_pairing_owner_dm.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ async def test_unknown_user_triggers_pairing_request(self):
approval_dm = self.adapter.approval_dms[0]
assert approval_dm["to"] == "owner-123"
assert approval_dm["user_name"] == "Alice"
assert approval_dm["channel"] == "dm-123"
assert approval_dm["channel"] == "telegram"
assert approval_dm["user_id"] == "new-user"

# Should notify user that request was sent
Expand All @@ -128,12 +128,12 @@ async def test_owner_approval_allows_future_messages(self):
approval_dm = self.adapter.approval_dms[0]
code = approval_dm["code"]

# Simulate owner approval using real signed callback
# Simulate owner approval using callback payload from the approval DM
keyboard = PairingUIBuilder.create_telegram_keyboard(
user_name="Alice",
user_name=approval_dm["user_name"],
code=code,
channel="telegram",
user_id="new-user",
channel=approval_dm["channel"],
user_id=approval_dm["user_id"],
)
callback_data = keyboard["inline_keyboard"][0][0]["callback_data"] # Get approve button callback

Expand Down
Loading