portacode 1.3.30__tar.gz → 1.3.32__tar.gz
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.
Potentially problematic release.
This version of portacode might be problematic. Click here for more details.
- {portacode-1.3.30 → portacode-1.3.32}/.claude/settings.local.json +3 -2
- {portacode-1.3.30 → portacode-1.3.32}/PKG-INFO +1 -1
- {portacode-1.3.30 → portacode-1.3.32}/portacode/_version.py +2 -2
- {portacode-1.3.30 → portacode-1.3.32}/portacode/connection/handlers/project_state/file_system_watcher.py +24 -10
- {portacode-1.3.30 → portacode-1.3.32}/portacode/connection/handlers/project_state/git_manager.py +11 -3
- {portacode-1.3.30 → portacode-1.3.32}/portacode/connection/handlers/project_state/handlers.py +9 -9
- {portacode-1.3.30 → portacode-1.3.32}/portacode/connection/handlers/project_state/manager.py +25 -11
- {portacode-1.3.30 → portacode-1.3.32}/portacode.egg-info/PKG-INFO +1 -1
- {portacode-1.3.30 → portacode-1.3.32}/portacode.egg-info/SOURCES.txt +4 -0
- portacode-1.3.32/todo/agent_context_management.md +12 -0
- portacode-1.3.32/todo/issues/device_performance_degradation.md +129 -0
- portacode-1.3.32/todo/issues/git_data_not_captured_in_proxmox.md +2004 -0
- portacode-1.3.32/todo/issues/terminals_exit_upon_starting.md +3 -0
- {portacode-1.3.30 → portacode-1.3.32}/.claude/agents/communication-manager.md +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/.gitignore +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/.gitmodules +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/LICENSE +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/MANIFEST.in +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/Makefile +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/README.md +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/backup.sh +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/connect.py +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/connect.sh +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/docker-compose.yaml +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/portacode/README.md +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/portacode/__init__.py +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/portacode/__main__.py +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/portacode/cli.py +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/portacode/connection/README.md +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/portacode/connection/__init__.py +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/portacode/connection/client.py +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/portacode/connection/handlers/README.md +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/portacode/connection/handlers/WEBSOCKET_PROTOCOL.md +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/portacode/connection/handlers/__init__.py +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/portacode/connection/handlers/base.py +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/portacode/connection/handlers/chunked_content.py +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/portacode/connection/handlers/file_handlers.py +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/portacode/connection/handlers/project_aware_file_handlers.py +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/portacode/connection/handlers/project_state/README.md +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/portacode/connection/handlers/project_state/__init__.py +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/portacode/connection/handlers/project_state/models.py +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/portacode/connection/handlers/project_state/utils.py +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/portacode/connection/handlers/project_state_handlers.py +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/portacode/connection/handlers/registry.py +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/portacode/connection/handlers/session.py +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/portacode/connection/handlers/system_handlers.py +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/portacode/connection/handlers/tab_factory.py +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/portacode/connection/handlers/terminal_handlers.py +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/portacode/connection/multiplex.py +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/portacode/connection/terminal.py +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/portacode/data.py +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/portacode/keypair.py +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/portacode/logging_categories.py +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/portacode/service.py +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/portacode/static/js/test-ntp-clock.html +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/portacode/static/js/utils/ntp-clock.js +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/portacode/utils/NTP_ARCHITECTURE.md +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/portacode/utils/__init__.py +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/portacode/utils/ntp_clock.py +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/portacode.egg-info/dependency_links.txt +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/portacode.egg-info/entry_points.txt +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/portacode.egg-info/requires.txt +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/portacode.egg-info/top_level.txt +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/pyproject.toml +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/restore.sh +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/run_tests.py +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/setup.cfg +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/setup.py +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/test.sh +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/test_modules/README.md +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/test_modules/__init__.py +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/test_modules/test_device_online.py +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/test_modules/test_file_operations.py +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/test_modules/test_git_status_ui.py +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/test_modules/test_login_flow.py +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/test_modules/test_navigate_testing_folder.py +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/test_modules/test_terminal_buffer_performance.py +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/test_modules/test_terminal_interaction.py +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/test_modules/test_terminal_loading_race_condition.py +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/test_modules/test_terminal_start.py +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/test_request_id.py +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/testing_framework/.env.example +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/testing_framework/README.md +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/testing_framework/__init__.py +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/testing_framework/cli.py +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/testing_framework/core/__init__.py +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/testing_framework/core/base_test.py +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/testing_framework/core/cli_manager.py +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/testing_framework/core/hierarchical_runner.py +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/testing_framework/core/playwright_manager.py +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/testing_framework/core/runner.py +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/testing_framework/core/shared_cli_manager.py +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/testing_framework/core/test_discovery.py +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/testing_framework/requirements.txt +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/todo/issues/indefinite_resource_loading.md +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/todo/issues/premature_terminal_exit.md +0 -0
- {portacode-1.3.30 → portacode-1.3.32}/tools/test_python_ntp_clock.py +0 -0
|
@@ -15,8 +15,9 @@
|
|
|
15
15
|
"Bash(mkdir:*)",
|
|
16
16
|
"Bash(./connect.sh)",
|
|
17
17
|
"Bash(git -C /home/menas/testing_folder status --porcelain)",
|
|
18
|
-
"Bash(docker-compose restart:*)"
|
|
18
|
+
"Bash(docker-compose restart:*)",
|
|
19
|
+
"Bash(./debug/list_user_devices_and_projects.sh:*)"
|
|
19
20
|
],
|
|
20
21
|
"deny": []
|
|
21
22
|
}
|
|
22
|
-
}
|
|
23
|
+
}
|
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '1.3.
|
|
32
|
-
__version_tuple__ = version_tuple = (1, 3,
|
|
31
|
+
__version__ = version = '1.3.32'
|
|
32
|
+
__version_tuple__ = version_tuple = (1, 3, 32)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
|
@@ -28,12 +28,13 @@ except ImportError:
|
|
|
28
28
|
|
|
29
29
|
class FileSystemWatcher:
|
|
30
30
|
"""Watches file system changes for project folders."""
|
|
31
|
-
|
|
31
|
+
|
|
32
32
|
def __init__(self, project_manager):
|
|
33
33
|
self.project_manager = project_manager # Reference to ProjectStateManager
|
|
34
34
|
self.observer: Optional[Observer] = None
|
|
35
35
|
self.event_handler: Optional[FileSystemEventHandler] = None
|
|
36
36
|
self.watched_paths: Set[str] = set()
|
|
37
|
+
self.watch_handles: dict = {} # Map path -> watch handle for proper cleanup
|
|
37
38
|
# Store reference to the event loop for thread-safe async task creation
|
|
38
39
|
try:
|
|
39
40
|
self.event_loop = asyncio.get_running_loop()
|
|
@@ -140,14 +141,15 @@ class FileSystemWatcher:
|
|
|
140
141
|
if not WATCHDOG_AVAILABLE or not self.observer:
|
|
141
142
|
logger.warning("Watchdog not available, cannot start watching: %s", path)
|
|
142
143
|
return
|
|
143
|
-
|
|
144
|
+
|
|
144
145
|
if path not in self.watched_paths:
|
|
145
146
|
try:
|
|
146
147
|
# Use recursive=False to watch only direct contents of each folder
|
|
147
|
-
self.observer.schedule(self.event_handler, path, recursive=False)
|
|
148
|
+
watch_handle = self.observer.schedule(self.event_handler, path, recursive=False)
|
|
148
149
|
self.watched_paths.add(path)
|
|
150
|
+
self.watch_handles[path] = watch_handle # Store handle for cleanup
|
|
149
151
|
logger.info("Started watching path (non-recursive): %s", path)
|
|
150
|
-
|
|
152
|
+
|
|
151
153
|
if not self.observer.is_alive():
|
|
152
154
|
self.observer.start()
|
|
153
155
|
logger.info("Started file system observer")
|
|
@@ -161,14 +163,15 @@ class FileSystemWatcher:
|
|
|
161
163
|
if not WATCHDOG_AVAILABLE or not self.observer:
|
|
162
164
|
logger.warning("Watchdog not available, cannot start watching git directory: %s", git_path)
|
|
163
165
|
return
|
|
164
|
-
|
|
166
|
+
|
|
165
167
|
if git_path not in self.watched_paths:
|
|
166
168
|
try:
|
|
167
169
|
# Watch .git directory recursively to catch changes in refs/, logs/, etc.
|
|
168
|
-
self.observer.schedule(self.event_handler, git_path, recursive=True)
|
|
170
|
+
watch_handle = self.observer.schedule(self.event_handler, git_path, recursive=True)
|
|
169
171
|
self.watched_paths.add(git_path)
|
|
172
|
+
self.watch_handles[git_path] = watch_handle # Store handle for cleanup
|
|
170
173
|
logger.info("Started watching git directory (recursive): %s", git_path)
|
|
171
|
-
|
|
174
|
+
|
|
172
175
|
if not self.observer.is_alive():
|
|
173
176
|
self.observer.start()
|
|
174
177
|
logger.info("Started file system observer")
|
|
@@ -181,9 +184,19 @@ class FileSystemWatcher:
|
|
|
181
184
|
"""Stop watching a specific path."""
|
|
182
185
|
if not WATCHDOG_AVAILABLE or not self.observer:
|
|
183
186
|
return
|
|
184
|
-
|
|
187
|
+
|
|
185
188
|
if path in self.watched_paths:
|
|
186
|
-
#
|
|
189
|
+
# Actually unschedule the watch using stored handle
|
|
190
|
+
watch_handle = self.watch_handles.get(path)
|
|
191
|
+
if watch_handle:
|
|
192
|
+
try:
|
|
193
|
+
self.observer.unschedule(watch_handle)
|
|
194
|
+
logger.info("Successfully unscheduled watch for: %s", path)
|
|
195
|
+
except Exception as e:
|
|
196
|
+
logger.error("Error unscheduling watch for %s: %s", path, e)
|
|
197
|
+
finally:
|
|
198
|
+
self.watch_handles.pop(path, None)
|
|
199
|
+
|
|
187
200
|
self.watched_paths.discard(path)
|
|
188
201
|
logger.debug("Stopped watching path: %s", path)
|
|
189
202
|
|
|
@@ -192,4 +205,5 @@ class FileSystemWatcher:
|
|
|
192
205
|
if self.observer and self.observer.is_alive():
|
|
193
206
|
self.observer.stop()
|
|
194
207
|
self.observer.join()
|
|
195
|
-
self.watched_paths.clear()
|
|
208
|
+
self.watched_paths.clear()
|
|
209
|
+
self.watch_handles.clear()
|
{portacode-1.3.30 → portacode-1.3.32}/portacode/connection/handlers/project_state/git_manager.py
RENAMED
|
@@ -350,9 +350,18 @@ class GitManager:
|
|
|
350
350
|
status_output = self.repo.git.status(*rel_paths, porcelain=True)
|
|
351
351
|
if status_output.strip():
|
|
352
352
|
for line in status_output.strip().split('\n'):
|
|
353
|
+
# Git porcelain format: XY path (X=index, Y=worktree, then space, then path)
|
|
354
|
+
# Some files may have renamed format: XY path -> new_path
|
|
353
355
|
if len(line) >= 3:
|
|
354
|
-
|
|
355
|
-
|
|
356
|
+
# Skip first 3 characters (2 status + 1 space) to get the file path
|
|
357
|
+
# But git uses exactly 2 chars for status then space, so position 3 onwards is path
|
|
358
|
+
parts = line.split(None, 1) # Split on first whitespace to separate status from path
|
|
359
|
+
if len(parts) >= 2:
|
|
360
|
+
file_path_from_status = parts[1]
|
|
361
|
+
# Handle renames (format: "old_path -> new_path")
|
|
362
|
+
if ' -> ' in file_path_from_status:
|
|
363
|
+
file_path_from_status = file_path_from_status.split(' -> ')[1]
|
|
364
|
+
status_map[file_path_from_status] = line
|
|
356
365
|
except Exception as e:
|
|
357
366
|
logger.debug("Error getting batch status: %s", e)
|
|
358
367
|
|
|
@@ -469,7 +478,6 @@ class GitManager:
|
|
|
469
478
|
elif index_status == 'D' or worktree_status == 'D':
|
|
470
479
|
has_deleted = True
|
|
471
480
|
|
|
472
|
-
# Priority order: untracked > modified/deleted > clean
|
|
473
481
|
if has_untracked:
|
|
474
482
|
return {"is_tracked": False, "status": "untracked", "is_ignored": False, "is_staged": is_staged}
|
|
475
483
|
elif has_deleted:
|
{portacode-1.3.30 → portacode-1.3.32}/portacode/connection/handlers/project_state/handlers.py
RENAMED
|
@@ -408,10 +408,10 @@ class ProjectStateGitStageHandler(AsyncHandler):
|
|
|
408
408
|
success = git_manager.stage_file(file_paths_to_stage[0])
|
|
409
409
|
else:
|
|
410
410
|
success = git_manager.stage_files(file_paths_to_stage)
|
|
411
|
-
|
|
411
|
+
|
|
412
412
|
if success:
|
|
413
|
-
# Refresh
|
|
414
|
-
await manager._refresh_project_state(source_client_session)
|
|
413
|
+
# Refresh git status only (no filesystem changes from staging)
|
|
414
|
+
await manager._refresh_project_state(source_client_session, git_only=True)
|
|
415
415
|
|
|
416
416
|
# Build response
|
|
417
417
|
response = {
|
|
@@ -482,10 +482,10 @@ class ProjectStateGitUnstageHandler(AsyncHandler):
|
|
|
482
482
|
success = git_manager.unstage_file(file_paths_to_unstage[0])
|
|
483
483
|
else:
|
|
484
484
|
success = git_manager.unstage_files(file_paths_to_unstage)
|
|
485
|
-
|
|
485
|
+
|
|
486
486
|
if success:
|
|
487
|
-
# Refresh
|
|
488
|
-
await manager._refresh_project_state(source_client_session)
|
|
487
|
+
# Refresh git status only (no filesystem changes from unstaging)
|
|
488
|
+
await manager._refresh_project_state(source_client_session, git_only=True)
|
|
489
489
|
|
|
490
490
|
# Build response
|
|
491
491
|
response = {
|
|
@@ -620,9 +620,9 @@ class ProjectStateGitCommitHandler(AsyncHandler):
|
|
|
620
620
|
if success:
|
|
621
621
|
# Get the commit hash of the new commit
|
|
622
622
|
commit_hash = git_manager.get_head_commit_hash()
|
|
623
|
-
|
|
624
|
-
# Refresh
|
|
625
|
-
await manager._refresh_project_state(source_client_session)
|
|
623
|
+
|
|
624
|
+
# Refresh git status only (no filesystem changes from commit)
|
|
625
|
+
await manager._refresh_project_state(source_client_session, git_only=True)
|
|
626
626
|
except Exception as e:
|
|
627
627
|
error_message = str(e)
|
|
628
628
|
logger.error("Error during commit: %s", error_message)
|
{portacode-1.3.30 → portacode-1.3.32}/portacode/connection/handlers/project_state/manager.py
RENAMED
|
@@ -152,7 +152,8 @@ class ProjectStateManager:
|
|
|
152
152
|
async def git_change_callback():
|
|
153
153
|
"""Callback when git status changes are detected."""
|
|
154
154
|
logger.debug("Git change detected, refreshing project state for %s", client_session_id)
|
|
155
|
-
|
|
155
|
+
# Git directory changes only affect git status, not filesystem
|
|
156
|
+
await self._refresh_project_state(client_session_id, git_only=True)
|
|
156
157
|
|
|
157
158
|
git_manager = GitManager(project_folder_path, change_callback=git_change_callback)
|
|
158
159
|
self.git_managers[client_session_id] = git_manager
|
|
@@ -877,9 +878,17 @@ class ProjectStateManager:
|
|
|
877
878
|
self._pending_changes.clear()
|
|
878
879
|
logger.debug("🔍 [TRACE] ✅ Finished processing file changes")
|
|
879
880
|
|
|
880
|
-
async def _refresh_project_state(self, client_session_id: str):
|
|
881
|
-
"""Refresh project state after file changes.
|
|
882
|
-
|
|
881
|
+
async def _refresh_project_state(self, client_session_id: str, git_only: bool = False):
|
|
882
|
+
"""Refresh project state after file changes.
|
|
883
|
+
|
|
884
|
+
Args:
|
|
885
|
+
client_session_id: The client session ID
|
|
886
|
+
git_only: If True, only git status changed (skip filesystem operations like
|
|
887
|
+
detecting new directories and syncing file state). Use this for
|
|
888
|
+
git operations (stage, unstage, revert) to avoid unnecessary work.
|
|
889
|
+
"""
|
|
890
|
+
logger.debug("🔍 [TRACE] _refresh_project_state called for session: %s (git_only=%s)",
|
|
891
|
+
client_session_id, git_only)
|
|
883
892
|
|
|
884
893
|
if client_session_id not in self.projects:
|
|
885
894
|
logger.debug("🔍 [TRACE] ❌ Session not found in projects: %s", client_session_id)
|
|
@@ -930,15 +939,20 @@ class ProjectStateManager:
|
|
|
930
939
|
old_status_summary, project_state.git_status_summary)
|
|
931
940
|
else:
|
|
932
941
|
logger.debug("🔍 [TRACE] ❌ No git manager found for session: %s", client_session_id)
|
|
933
|
-
|
|
934
|
-
#
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
942
|
+
|
|
943
|
+
# For git-only operations, skip scanning for new directories
|
|
944
|
+
# but still sync items to update git attributes for UI
|
|
945
|
+
if not git_only:
|
|
946
|
+
# Detect and add new directories in expanded folders before syncing
|
|
947
|
+
logger.debug("🔍 [TRACE] Detecting and adding new directories...")
|
|
948
|
+
await self._detect_and_add_new_directories(project_state)
|
|
949
|
+
else:
|
|
950
|
+
logger.debug("🔍 [TRACE] Skipping directory detection (git_only=True)")
|
|
951
|
+
|
|
952
|
+
# Always sync state to update git attributes on items (needed for UI updates)
|
|
939
953
|
logger.debug("🔍 [TRACE] Syncing all state with monitored folders...")
|
|
940
954
|
await self._sync_all_state_with_monitored_folders(project_state)
|
|
941
|
-
|
|
955
|
+
|
|
942
956
|
# Send update to clients
|
|
943
957
|
logger.debug("🔍 [TRACE] About to send project state update...")
|
|
944
958
|
await self._send_project_state_update(project_state)
|
|
@@ -86,6 +86,10 @@ testing_framework/core/playwright_manager.py
|
|
|
86
86
|
testing_framework/core/runner.py
|
|
87
87
|
testing_framework/core/shared_cli_manager.py
|
|
88
88
|
testing_framework/core/test_discovery.py
|
|
89
|
+
todo/agent_context_management.md
|
|
90
|
+
todo/issues/device_performance_degradation.md
|
|
91
|
+
todo/issues/git_data_not_captured_in_proxmox.md
|
|
89
92
|
todo/issues/indefinite_resource_loading.md
|
|
90
93
|
todo/issues/premature_terminal_exit.md
|
|
94
|
+
todo/issues/terminals_exit_upon_starting.md
|
|
91
95
|
tools/test_python_ntp_clock.py
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Agent Context Management
|
|
2
|
+
|
|
3
|
+
This document describes how the unibot portacode agent is expected to access contextual information to complete show awerness of user data. It is in the form of phases to be implemented one by one.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# Phase one: Project selection
|
|
7
|
+
|
|
8
|
+
- Each account object should carry some state data, which needs to include the fields "Selected Project": project UUID (can be null and default is null)
|
|
9
|
+
- A user can only select a project if they are authorized to access that project. And by default, no pojects are selected.
|
|
10
|
+
- project selection data should be accessible through the Account model
|
|
11
|
+
- the agent/bot needs to be equipped with a tool to select/unselect projects
|
|
12
|
+
- The system message of the agent is affected by which project is currently selected. If non, in contains data similar to that in the dashboard page. If a project is selected, it shows data similar to that of the project page.
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Device Performance Degradation over time.
|
|
2
|
+
|
|
3
|
+
## **CONFIRMED ROOT CAUSE (2025-10-13)**
|
|
4
|
+
|
|
5
|
+
**Trigger: Opening/refreshing project workspace pages for git repositories**
|
|
6
|
+
|
|
7
|
+
Experimental findings:
|
|
8
|
+
- ✅ Terminal sessions have NO impact on performance (can run for days with heavy I/O)
|
|
9
|
+
- ✅ Project workspace pages WITHOUT .git folder: always fast, no degradation
|
|
10
|
+
- ❌ Project workspace pages WITH .git folder: **catastrophic degradation**
|
|
11
|
+
- 1st load: takes several seconds
|
|
12
|
+
- 2nd load: noticeably slower
|
|
13
|
+
- 5th load: barely responsive, device goes offline momentarily
|
|
14
|
+
|
|
15
|
+
**Diagnostic evidence from running service (PID 1891992, 16+ hours uptime):**
|
|
16
|
+
- 200 pipes (normally <20)
|
|
17
|
+
- 65 threads (normally 5-10)
|
|
18
|
+
- 91% sustained CPU usage
|
|
19
|
+
- Performance: 55ms → 12,418ms (224x slowdown)
|
|
20
|
+
|
|
21
|
+
**Primary culprit: Watchdog file system watcher + Git operations**
|
|
22
|
+
- file_system_watcher.py:186-188 - `stop_watching()` doesn't unschedule watches
|
|
23
|
+
- file_system_watcher.py:168 - .git directories watched **recursively**
|
|
24
|
+
- manager.py:904 - Every project state refresh adds .git watch
|
|
25
|
+
- Result: Orphaned watches accumulate, every file change triggers 100+ unnecessary handlers
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Original Issue Description
|
|
30
|
+
|
|
31
|
+
When we run "portacode connect" or "portacode service install" and the device gets connected, initially, the totall time elapsed from the moment a client session sends a command to the device till the device response event arrivs back to the client session is typically less than 100ms with an average of 55ms. However, when we start actively using the device, the device starts to slow down gradually until within just a couple of hours or so, it becomes so slow it takes more than 12 seconds! The trace looks something like this:
|
|
32
|
+
|
|
33
|
+
{
|
|
34
|
+
"client_send": 1760282723260.5,
|
|
35
|
+
"ping": 12418,
|
|
36
|
+
"server_receive": 1760282723264,
|
|
37
|
+
"server_send": 1760282723278,
|
|
38
|
+
"device_receive": 1760282727601,
|
|
39
|
+
"handler_receive": 1760282728420,
|
|
40
|
+
"handler_dispatch": 1760282728420,
|
|
41
|
+
"handler_complete": 1760282735611,
|
|
42
|
+
"device_send": 1760282735611,
|
|
43
|
+
"server_receive_response": 1760282735611,
|
|
44
|
+
"server_send_response": 1760282735611,
|
|
45
|
+
"client_receive": 1760282735678.5
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
Client → Server: 3.50ms
|
|
49
|
+
Server Processing: 14.00ms
|
|
50
|
+
Server → Device: 4323.00ms
|
|
51
|
+
Device Processing: 819.00ms
|
|
52
|
+
Handler Queue: 0.00ms
|
|
53
|
+
Handler Execution: 7191.00ms
|
|
54
|
+
Device Response: 0.00ms
|
|
55
|
+
Device → Server: 0.00ms
|
|
56
|
+
Server → Client: 67.50ms
|
|
57
|
+
TOTAL: 12418.00ms
|
|
58
|
+
|
|
59
|
+
While the trace might missleadingly make it look like the connection Server → Device is contributing to the issue, but the truth is that the timestamp taken in the device side not immidiately as soon as the message is actually received. It's also clear from the "Device Processing" and the "Handler Execution" that the device is actually very slow. We also tried closing all terminal sessions in the device assuming some might be overwhelming it with the size of their buffer or so but that didn't change anything and it stayed very slow until we restart the portacode service.
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
Hypothesis 1: Asyncio Task Accumulation (Memory Leak)
|
|
63
|
+
|
|
64
|
+
Location: Multiple locations throughout the codebase
|
|
65
|
+
|
|
66
|
+
Evidence:
|
|
67
|
+
- asyncio.create_task() is called extensively but tasks are rarely tracked or cleaned up:
|
|
68
|
+
- Terminal session debounce tasks: /home/menas/portacode/portacode/connection/handlers/session.py:260
|
|
69
|
+
- File watcher event handling: /home/menas/portacode/portacode/connection/handlers/project_state/file_system_watcher.py:125-128
|
|
70
|
+
- Project state refresh tasks: /home/menas/portacode/portacode/connection/terminal.py:502-503
|
|
71
|
+
- Git change callbacks: /home/menas/portacode/portacode/connection/handlers/project_state/manager.py:840
|
|
72
|
+
|
|
73
|
+
Why it causes degradation:
|
|
74
|
+
- Each uncancelled/uncompleted task consumes memory and CPU
|
|
75
|
+
- Over hours of usage, thousands of orphaned tasks accumulate in the event loop
|
|
76
|
+
- The event loop has to check all pending tasks on each iteration, causing exponential slowdown
|
|
77
|
+
- The 7191ms "Handler Execution" time suggests the event loop is overwhelmed
|
|
78
|
+
|
|
79
|
+
Key smoking gun: Line 260 in session.py creates debounce tasks for terminal data that may never complete if terminals are long-running. Similarly, line 840 in
|
|
80
|
+
manager.py creates tasks without storing references to cancel them later.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
Hypothesis 2: Git Operations Blocking the Event Loop
|
|
84
|
+
|
|
85
|
+
Location: /home/menas/portacode/portacode/connection/handlers/project_state/git_manager.py and
|
|
86
|
+
/home/menas/portacode/portacode/connection/handlers/project_state/manager.py
|
|
87
|
+
|
|
88
|
+
Evidence:
|
|
89
|
+
- Git periodic monitoring runs every 1 second checking full repository status: git_manager.py:1839
|
|
90
|
+
- File system watcher triggers git operations on every file change: manager.py:842-878
|
|
91
|
+
- Many git operations are NOT using run_in_executor despite being synchronous I/O
|
|
92
|
+
- get_file_status_batch() performs multiple git commands per file: git_manager.py:295-406
|
|
93
|
+
|
|
94
|
+
Why it causes degradation:
|
|
95
|
+
- Git operations on large repos can take 100-500ms each
|
|
96
|
+
- With active file editing, git operations are triggered continuously
|
|
97
|
+
- The synchronous git calls block the event loop, preventing handlers from processing
|
|
98
|
+
- The 819ms "Device Processing" + 7191ms "Handler Execution" = 8010ms spent in device-side operations
|
|
99
|
+
|
|
100
|
+
Key smoking gun: Line 1839 shows monitoring runs every 1 second, and lines 372-407 show batch git operations that aren't async-wrapped, causing cumulative
|
|
101
|
+
blocking over time.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
Hypothesis 3: Watchdog File System Watcher Resource Leak
|
|
105
|
+
|
|
106
|
+
Location: /home/menas/portacode/portacode/connection/handlers/project_state/file_system_watcher.py and
|
|
107
|
+
/home/menas/portacode/portacode/connection/handlers/project_state/manager.py
|
|
108
|
+
|
|
109
|
+
Evidence:
|
|
110
|
+
- Watchdog observers are created but never stopped: file_system_watcher.py:136
|
|
111
|
+
- .git directories are watched recursively: file_system_watcher.py:168
|
|
112
|
+
- New paths are added to watched_paths but cleanup only discards from set without stopping observers: file_system_watcher.py:180-188
|
|
113
|
+
- Multiple project sessions can create multiple watchers for overlapping paths
|
|
114
|
+
|
|
115
|
+
Why it causes degradation:
|
|
116
|
+
- Each watchdog observer creates background threads that consume resources
|
|
117
|
+
- Recursive watching of .git/objects/ can trigger thousands of events during git operations
|
|
118
|
+
- File system events queue up faster than they can be processed
|
|
119
|
+
- The cross-thread communication (watchdog thread → asyncio event loop) adds overhead
|
|
120
|
+
|
|
121
|
+
Key smoking gun: Line 168 watches git directories recursively, and line 186 shows stop_watching() only removes from the set but doesn't actually stop the observer
|
|
122
|
+
schedules. The Observer instance lives for the lifetime of the watcher, accumulating schedules.
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
Recommended Investigation Order:
|
|
126
|
+
|
|
127
|
+
1. Check for task accumulation first (use asyncio.all_tasks() to count pending tasks)
|
|
128
|
+
2. Profile git operation frequency and duration
|
|
129
|
+
3. Check watchdog observer thread count and file descriptor usage
|