portacode 1.3.30__tar.gz → 1.3.31__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.

Files changed (97) hide show
  1. {portacode-1.3.30 → portacode-1.3.31}/.claude/settings.local.json +3 -2
  2. {portacode-1.3.30 → portacode-1.3.31}/PKG-INFO +1 -1
  3. {portacode-1.3.30 → portacode-1.3.31}/portacode/_version.py +2 -2
  4. {portacode-1.3.30 → portacode-1.3.31}/portacode/connection/handlers/project_state/file_system_watcher.py +24 -10
  5. {portacode-1.3.30 → portacode-1.3.31}/portacode.egg-info/PKG-INFO +1 -1
  6. {portacode-1.3.30 → portacode-1.3.31}/portacode.egg-info/SOURCES.txt +4 -0
  7. portacode-1.3.31/todo/agent_context_management.md +12 -0
  8. portacode-1.3.31/todo/issues/device_performance_degradation.md +129 -0
  9. portacode-1.3.31/todo/issues/git_data_not_captured_in_proxmox.md +2004 -0
  10. portacode-1.3.31/todo/issues/terminals_exit_upon_starting.md +3 -0
  11. {portacode-1.3.30 → portacode-1.3.31}/.claude/agents/communication-manager.md +0 -0
  12. {portacode-1.3.30 → portacode-1.3.31}/.gitignore +0 -0
  13. {portacode-1.3.30 → portacode-1.3.31}/.gitmodules +0 -0
  14. {portacode-1.3.30 → portacode-1.3.31}/LICENSE +0 -0
  15. {portacode-1.3.30 → portacode-1.3.31}/MANIFEST.in +0 -0
  16. {portacode-1.3.30 → portacode-1.3.31}/Makefile +0 -0
  17. {portacode-1.3.30 → portacode-1.3.31}/README.md +0 -0
  18. {portacode-1.3.30 → portacode-1.3.31}/backup.sh +0 -0
  19. {portacode-1.3.30 → portacode-1.3.31}/connect.py +0 -0
  20. {portacode-1.3.30 → portacode-1.3.31}/connect.sh +0 -0
  21. {portacode-1.3.30 → portacode-1.3.31}/docker-compose.yaml +0 -0
  22. {portacode-1.3.30 → portacode-1.3.31}/portacode/README.md +0 -0
  23. {portacode-1.3.30 → portacode-1.3.31}/portacode/__init__.py +0 -0
  24. {portacode-1.3.30 → portacode-1.3.31}/portacode/__main__.py +0 -0
  25. {portacode-1.3.30 → portacode-1.3.31}/portacode/cli.py +0 -0
  26. {portacode-1.3.30 → portacode-1.3.31}/portacode/connection/README.md +0 -0
  27. {portacode-1.3.30 → portacode-1.3.31}/portacode/connection/__init__.py +0 -0
  28. {portacode-1.3.30 → portacode-1.3.31}/portacode/connection/client.py +0 -0
  29. {portacode-1.3.30 → portacode-1.3.31}/portacode/connection/handlers/README.md +0 -0
  30. {portacode-1.3.30 → portacode-1.3.31}/portacode/connection/handlers/WEBSOCKET_PROTOCOL.md +0 -0
  31. {portacode-1.3.30 → portacode-1.3.31}/portacode/connection/handlers/__init__.py +0 -0
  32. {portacode-1.3.30 → portacode-1.3.31}/portacode/connection/handlers/base.py +0 -0
  33. {portacode-1.3.30 → portacode-1.3.31}/portacode/connection/handlers/chunked_content.py +0 -0
  34. {portacode-1.3.30 → portacode-1.3.31}/portacode/connection/handlers/file_handlers.py +0 -0
  35. {portacode-1.3.30 → portacode-1.3.31}/portacode/connection/handlers/project_aware_file_handlers.py +0 -0
  36. {portacode-1.3.30 → portacode-1.3.31}/portacode/connection/handlers/project_state/README.md +0 -0
  37. {portacode-1.3.30 → portacode-1.3.31}/portacode/connection/handlers/project_state/__init__.py +0 -0
  38. {portacode-1.3.30 → portacode-1.3.31}/portacode/connection/handlers/project_state/git_manager.py +0 -0
  39. {portacode-1.3.30 → portacode-1.3.31}/portacode/connection/handlers/project_state/handlers.py +0 -0
  40. {portacode-1.3.30 → portacode-1.3.31}/portacode/connection/handlers/project_state/manager.py +0 -0
  41. {portacode-1.3.30 → portacode-1.3.31}/portacode/connection/handlers/project_state/models.py +0 -0
  42. {portacode-1.3.30 → portacode-1.3.31}/portacode/connection/handlers/project_state/utils.py +0 -0
  43. {portacode-1.3.30 → portacode-1.3.31}/portacode/connection/handlers/project_state_handlers.py +0 -0
  44. {portacode-1.3.30 → portacode-1.3.31}/portacode/connection/handlers/registry.py +0 -0
  45. {portacode-1.3.30 → portacode-1.3.31}/portacode/connection/handlers/session.py +0 -0
  46. {portacode-1.3.30 → portacode-1.3.31}/portacode/connection/handlers/system_handlers.py +0 -0
  47. {portacode-1.3.30 → portacode-1.3.31}/portacode/connection/handlers/tab_factory.py +0 -0
  48. {portacode-1.3.30 → portacode-1.3.31}/portacode/connection/handlers/terminal_handlers.py +0 -0
  49. {portacode-1.3.30 → portacode-1.3.31}/portacode/connection/multiplex.py +0 -0
  50. {portacode-1.3.30 → portacode-1.3.31}/portacode/connection/terminal.py +0 -0
  51. {portacode-1.3.30 → portacode-1.3.31}/portacode/data.py +0 -0
  52. {portacode-1.3.30 → portacode-1.3.31}/portacode/keypair.py +0 -0
  53. {portacode-1.3.30 → portacode-1.3.31}/portacode/logging_categories.py +0 -0
  54. {portacode-1.3.30 → portacode-1.3.31}/portacode/service.py +0 -0
  55. {portacode-1.3.30 → portacode-1.3.31}/portacode/static/js/test-ntp-clock.html +0 -0
  56. {portacode-1.3.30 → portacode-1.3.31}/portacode/static/js/utils/ntp-clock.js +0 -0
  57. {portacode-1.3.30 → portacode-1.3.31}/portacode/utils/NTP_ARCHITECTURE.md +0 -0
  58. {portacode-1.3.30 → portacode-1.3.31}/portacode/utils/__init__.py +0 -0
  59. {portacode-1.3.30 → portacode-1.3.31}/portacode/utils/ntp_clock.py +0 -0
  60. {portacode-1.3.30 → portacode-1.3.31}/portacode.egg-info/dependency_links.txt +0 -0
  61. {portacode-1.3.30 → portacode-1.3.31}/portacode.egg-info/entry_points.txt +0 -0
  62. {portacode-1.3.30 → portacode-1.3.31}/portacode.egg-info/requires.txt +0 -0
  63. {portacode-1.3.30 → portacode-1.3.31}/portacode.egg-info/top_level.txt +0 -0
  64. {portacode-1.3.30 → portacode-1.3.31}/pyproject.toml +0 -0
  65. {portacode-1.3.30 → portacode-1.3.31}/restore.sh +0 -0
  66. {portacode-1.3.30 → portacode-1.3.31}/run_tests.py +0 -0
  67. {portacode-1.3.30 → portacode-1.3.31}/setup.cfg +0 -0
  68. {portacode-1.3.30 → portacode-1.3.31}/setup.py +0 -0
  69. {portacode-1.3.30 → portacode-1.3.31}/test.sh +0 -0
  70. {portacode-1.3.30 → portacode-1.3.31}/test_modules/README.md +0 -0
  71. {portacode-1.3.30 → portacode-1.3.31}/test_modules/__init__.py +0 -0
  72. {portacode-1.3.30 → portacode-1.3.31}/test_modules/test_device_online.py +0 -0
  73. {portacode-1.3.30 → portacode-1.3.31}/test_modules/test_file_operations.py +0 -0
  74. {portacode-1.3.30 → portacode-1.3.31}/test_modules/test_git_status_ui.py +0 -0
  75. {portacode-1.3.30 → portacode-1.3.31}/test_modules/test_login_flow.py +0 -0
  76. {portacode-1.3.30 → portacode-1.3.31}/test_modules/test_navigate_testing_folder.py +0 -0
  77. {portacode-1.3.30 → portacode-1.3.31}/test_modules/test_terminal_buffer_performance.py +0 -0
  78. {portacode-1.3.30 → portacode-1.3.31}/test_modules/test_terminal_interaction.py +0 -0
  79. {portacode-1.3.30 → portacode-1.3.31}/test_modules/test_terminal_loading_race_condition.py +0 -0
  80. {portacode-1.3.30 → portacode-1.3.31}/test_modules/test_terminal_start.py +0 -0
  81. {portacode-1.3.30 → portacode-1.3.31}/test_request_id.py +0 -0
  82. {portacode-1.3.30 → portacode-1.3.31}/testing_framework/.env.example +0 -0
  83. {portacode-1.3.30 → portacode-1.3.31}/testing_framework/README.md +0 -0
  84. {portacode-1.3.30 → portacode-1.3.31}/testing_framework/__init__.py +0 -0
  85. {portacode-1.3.30 → portacode-1.3.31}/testing_framework/cli.py +0 -0
  86. {portacode-1.3.30 → portacode-1.3.31}/testing_framework/core/__init__.py +0 -0
  87. {portacode-1.3.30 → portacode-1.3.31}/testing_framework/core/base_test.py +0 -0
  88. {portacode-1.3.30 → portacode-1.3.31}/testing_framework/core/cli_manager.py +0 -0
  89. {portacode-1.3.30 → portacode-1.3.31}/testing_framework/core/hierarchical_runner.py +0 -0
  90. {portacode-1.3.30 → portacode-1.3.31}/testing_framework/core/playwright_manager.py +0 -0
  91. {portacode-1.3.30 → portacode-1.3.31}/testing_framework/core/runner.py +0 -0
  92. {portacode-1.3.30 → portacode-1.3.31}/testing_framework/core/shared_cli_manager.py +0 -0
  93. {portacode-1.3.30 → portacode-1.3.31}/testing_framework/core/test_discovery.py +0 -0
  94. {portacode-1.3.30 → portacode-1.3.31}/testing_framework/requirements.txt +0 -0
  95. {portacode-1.3.30 → portacode-1.3.31}/todo/issues/indefinite_resource_loading.md +0 -0
  96. {portacode-1.3.30 → portacode-1.3.31}/todo/issues/premature_terminal_exit.md +0 -0
  97. {portacode-1.3.30 → portacode-1.3.31}/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
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: portacode
3
- Version: 1.3.30
3
+ Version: 1.3.31
4
4
  Summary: Portacode CLI client and SDK
5
5
  Home-page: https://github.com/portacode/portacode
6
6
  Author: Meena Erian
@@ -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.30'
32
- __version_tuple__ = version_tuple = (1, 3, 30)
31
+ __version__ = version = '1.3.31'
32
+ __version_tuple__ = version_tuple = (1, 3, 31)
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
- # Note: watchdog doesn't have direct path removal, would need to recreate observer
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()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: portacode
3
- Version: 1.3.30
3
+ Version: 1.3.31
4
4
  Summary: Portacode CLI client and SDK
5
5
  Home-page: https://github.com/portacode/portacode
6
6
  Author: Meena Erian
@@ -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