portacode 1.4.35.dev4__tar.gz → 1.4.37.dev0__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.
Files changed (407) hide show
  1. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/PKG-INFO +1 -1
  2. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/_version.py +2 -2
  3. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/client.py +20 -3
  4. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/terminal.py +198 -0
  5. portacode-1.4.37.dev0/portacode/exit_codes.py +7 -0
  6. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/service.py +52 -10
  7. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode.egg-info/PKG-INFO +1 -1
  8. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode.egg-info/SOURCES.txt +2 -1
  9. portacode-1.4.37.dev0/todo/issues/websocket_client_silently_dead.md +51 -0
  10. portacode-1.4.35.dev4/portacode/test_updater.py +0 -100
  11. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/.claude/agents/communication-manager.md +0 -0
  12. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/.claude/settings.local.json +0 -0
  13. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/.gitignore +0 -0
  14. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/.gitmodules +0 -0
  15. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/LICENSE +0 -0
  16. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/MANIFEST.in +0 -0
  17. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/Makefile +0 -0
  18. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/README.md +0 -0
  19. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/backup.sh +0 -0
  20. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/build_android.sh +0 -0
  21. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/connect.py +0 -0
  22. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/connect.sh +0 -0
  23. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/docker-compose.yaml +0 -0
  24. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/docs/cloudflared-domain-connect-containers-audit.md +0 -0
  25. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/docs/creative-team-brief-portacode.md +0 -0
  26. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/docs/devops-messaging-ab-tests.md +0 -0
  27. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/docs/homepage-dashboard-positioning-fixes.md +0 -0
  28. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/docs/images/device-transfer-button.png +0 -0
  29. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/docs/images/device-transfer-modal.png +0 -0
  30. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/docs/images/pair-device-button.png +0 -0
  31. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/docs/images/pairing-request.png +0 -0
  32. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/docs/images/student-workspace.png +0 -0
  33. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/docs/template-guide.html +0 -0
  34. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/README.md +0 -0
  35. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/simple_device/Dockerfile +0 -0
  36. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/simple_device/README.md +0 -0
  37. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/simple_device/data/device-01/.local/share/portacode/keys/id_portacode +0 -0
  38. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/simple_device/data/device-01/.local/share/portacode/keys/id_portacode.pub +0 -0
  39. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/simple_device/docker-compose.yaml +0 -0
  40. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/Dockerfile +0 -0
  41. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/README.md +0 -0
  42. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/.local/share/portacode/keys/id_portacode +0 -0
  43. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/.local/share/portacode/keys/id_portacode.pub +0 -0
  44. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/.local/share/portacode/run/gateway.pid +0 -0
  45. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/.gitignore +0 -0
  46. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/README.md +0 -0
  47. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/db.sqlite3 +0 -0
  48. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__init__.py +0 -0
  49. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__pycache__/__init__.cpython-311.pyc +0 -0
  50. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__pycache__/settings.cpython-311.pyc +0 -0
  51. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__pycache__/urls.cpython-311.pyc +0 -0
  52. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__pycache__/wsgi.cpython-311.pyc +0 -0
  53. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/asgi.py +0 -0
  54. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/settings.py +0 -0
  55. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/urls.py +0 -0
  56. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/wsgi.py +0 -0
  57. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/manage.py +0 -0
  58. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/requirements.txt +0 -0
  59. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/templates/treats/home.html +0 -0
  60. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/__init__.py +0 -0
  61. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/__init__.cpython-311.pyc +0 -0
  62. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/admin.cpython-311.pyc +0 -0
  63. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/apps.cpython-311.pyc +0 -0
  64. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/menu.cpython-311.pyc +0 -0
  65. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/models.cpython-311.pyc +0 -0
  66. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/urls.cpython-311.pyc +0 -0
  67. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/views.cpython-311.pyc +0 -0
  68. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/admin.py +0 -0
  69. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/apps.py +0 -0
  70. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/menu.py +0 -0
  71. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/migrations/__init__.py +0 -0
  72. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/migrations/__pycache__/__init__.cpython-311.pyc +0 -0
  73. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/models.py +0 -0
  74. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/tests.py +0 -0
  75. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/urls.py +0 -0
  76. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/views.py +0 -0
  77. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-02/.local/share/portacode/keys/id_portacode +0 -0
  78. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-02/.local/share/portacode/keys/id_portacode.pub +0 -0
  79. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-02/workspace/README.md +0 -0
  80. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/__init__.py +0 -0
  81. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/asgi.py +0 -0
  82. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/settings.py +0 -0
  83. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/urls.py +0 -0
  84. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/wsgi.py +0 -0
  85. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-02/workspace/manage.py +0 -0
  86. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-02/workspace/requirements.txt +0 -0
  87. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-02/workspace/templates/treats/home.html +0 -0
  88. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-02/workspace/treats/__init__.py +0 -0
  89. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-02/workspace/treats/admin.py +0 -0
  90. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-02/workspace/treats/apps.py +0 -0
  91. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-02/workspace/treats/menu.py +0 -0
  92. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-02/workspace/treats/migrations/__init__.py +0 -0
  93. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-02/workspace/treats/models.py +0 -0
  94. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-02/workspace/treats/tests.py +0 -0
  95. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-02/workspace/treats/urls.py +0 -0
  96. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-02/workspace/treats/views.py +0 -0
  97. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-03/.local/share/portacode/keys/id_portacode +0 -0
  98. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-03/.local/share/portacode/keys/id_portacode.pub +0 -0
  99. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-03/workspace/README.md +0 -0
  100. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/__init__.py +0 -0
  101. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/asgi.py +0 -0
  102. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/settings.py +0 -0
  103. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/urls.py +0 -0
  104. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/wsgi.py +0 -0
  105. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-03/workspace/manage.py +0 -0
  106. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-03/workspace/requirements.txt +0 -0
  107. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-03/workspace/templates/treats/home.html +0 -0
  108. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-03/workspace/treats/__init__.py +0 -0
  109. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-03/workspace/treats/admin.py +0 -0
  110. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-03/workspace/treats/apps.py +0 -0
  111. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-03/workspace/treats/menu.py +0 -0
  112. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-03/workspace/treats/migrations/__init__.py +0 -0
  113. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-03/workspace/treats/models.py +0 -0
  114. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-03/workspace/treats/tests.py +0 -0
  115. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-03/workspace/treats/urls.py +0 -0
  116. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-03/workspace/treats/views.py +0 -0
  117. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-04/.local/share/portacode/keys/id_portacode +0 -0
  118. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-04/.local/share/portacode/keys/id_portacode.pub +0 -0
  119. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-04/workspace/README.md +0 -0
  120. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/__init__.py +0 -0
  121. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/asgi.py +0 -0
  122. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/settings.py +0 -0
  123. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/urls.py +0 -0
  124. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/wsgi.py +0 -0
  125. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-04/workspace/manage.py +0 -0
  126. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-04/workspace/requirements.txt +0 -0
  127. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-04/workspace/templates/treats/home.html +0 -0
  128. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-04/workspace/treats/__init__.py +0 -0
  129. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-04/workspace/treats/admin.py +0 -0
  130. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-04/workspace/treats/apps.py +0 -0
  131. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-04/workspace/treats/menu.py +0 -0
  132. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-04/workspace/treats/migrations/__init__.py +0 -0
  133. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-04/workspace/treats/models.py +0 -0
  134. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-04/workspace/treats/tests.py +0 -0
  135. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-04/workspace/treats/urls.py +0 -0
  136. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-04/workspace/treats/views.py +0 -0
  137. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-05/.local/share/portacode/keys/id_portacode +0 -0
  138. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-05/.local/share/portacode/keys/id_portacode.pub +0 -0
  139. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-05/workspace/README.md +0 -0
  140. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/__init__.py +0 -0
  141. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/asgi.py +0 -0
  142. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/settings.py +0 -0
  143. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/urls.py +0 -0
  144. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/wsgi.py +0 -0
  145. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-05/workspace/manage.py +0 -0
  146. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-05/workspace/requirements.txt +0 -0
  147. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-05/workspace/templates/treats/home.html +0 -0
  148. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-05/workspace/treats/__init__.py +0 -0
  149. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-05/workspace/treats/admin.py +0 -0
  150. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-05/workspace/treats/apps.py +0 -0
  151. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-05/workspace/treats/menu.py +0 -0
  152. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-05/workspace/treats/migrations/__init__.py +0 -0
  153. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-05/workspace/treats/models.py +0 -0
  154. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-05/workspace/treats/tests.py +0 -0
  155. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-05/workspace/treats/urls.py +0 -0
  156. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-05/workspace/treats/views.py +0 -0
  157. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-06/.local/share/portacode/keys/id_portacode +0 -0
  158. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-06/.local/share/portacode/keys/id_portacode.pub +0 -0
  159. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-06/workspace/README.md +0 -0
  160. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/__init__.py +0 -0
  161. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/asgi.py +0 -0
  162. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/settings.py +0 -0
  163. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/urls.py +0 -0
  164. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/wsgi.py +0 -0
  165. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-06/workspace/manage.py +0 -0
  166. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-06/workspace/requirements.txt +0 -0
  167. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-06/workspace/templates/treats/home.html +0 -0
  168. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-06/workspace/treats/__init__.py +0 -0
  169. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-06/workspace/treats/admin.py +0 -0
  170. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-06/workspace/treats/apps.py +0 -0
  171. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-06/workspace/treats/menu.py +0 -0
  172. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-06/workspace/treats/migrations/__init__.py +0 -0
  173. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-06/workspace/treats/models.py +0 -0
  174. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-06/workspace/treats/tests.py +0 -0
  175. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-06/workspace/treats/urls.py +0 -0
  176. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-06/workspace/treats/views.py +0 -0
  177. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-07/.local/share/portacode/keys/id_portacode +0 -0
  178. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-07/.local/share/portacode/keys/id_portacode.pub +0 -0
  179. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-07/workspace/README.md +0 -0
  180. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/__init__.py +0 -0
  181. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/asgi.py +0 -0
  182. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/settings.py +0 -0
  183. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/urls.py +0 -0
  184. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/wsgi.py +0 -0
  185. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-07/workspace/manage.py +0 -0
  186. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-07/workspace/requirements.txt +0 -0
  187. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-07/workspace/templates/treats/home.html +0 -0
  188. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-07/workspace/treats/__init__.py +0 -0
  189. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-07/workspace/treats/admin.py +0 -0
  190. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-07/workspace/treats/apps.py +0 -0
  191. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-07/workspace/treats/menu.py +0 -0
  192. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-07/workspace/treats/migrations/__init__.py +0 -0
  193. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-07/workspace/treats/models.py +0 -0
  194. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-07/workspace/treats/tests.py +0 -0
  195. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-07/workspace/treats/urls.py +0 -0
  196. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-07/workspace/treats/views.py +0 -0
  197. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-08/.local/share/portacode/keys/id_portacode +0 -0
  198. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-08/.local/share/portacode/keys/id_portacode.pub +0 -0
  199. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-08/workspace/README.md +0 -0
  200. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/__init__.py +0 -0
  201. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/asgi.py +0 -0
  202. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/settings.py +0 -0
  203. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/urls.py +0 -0
  204. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/wsgi.py +0 -0
  205. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-08/workspace/manage.py +0 -0
  206. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-08/workspace/requirements.txt +0 -0
  207. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-08/workspace/templates/treats/home.html +0 -0
  208. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-08/workspace/treats/__init__.py +0 -0
  209. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-08/workspace/treats/admin.py +0 -0
  210. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-08/workspace/treats/apps.py +0 -0
  211. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-08/workspace/treats/menu.py +0 -0
  212. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-08/workspace/treats/migrations/__init__.py +0 -0
  213. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-08/workspace/treats/models.py +0 -0
  214. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-08/workspace/treats/tests.py +0 -0
  215. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-08/workspace/treats/urls.py +0 -0
  216. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-08/workspace/treats/views.py +0 -0
  217. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-09/.local/share/portacode/keys/id_portacode +0 -0
  218. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-09/.local/share/portacode/keys/id_portacode.pub +0 -0
  219. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-09/workspace/README.md +0 -0
  220. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/__init__.py +0 -0
  221. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/asgi.py +0 -0
  222. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/settings.py +0 -0
  223. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/urls.py +0 -0
  224. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/wsgi.py +0 -0
  225. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-09/workspace/manage.py +0 -0
  226. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-09/workspace/requirements.txt +0 -0
  227. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-09/workspace/templates/treats/home.html +0 -0
  228. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-09/workspace/treats/__init__.py +0 -0
  229. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-09/workspace/treats/admin.py +0 -0
  230. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-09/workspace/treats/apps.py +0 -0
  231. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-09/workspace/treats/menu.py +0 -0
  232. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-09/workspace/treats/migrations/__init__.py +0 -0
  233. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-09/workspace/treats/models.py +0 -0
  234. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-09/workspace/treats/tests.py +0 -0
  235. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-09/workspace/treats/urls.py +0 -0
  236. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-09/workspace/treats/views.py +0 -0
  237. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-10/.local/share/portacode/keys/id_portacode +0 -0
  238. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-10/.local/share/portacode/keys/id_portacode.pub +0 -0
  239. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-10/workspace/README.md +0 -0
  240. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/__init__.py +0 -0
  241. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/asgi.py +0 -0
  242. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/settings.py +0 -0
  243. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/urls.py +0 -0
  244. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/wsgi.py +0 -0
  245. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-10/workspace/manage.py +0 -0
  246. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-10/workspace/requirements.txt +0 -0
  247. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-10/workspace/templates/treats/home.html +0 -0
  248. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-10/workspace/treats/__init__.py +0 -0
  249. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-10/workspace/treats/admin.py +0 -0
  250. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-10/workspace/treats/apps.py +0 -0
  251. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-10/workspace/treats/menu.py +0 -0
  252. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-10/workspace/treats/migrations/__init__.py +0 -0
  253. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-10/workspace/treats/models.py +0 -0
  254. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-10/workspace/treats/tests.py +0 -0
  255. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-10/workspace/treats/urls.py +0 -0
  256. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-10/workspace/treats/views.py +0 -0
  257. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/docker-compose.yaml +0 -0
  258. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/initial_content/README.md +0 -0
  259. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/initial_content/galactic_bakeshop/__init__.py +0 -0
  260. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/initial_content/galactic_bakeshop/asgi.py +0 -0
  261. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/initial_content/galactic_bakeshop/settings.py +0 -0
  262. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/initial_content/galactic_bakeshop/urls.py +0 -0
  263. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/initial_content/galactic_bakeshop/wsgi.py +0 -0
  264. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/initial_content/manage.py +0 -0
  265. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/initial_content/requirements.txt +0 -0
  266. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/initial_content/templates/treats/home.html +0 -0
  267. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/initial_content/treats/__init__.py +0 -0
  268. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/initial_content/treats/admin.py +0 -0
  269. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/initial_content/treats/apps.py +0 -0
  270. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/initial_content/treats/menu.py +0 -0
  271. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/initial_content/treats/migrations/__init__.py +0 -0
  272. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/initial_content/treats/models.py +0 -0
  273. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/initial_content/treats/tests.py +0 -0
  274. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/initial_content/treats/urls.py +0 -0
  275. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/initial_content/treats/views.py +0 -0
  276. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/instructions/WELCOME.md +0 -0
  277. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/README.md +0 -0
  278. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/__init__.py +0 -0
  279. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/__main__.py +0 -0
  280. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/cli.py +0 -0
  281. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/README.md +0 -0
  282. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/__init__.py +0 -0
  283. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/handlers/README.md +0 -0
  284. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/handlers/WEBSOCKET_PROTOCOL.md +0 -0
  285. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/handlers/__init__.py +0 -0
  286. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/handlers/automation_v2_handlers.py +0 -0
  287. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/handlers/base.py +0 -0
  288. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/handlers/chunked_content.py +0 -0
  289. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/handlers/cloudflare_forwarding.py +0 -0
  290. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/handlers/cloudflare_tunnel.py +0 -0
  291. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/handlers/diff_handlers.py +0 -0
  292. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/handlers/file_handlers.py +0 -0
  293. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/handlers/project_aware_file_handlers.py +0 -0
  294. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/handlers/project_state/README.md +0 -0
  295. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/handlers/project_state/__init__.py +0 -0
  296. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/handlers/project_state/file_system_watcher.py +0 -0
  297. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/handlers/project_state/git_manager.py +0 -0
  298. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/handlers/project_state/handlers.py +0 -0
  299. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/handlers/project_state/manager.py +0 -0
  300. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/handlers/project_state/models.py +0 -0
  301. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/handlers/project_state/utils.py +0 -0
  302. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/handlers/project_state_handlers.py +0 -0
  303. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/handlers/proxmox_infra.py +0 -0
  304. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/handlers/registry.py +0 -0
  305. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/handlers/session.py +0 -0
  306. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/handlers/system_handlers.py +0 -0
  307. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/handlers/tab_factory.py +0 -0
  308. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/handlers/terminal_handlers.py +0 -0
  309. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/handlers/test_proxmox_infra.py +0 -0
  310. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/handlers/update_handler.py +0 -0
  311. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/multiplex.py +0 -0
  312. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/data.py +0 -0
  313. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/keypair.py +0 -0
  314. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/link_capture/__init__.py +0 -0
  315. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/link_capture/__pycache__/__init__.cpython-311.pyc +0 -0
  316. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/link_capture/bin/__pycache__/link_capture_wrapper.cpython-311.pyc +0 -0
  317. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/link_capture/bin/elinks +0 -0
  318. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/link_capture/bin/gio-open +0 -0
  319. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/link_capture/bin/gnome-open +0 -0
  320. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/link_capture/bin/gvfs-open +0 -0
  321. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/link_capture/bin/kde-open +0 -0
  322. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/link_capture/bin/kfmclient +0 -0
  323. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/link_capture/bin/link_capture_exec.sh +0 -0
  324. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/link_capture/bin/link_capture_wrapper.py +0 -0
  325. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/link_capture/bin/links +0 -0
  326. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/link_capture/bin/links2 +0 -0
  327. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/link_capture/bin/lynx +0 -0
  328. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/link_capture/bin/mate-open +0 -0
  329. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/link_capture/bin/netsurf +0 -0
  330. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/link_capture/bin/sensible-browser +0 -0
  331. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/link_capture/bin/w3m +0 -0
  332. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/link_capture/bin/x-www-browser +0 -0
  333. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/link_capture/bin/xdg-open +0 -0
  334. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/logging_categories.py +0 -0
  335. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/pairing.py +0 -0
  336. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/restart.py +0 -0
  337. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/static/js/test-ntp-clock.html +0 -0
  338. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/static/js/utils/ntp-clock.js +0 -0
  339. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/tunneling/__init__.py +0 -0
  340. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/tunneling/cloudflared_login.py +0 -0
  341. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/tunneling/ensure_cloudflared.py +0 -0
  342. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/tunneling/ensure_pyyaml.py +0 -0
  343. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/tunneling/forwarding_state.py +0 -0
  344. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/tunneling/get_domain.py +0 -0
  345. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/tunneling/privileged.py +0 -0
  346. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/tunneling/service_install.py +0 -0
  347. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/tunneling/state.py +0 -0
  348. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/updater.py +0 -0
  349. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/utils/NTP_ARCHITECTURE.md +0 -0
  350. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/utils/__init__.py +0 -0
  351. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/utils/diff_apply.py +0 -0
  352. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/utils/diff_renderer.py +0 -0
  353. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/utils/ntp_clock.py +0 -0
  354. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode.egg-info/dependency_links.txt +0 -0
  355. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode.egg-info/entry_points.txt +0 -0
  356. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode.egg-info/requires.txt +0 -0
  357. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode.egg-info/top_level.txt +0 -0
  358. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/pyproject.toml +0 -0
  359. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/restore.sh +0 -0
  360. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/run_tests.py +0 -0
  361. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/setup.cfg +0 -0
  362. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/setup.py +0 -0
  363. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/test.sh +0 -0
  364. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/test_modules/README.md +0 -0
  365. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/test_modules/__init__.py +0 -0
  366. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/test_modules/test_device_online.py +0 -0
  367. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/test_modules/test_file_operations.py +0 -0
  368. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/test_modules/test_git_status_ui.py +0 -0
  369. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/test_modules/test_login_flow.py +0 -0
  370. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/test_modules/test_navigate_testing_folder.py +0 -0
  371. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/test_modules/test_play_store_screenshots.py +0 -0
  372. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/test_modules/test_terminal_buffer_performance.py +0 -0
  373. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/test_modules/test_terminal_interaction.py +0 -0
  374. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/test_modules/test_terminal_loading_race_condition.py +0 -0
  375. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/test_modules/test_terminal_start.py +0 -0
  376. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/test_request_id.py +0 -0
  377. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/testing_framework/.env.example +0 -0
  378. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/testing_framework/README.md +0 -0
  379. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/testing_framework/__init__.py +0 -0
  380. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/testing_framework/cli.py +0 -0
  381. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/testing_framework/core/__init__.py +0 -0
  382. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/testing_framework/core/base_test.py +0 -0
  383. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/testing_framework/core/cli_manager.py +0 -0
  384. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/testing_framework/core/hierarchical_runner.py +0 -0
  385. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/testing_framework/core/playwright_manager.py +0 -0
  386. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/testing_framework/core/runner.py +0 -0
  387. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/testing_framework/core/shared_cli_manager.py +0 -0
  388. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/testing_framework/core/test_discovery.py +0 -0
  389. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/testing_framework/requirements.txt +0 -0
  390. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/todo/UI_UX/opening_a_file_on_desktop_results_in_nothing.md +0 -0
  391. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/todo/UI_UX/server_occasionally_stops_communicating_with_all_devices.md +0 -0
  392. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/todo/agent_context_management.md +0 -0
  393. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/todo/django_server_time_sync.md +0 -0
  394. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/todo/issues/device_performance_degradation.md +0 -0
  395. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/todo/issues/git_data_not_captured_in_proxmox.md +0 -0
  396. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/todo/issues/indefinite_resource_loading.md +0 -0
  397. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/todo/issues/portacode_service_silently_down.md +0 -0
  398. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/todo/issues/premature_terminal_exit.md +0 -0
  399. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/todo/issues/project_cpu_hotspots.md +0 -0
  400. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/todo/issues/terminals_exit_upon_starting.md +0 -0
  401. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/todo/issues/wrong_item_classification_on_client_side.md +0 -0
  402. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/todo/smartphone_terminal_input_frustrations.md +0 -0
  403. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/tools/generate_play_store_assets.py +0 -0
  404. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/tools/pairing_tester.py +0 -0
  405. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/tools/run_screenshot_suite.sh +0 -0
  406. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/tools/test_python_ntp_clock.py +0 -0
  407. {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/validate.sh +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: portacode
3
- Version: 1.4.35.dev4
3
+ Version: 1.4.37.dev0
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.4.35.dev4'
32
- __version_tuple__ = version_tuple = (1, 4, 35, 'dev4')
31
+ __version__ = version = '1.4.37.dev0'
32
+ __version_tuple__ = version_tuple = (1, 4, 37, 'dev0')
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -17,9 +17,13 @@ from websockets import WebSocketClientProtocol
17
17
  from ..keypair import KeyPair
18
18
  from .multiplex import Multiplexer
19
19
  from ..logging_categories import get_categorized_logger, LogCategory
20
+ from ..exit_codes import AUTH_REJECTED_EXIT_CODE
20
21
 
21
22
  logger = get_categorized_logger(__name__)
22
23
 
24
+ class AuthenticationRejectedError(RuntimeError):
25
+ """Raised when gateway authentication is rejected for this device key."""
26
+
23
27
 
24
28
  class ConnectionManager:
25
29
  """Maintain a persistent connection to the Portacode gateway.
@@ -43,6 +47,8 @@ class ConnectionManager:
43
47
  CLOCK_SYNC_INITIAL_REQUESTS = 5
44
48
  CLOCK_SYNC_TIMEOUT = 20.0
45
49
  CLOCK_SYNC_MAX_FAILURES = 3
50
+ WS_PING_INTERVAL = 60.0
51
+ WS_PING_TIMEOUT = 20.0
46
52
 
47
53
  def __init__(self, gateway_url: str, keypair: KeyPair, reconnect_delay: float = 1.0, max_retries: int = None, debug: bool = False):
48
54
  self.gateway_url = gateway_url
@@ -85,7 +91,11 @@ class ConnectionManager:
85
91
  logger.warning("Reconnecting in %.1f s (attempt %d)…", LogCategory.CONNECTION, delay, attempt)
86
92
  await asyncio.sleep(delay)
87
93
  logger.info("Connecting to gateway at %s", LogCategory.CONNECTION, self.gateway_url)
88
- async with websockets.connect(self.gateway_url) as ws:
94
+ async with websockets.connect(
95
+ self.gateway_url,
96
+ ping_interval=self.WS_PING_INTERVAL,
97
+ ping_timeout=self.WS_PING_TIMEOUT,
98
+ ) as ws:
89
99
  # Reset attempt counter after successful connection
90
100
  attempt = 0
91
101
 
@@ -118,6 +128,9 @@ class ConnectionManager:
118
128
  logger.warning("Connection error: %s", LogCategory.CONNECTION, exc)
119
129
  # Remove the max_retries limit - keep trying indefinitely
120
130
  # The service manager (systemd) will handle any necessary restarts
131
+ except AuthenticationRejectedError as exc:
132
+ logger.error("Authentication rejected by gateway: %s", LogCategory.CONNECTION, exc)
133
+ sys.exit(AUTH_REJECTED_EXIT_CODE)
121
134
  except Exception as exc:
122
135
  # For truly fatal errors (like authentication failures),
123
136
  # log and exit cleanly so systemd can restart the service
@@ -137,7 +150,7 @@ class ConnectionManager:
137
150
  challenge = data["challenge"]
138
151
  except Exception:
139
152
  # Not a challenge, must be an error
140
- raise RuntimeError(f"Gateway rejected authentication: {response}")
153
+ raise AuthenticationRejectedError(f"Gateway rejected authentication: {response}")
141
154
  # Step 3: Sign challenge and send signature
142
155
  signature = self.keypair.sign_challenge(challenge)
143
156
  signature_b64 = base64.b64encode(signature).decode()
@@ -145,7 +158,7 @@ class ConnectionManager:
145
158
  # Step 4: Receive final status
146
159
  status = await self.websocket.recv()
147
160
  if status != "ok":
148
- raise RuntimeError(f"Gateway rejected authentication: {status}")
161
+ raise AuthenticationRejectedError(f"Gateway rejected authentication: {status}")
149
162
  # Print success message in green and show close instructions
150
163
  try:
151
164
  import click
@@ -206,6 +219,7 @@ class ConnectionManager:
206
219
  self._clock_sync_task.cancel()
207
220
  self._clock_sync_task = asyncio.create_task(self._clock_sync_loop())
208
221
  self._remaining_initial_syncs = self.CLOCK_SYNC_INITIAL_REQUESTS
222
+ self._clock_sync_failures = 0
209
223
 
210
224
  async def _stop_clock_sync_task(self) -> None:
211
225
  if self._clock_sync_task:
@@ -228,6 +242,9 @@ class ConnectionManager:
228
242
  self._remaining_initial_syncs -= 1
229
243
  if self._remaining_initial_syncs > 0:
230
244
  await asyncio.sleep(self.CLOCK_SYNC_FAST_INTERVAL)
245
+ while not self._stop_event.is_set():
246
+ await asyncio.sleep(self.CLOCK_SYNC_INTERVAL)
247
+ await self._perform_clock_sync()
231
248
  except asyncio.CancelledError:
232
249
  pass
233
250
 
@@ -14,9 +14,12 @@ in the handlers directory.
14
14
  """
15
15
 
16
16
  import asyncio
17
+ import faulthandler
17
18
  import json
18
19
  import logging
19
20
  import os
21
+ import sys
22
+ import threading
20
23
  import time
21
24
  from datetime import datetime, timezone
22
25
  from dataclasses import asdict
@@ -389,11 +392,185 @@ class TerminalManager:
389
392
  self.mux = mux
390
393
  self.debug = debug
391
394
  self._last_exposed_services_signature = "__unset__"
395
+ self._diag_lock = threading.Lock()
396
+ now_mono = time.monotonic()
397
+ self._diag_state: Dict[str, Any] = {
398
+ "heartbeat_monotonic": now_mono,
399
+ "heartbeat_epoch_s": time.time(),
400
+ "loop_phase": "init",
401
+ "active_cmd": None,
402
+ "active_cmd_started_monotonic": None,
403
+ "active_cmd_reply_channel": None,
404
+ "last_completed_cmd": None,
405
+ "last_completed_cmd_duration_s": None,
406
+ "queue_stats": {},
407
+ }
408
+ self._diag_monitor_enabled = os.getenv("PORTACODE_EVENT_LOOP_BLOCK_MONITOR", "1").lower() not in ("0", "false", "no")
409
+ self._diag_threshold_s = float(os.getenv("PORTACODE_EVENT_LOOP_BLOCK_THRESHOLD_S", "5"))
410
+ self._diag_monitor_interval_s = float(os.getenv("PORTACODE_EVENT_LOOP_BLOCK_MONITOR_INTERVAL_S", "0.5"))
411
+ self._diag_report_interval_s = float(os.getenv("PORTACODE_EVENT_LOOP_BLOCK_REPORT_INTERVAL_S", "30"))
412
+ self._diag_heartbeat_interval_s = float(os.getenv("PORTACODE_EVENT_LOOP_BLOCK_HEARTBEAT_INTERVAL_S", "0.5"))
413
+ self._diag_dump_threads = os.getenv("PORTACODE_EVENT_LOOP_BLOCK_DUMP_THREADS", "1").lower() not in ("0", "false", "no")
414
+ self._diag_monitor_stop = threading.Event()
415
+ self._diag_monitor_thread: Optional[threading.Thread] = None
416
+ self._diag_last_report_monotonic = 0.0
417
+ self._diag_last_report_key: Optional[str] = None
392
418
  self._session_manager = None # Initialize as None first
393
419
  self._client_session_manager = ClientSessionManager() # Initialize client session manager
394
420
  self._client_session_manager.set_terminal_manager(self) # Set reference for cleanup
421
+ self._start_block_monitor_thread_if_enabled()
395
422
  self._set_mux(mux, is_initial=True)
396
423
 
424
+ def _start_block_monitor_thread_if_enabled(self) -> None:
425
+ if not self._diag_monitor_enabled or self._diag_monitor_thread:
426
+ return
427
+ self._diag_monitor_thread = threading.Thread(
428
+ target=self._event_loop_block_monitor_thread,
429
+ name="portacode-event-loop-block-monitor",
430
+ daemon=True,
431
+ )
432
+ self._diag_monitor_thread.start()
433
+
434
+ def _set_diag_phase(self, phase: str) -> None:
435
+ now_mono = time.monotonic()
436
+ now_epoch = time.time()
437
+ with self._diag_lock:
438
+ self._diag_state["loop_phase"] = phase
439
+ self._diag_state["heartbeat_monotonic"] = now_mono
440
+ self._diag_state["heartbeat_epoch_s"] = now_epoch
441
+
442
+ def _set_diag_active_cmd(self, cmd: str, reply_channel: Optional[str]) -> None:
443
+ now_mono = time.monotonic()
444
+ now_epoch = time.time()
445
+ with self._diag_lock:
446
+ self._diag_state["active_cmd"] = cmd
447
+ self._diag_state["active_cmd_reply_channel"] = reply_channel
448
+ self._diag_state["active_cmd_started_monotonic"] = now_mono
449
+ self._diag_state["heartbeat_monotonic"] = now_mono
450
+ self._diag_state["heartbeat_epoch_s"] = now_epoch
451
+
452
+ def _clear_diag_active_cmd(self, completed_cmd: str, duration_s: float) -> None:
453
+ now_mono = time.monotonic()
454
+ now_epoch = time.time()
455
+ with self._diag_lock:
456
+ self._diag_state["active_cmd"] = None
457
+ self._diag_state["active_cmd_reply_channel"] = None
458
+ self._diag_state["active_cmd_started_monotonic"] = None
459
+ self._diag_state["last_completed_cmd"] = completed_cmd
460
+ self._diag_state["last_completed_cmd_duration_s"] = round(duration_s, 4)
461
+ self._diag_state["heartbeat_monotonic"] = now_mono
462
+ self._diag_state["heartbeat_epoch_s"] = now_epoch
463
+
464
+ def _snapshot_diag_state_for_monitor(self) -> Dict[str, Any]:
465
+ with self._diag_lock:
466
+ return dict(self._diag_state)
467
+
468
+ def _event_loop_block_monitor_thread(self) -> None:
469
+ while not self._diag_monitor_stop.wait(self._diag_monitor_interval_s):
470
+ snapshot = self._snapshot_diag_state_for_monitor()
471
+ now = time.monotonic()
472
+ last_hb = float(snapshot.get("heartbeat_monotonic") or now)
473
+ blocked_for_s = max(now - last_hb, 0.0)
474
+
475
+ if blocked_for_s < self._diag_threshold_s:
476
+ continue
477
+
478
+ active_cmd = snapshot.get("active_cmd")
479
+ queue_stats = snapshot.get("queue_stats") if isinstance(snapshot.get("queue_stats"), dict) else {}
480
+ control_q = queue_stats.get("control_channel_queue_size")
481
+ total_mux_q = queue_stats.get("mux_total_queued_frames")
482
+ has_backlog = (
483
+ (isinstance(control_q, int) and control_q > 0)
484
+ or (isinstance(total_mux_q, int) and total_mux_q > 0)
485
+ )
486
+ if not active_cmd and not has_backlog:
487
+ continue
488
+
489
+ active_cmd_started = snapshot.get("active_cmd_started_monotonic")
490
+ active_cmd_age_s = None
491
+ if active_cmd and isinstance(active_cmd_started, (int, float)):
492
+ active_cmd_age_s = max(now - float(active_cmd_started), 0.0)
493
+
494
+ report_key = f"{snapshot.get('loop_phase')}:{active_cmd}:{snapshot.get('heartbeat_epoch_s')}"
495
+ if report_key == self._diag_last_report_key and (now - self._diag_last_report_monotonic) < self._diag_report_interval_s:
496
+ continue
497
+
498
+ self._diag_last_report_key = report_key
499
+ self._diag_last_report_monotonic = now
500
+
501
+ payload = {
502
+ "event": "event_loop_blocking_detected",
503
+ "blocked_for_s": round(blocked_for_s, 3),
504
+ "threshold_s": self._diag_threshold_s,
505
+ "cause_hint": {
506
+ "loop_phase": snapshot.get("loop_phase"),
507
+ "active_cmd": active_cmd,
508
+ "active_cmd_reply_channel": snapshot.get("active_cmd_reply_channel"),
509
+ "active_cmd_age_s": round(active_cmd_age_s, 3) if active_cmd_age_s is not None else None,
510
+ "last_completed_cmd": snapshot.get("last_completed_cmd"),
511
+ "last_completed_cmd_duration_s": snapshot.get("last_completed_cmd_duration_s"),
512
+ },
513
+ "queue_stats": queue_stats,
514
+ "last_heartbeat_epoch_s": snapshot.get("heartbeat_epoch_s"),
515
+ "monitor_thread_epoch_s": time.time(),
516
+ "pid": os.getpid(),
517
+ }
518
+ logger.error("Event loop stall monitor: %s", json.dumps(payload, sort_keys=True))
519
+
520
+ if self._diag_dump_threads:
521
+ try:
522
+ faulthandler.dump_traceback(file=sys.stderr, all_threads=True)
523
+ except Exception as exc:
524
+ logger.error("Event loop stall monitor failed to dump thread traceback: %s", exc)
525
+
526
+ def _collect_loop_queue_stats(self) -> Dict[str, Any]:
527
+ control_q_size = None
528
+ try:
529
+ control_q = getattr(self._control_channel, "_incoming", None)
530
+ if control_q is not None:
531
+ control_q_size = control_q.qsize()
532
+ except Exception:
533
+ control_q_size = None
534
+
535
+ mux_channels = getattr(self.mux, "_channels", {}) if self.mux else {}
536
+ total_queued = 0
537
+ top_channels = []
538
+ for channel_id, channel in mux_channels.items():
539
+ try:
540
+ incoming_q = getattr(channel, "_incoming", None)
541
+ size = incoming_q.qsize() if incoming_q is not None else None
542
+ except Exception:
543
+ size = None
544
+ if isinstance(size, int):
545
+ total_queued += size
546
+ top_channels.append((str(channel_id), size))
547
+
548
+ top_channels.sort(key=lambda item: item[1], reverse=True)
549
+ return {
550
+ "control_channel_queue_size": control_q_size,
551
+ "mux_channel_count": len(mux_channels),
552
+ "mux_total_queued_frames": total_queued,
553
+ "mux_top_queues": [{"channel_id": cid, "size": size} for cid, size in top_channels[:5]],
554
+ "client_sessions_count": len(self._client_session_manager.get_sessions()),
555
+ }
556
+
557
+ async def _diag_heartbeat_loop(self) -> None:
558
+ while True:
559
+ try:
560
+ await asyncio.sleep(self._diag_heartbeat_interval_s)
561
+ queue_stats = self._collect_loop_queue_stats()
562
+ now_mono = time.monotonic()
563
+ now_epoch = time.time()
564
+ with self._diag_lock:
565
+ self._diag_state["heartbeat_monotonic"] = now_mono
566
+ self._diag_state["heartbeat_epoch_s"] = now_epoch
567
+ self._diag_state["queue_stats"] = queue_stats
568
+ except asyncio.CancelledError:
569
+ return
570
+ except Exception as exc:
571
+ logger.debug("Event loop heartbeat diagnostics error: %s", exc)
572
+ await asyncio.sleep(self._diag_heartbeat_interval_s)
573
+
397
574
  # ------------------------------------------------------------------
398
575
  # Mux attach/detach helpers (for reconnection resilience)
399
576
  # ------------------------------------------------------------------
@@ -469,6 +646,14 @@ class TerminalManager:
469
646
  except Exception:
470
647
  pass
471
648
  self._exposed_services_task = asyncio.create_task(self._watch_exposed_services())
649
+
650
+ # Start passive event-loop diagnostics heartbeat (asyncio thread only).
651
+ if getattr(self, "_diag_heartbeat_task", None):
652
+ try:
653
+ self._diag_heartbeat_task.cancel()
654
+ except Exception:
655
+ pass
656
+ self._diag_heartbeat_task = asyncio.create_task(self._diag_heartbeat_loop())
472
657
 
473
658
  # For initial connections, request client sessions after control loop starts
474
659
  if is_initial:
@@ -531,7 +716,9 @@ class TerminalManager:
531
716
  logger.info("terminal_manager: Starting control loop")
532
717
  while True:
533
718
  try:
719
+ self._set_diag_phase("control_loop_waiting_for_message")
534
720
  message = await self._control_channel.recv()
721
+ self._set_diag_phase("control_loop_received_message")
535
722
  logger.debug("terminal_manager: Received message: %s", message)
536
723
 
537
724
  # Older parts of the system may send *raw* str. Ensure dict.
@@ -554,6 +741,8 @@ class TerminalManager:
554
741
  logger.warning("terminal_manager: Missing 'cmd' in control frame: %s", message)
555
742
  continue
556
743
  reply_chan = message.get("reply_channel")
744
+ cmd_start = time.monotonic()
745
+ self._set_diag_active_cmd(cmd, reply_chan)
557
746
 
558
747
  logger.info("terminal_manager: Processing command '%s' with reply_channel=%s", cmd, reply_chan)
559
748
  logger.debug("terminal_manager: Full message: %s", message)
@@ -573,6 +762,8 @@ class TerminalManager:
573
762
  asyncio.create_task(self._send_initial_data_to_clients(newly_added_sessions))
574
763
  else:
575
764
  logger.info("terminal_manager: ℹ️ No new sessions to send data to")
765
+ self._clear_diag_active_cmd(cmd, time.monotonic() - cmd_start)
766
+ self._set_diag_phase("control_loop_completed_client_sessions_update")
576
767
  continue
577
768
 
578
769
  # Dispatch command through registry
@@ -580,9 +771,16 @@ class TerminalManager:
580
771
  if not handled:
581
772
  logger.warning("terminal_manager: Command '%s' was not handled by any handler", cmd)
582
773
  await self._send_error(f"Unknown cmd: {cmd}", reply_chan)
774
+ self._clear_diag_active_cmd(cmd, time.monotonic() - cmd_start)
775
+ self._set_diag_phase(f"control_loop_completed_{cmd}")
583
776
 
584
777
  except Exception as exc:
585
778
  logger.exception("terminal_manager: Error in control loop: %s", exc)
779
+ with self._diag_lock:
780
+ self._diag_state["active_cmd"] = None
781
+ self._diag_state["active_cmd_started_monotonic"] = None
782
+ self._diag_state["active_cmd_reply_channel"] = None
783
+ self._set_diag_phase("control_loop_exception")
586
784
  # Continue processing other messages
587
785
  continue
588
786
 
@@ -0,0 +1,7 @@
1
+ """Shared process exit codes used by the Portacode runtime and service managers."""
2
+
3
+ # Process exited because the gateway rejected device authentication (for example,
4
+ # an unknown/deleted device key). Service supervisors should not auto-restart on
5
+ # this code to avoid endless auth failure loops.
6
+ AUTH_REJECTED_EXIT_CODE = 86
7
+
@@ -29,6 +29,8 @@ import shutil
29
29
  import pwd
30
30
  import tempfile
31
31
 
32
+ from .exit_codes import AUTH_REJECTED_EXIT_CODE
33
+
32
34
  __all__ = [
33
35
  "ServiceManager",
34
36
  "get_manager",
@@ -117,8 +119,6 @@ class _SystemdUserService:
117
119
  current_shell = os.getenv("SHELL", "/bin/bash")
118
120
 
119
121
  if self.system_mode:
120
- sudo_needed = os.geteuid() != 0
121
- prefix = ["sudo"] if sudo_needed else []
122
122
  unit = textwrap.dedent(f"""
123
123
  [Unit]
124
124
  Description=Portacode persistent connection (system-wide)
@@ -131,6 +131,7 @@ class _SystemdUserService:
131
131
  Environment=SHELL={current_shell}
132
132
  ExecStart={self.python} -m portacode connect --non-interactive
133
133
  Restart=on-failure
134
+ RestartPreventExitStatus={AUTH_REJECTED_EXIT_CODE}
134
135
  RestartSec=5
135
136
 
136
137
  [Install]
@@ -147,6 +148,7 @@ class _SystemdUserService:
147
148
  Environment=SHELL={current_shell}
148
149
  ExecStart={self.python} -m portacode.cli connect --non-interactive
149
150
  Restart=on-failure
151
+ RestartPreventExitStatus={AUTH_REJECTED_EXIT_CODE}
150
152
  RestartSec=5
151
153
 
152
154
  [Install]
@@ -256,8 +258,6 @@ class _OpenRCService:
256
258
  pidfile="/run/portacode.pid"
257
259
  directory="{self.home}"
258
260
  supervisor=supervise-daemon
259
- respawn_delay=5
260
- respawn_max=0
261
261
 
262
262
  depend() {{
263
263
  need net
@@ -289,7 +289,18 @@ class _OpenRCService:
289
289
  script = textwrap.dedent(f"""
290
290
  #!/bin/sh
291
291
  cd "{self.home}"
292
- exec "{self.python}" -m portacode connect --non-interactive >> "{self.log_path}" 2>&1
292
+ while true; do
293
+ "{self.python}" -m portacode connect --non-interactive >> "{self.log_path}" 2>&1
294
+ rc="$?"
295
+ if [ "$rc" -eq {AUTH_REJECTED_EXIT_CODE} ]; then
296
+ echo "Portacode authentication rejected; stopping service restart loop." >> "{self.log_path}"
297
+ exit 0
298
+ fi
299
+ if [ "$rc" -eq 0 ]; then
300
+ exit 0
301
+ fi
302
+ sleep 5
303
+ done
293
304
  """).lstrip()
294
305
  tmp_path = Path(tempfile.gettempdir()) / f"portacode-wrapper-{os.getpid()}"
295
306
  tmp_path.write_text(script)
@@ -362,6 +373,8 @@ class _LaunchdService:
362
373
  / "Library/LaunchAgents"
363
374
  / f"{self.LABEL}.plist"
364
375
  )
376
+ self.script_path = Path.home() / ".local" / "share" / "portacode" / "connect_service.sh"
377
+ self.log_path = Path.home() / ".local" / "share" / "portacode" / "connect.log"
365
378
 
366
379
  def _run(self, *args: str) -> subprocess.CompletedProcess[str]:
367
380
  cmd = ["launchctl", *args]
@@ -369,6 +382,27 @@ class _LaunchdService:
369
382
 
370
383
  def install(self) -> None:
371
384
  self.plist_path.parent.mkdir(parents=True, exist_ok=True)
385
+ self.script_path.parent.mkdir(parents=True, exist_ok=True)
386
+ script = textwrap.dedent(
387
+ f"""
388
+ #!/bin/sh
389
+ cd "$HOME" || exit 1
390
+ while true; do
391
+ "{sys.executable}" -m portacode connect --non-interactive >> "{self.log_path}" 2>&1
392
+ rc="$?"
393
+ if [ "$rc" -eq {AUTH_REJECTED_EXIT_CODE} ]; then
394
+ echo "Portacode authentication rejected; stopping service restart loop." >> "{self.log_path}"
395
+ exit 0
396
+ fi
397
+ if [ "$rc" -eq 0 ]; then
398
+ exit 0
399
+ fi
400
+ sleep 5
401
+ done
402
+ """
403
+ ).lstrip()
404
+ self.script_path.write_text(script)
405
+ self.script_path.chmod(0o755)
372
406
  plist = textwrap.dedent(
373
407
  f"""
374
408
  <?xml version="1.0" encoding="UTF-8"?>
@@ -378,13 +412,13 @@ class _LaunchdService:
378
412
  <key>Label</key><string>{self.LABEL}</string>
379
413
  <key>ProgramArguments</key>
380
414
  <array>
381
- <string>{sys.executable}</string>
382
- <string>-m</string><string>portacode</string>
383
- <string>connect</string>
384
- <string>--non-interactive</string>
415
+ <string>{self.script_path}</string>
385
416
  </array>
386
417
  <key>RunAtLoad</key><true/>
387
- <key>KeepAlive</key><true/>
418
+ <key>KeepAlive</key>
419
+ <dict>
420
+ <key>SuccessfulExit</key><false/>
421
+ </dict>
388
422
  </dict>
389
423
  </plist>
390
424
  """
@@ -396,6 +430,8 @@ class _LaunchdService:
396
430
  self._run("unload", "-w", str(self.plist_path))
397
431
  if self.plist_path.exists():
398
432
  self.plist_path.unlink()
433
+ if self.script_path.exists():
434
+ self.script_path.unlink()
399
435
 
400
436
  def start(self) -> None:
401
437
  self._run("start", self.LABEL)
@@ -451,7 +487,13 @@ class _WindowsTask:
451
487
  script = (
452
488
  "@echo off\r\n"
453
489
  "cd /d %USERPROFILE%\r\n"
490
+ ":loop\r\n"
454
491
  f"{py_cmd} -m portacode connect --non-interactive >> \"%USERPROFILE%\\.local\\share\\portacode\\connect.log\" 2>>&1\r\n"
492
+ "set \"RC=%ERRORLEVEL%\"\r\n"
493
+ f"if \"%RC%\"==\"{AUTH_REJECTED_EXIT_CODE}\" exit /b 0\r\n"
494
+ "if \"%RC%\"==\"0\" exit /b 0\r\n"
495
+ "timeout /t 5 /nobreak >nul\r\n"
496
+ "goto loop\r\n"
455
497
  )
456
498
  self._script_path.write_text(script)
457
499
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: portacode
3
- Version: 1.4.35.dev4
3
+ Version: 1.4.37.dev0
4
4
  Summary: Portacode CLI client and SDK
5
5
  Home-page: https://github.com/portacode/portacode
6
6
  Author: Meena Erian
@@ -277,12 +277,12 @@ portacode/__main__.py
277
277
  portacode/_version.py
278
278
  portacode/cli.py
279
279
  portacode/data.py
280
+ portacode/exit_codes.py
280
281
  portacode/keypair.py
281
282
  portacode/logging_categories.py
282
283
  portacode/pairing.py
283
284
  portacode/restart.py
284
285
  portacode/service.py
285
- portacode/test_updater.py
286
286
  portacode/updater.py
287
287
  portacode.egg-info/PKG-INFO
288
288
  portacode.egg-info/SOURCES.txt
@@ -396,6 +396,7 @@ todo/issues/portacode_service_silently_down.md
396
396
  todo/issues/premature_terminal_exit.md
397
397
  todo/issues/project_cpu_hotspots.md
398
398
  todo/issues/terminals_exit_upon_starting.md
399
+ todo/issues/websocket_client_silently_dead.md
399
400
  todo/issues/wrong_item_classification_on_client_side.md
400
401
  tools/generate_play_store_assets.py
401
402
  tools/pairing_tester.py
@@ -0,0 +1,51 @@
1
+ # Issue summary
2
+
3
+ The SDK is supposed to keep trying to reconnect whenever the websocket connection to the server is dead, and that is what it do most of the time, but in some rare instances, the connection drops in a way where the server shows the the device is offline, but the device logs show that the SDK assums that it is connected. Result is that a manual restart is needed as the device SDK never detects that is it offline/disconnected so the reconnect loop never triggers.
4
+
5
+ # Issue details and findings
6
+
7
+ • Most likely root cause: the client got stuck in a half-closed TCP state (CLOSE-WAIT) where the websocket reader stopped consuming bytes, so reconnect logic never
8
+ triggered.
9
+
10
+ What I found (read-only):
11
+
12
+ 1. Live process is still running since 2026-03-04 08:15:27 UTC with no restart.
13
+ 2. Last Portacode log line is 2026-03-04 08:28:58 UTC; no logs after that.
14
+ 3. The only Portacode network socket is:
15
+ - 10.10.0.136:41718 -> 94.204.184.54:443 in CLOSE-WAIT
16
+ - kernel Recv-Q is 177 bytes (unread data still queued)
17
+ - FD is 6 in process 7061
18
+ 4. Restart policy is Restart=on-failure, but this process never exits, so systemd won’t restart it.
19
+
20
+ Code correlation (installed portacode==1.4.35 matches tag v1.4.35 exactly):
21
+
22
+ 1. Receive loop only reconnects when recv() raises websockets.ConnectionClosed:
23
+ - /home/user/debo/portacode/portacode/connection/client.py:172:172
24
+ - /home/user/debo/portacode/portacode/connection/client.py:203:203
25
+ 2. It wraps recv() in asyncio.wait_for(..., timeout=1.0) and cancels it every second on idle:
26
+ - /home/user/debo/portacode/portacode/connection/client.py:176:176
27
+ 3. Clock-sync-based forced close is only initial burst (5 times), not continuous:
28
+ - /home/user/debo/portacode/portacode/connection/client.py:231:231
29
+
30
+ Inference:
31
+
32
+ - You hit a rare wedge where the websocket path stopped draining/closing after peer-side close (or close frame), leaving FD in CLOSE-WAIT.
33
+ - Because the process didn’t crash and no close exception propagated, reconnect loop never ran, and systemd didn’t intervene.
34
+ - Limited ongoing heartbeat (only initial clock sync) removes a second safety net once this wedge happens.
35
+
36
+ Confidence: medium-high on “stuck reader in CLOSE-WAIT without reconnect”; medium on exact trigger being the per-second wait_for(recv) cancellation race vs a lower-level
37
+ websockets/SSL edge case.
38
+
39
+ # Suggestions
40
+
41
+ 1. WebSocket protocol keepalive (Ping/Pong)
42
+ Enable client-side idle ping at a low frequency (for example every 60-180s only when no traffic). This uses WebSocket control frames, not JSON payloads, and a
43
+ compliant server stack auto-replies with Pong without custom server code. If pong is missed, client force-closes and reconnects.
44
+ 2. TCP keepalive probes
45
+ Enable and tune socket keepalive (SO_KEEPALIVE, TCP_KEEPIDLE, TCP_KEEPINTVL, TCP_KEEPCNT) on the client connection. This is kernel-level liveness detection with
46
+ minimal overhead and no app-channel traffic. If peer becomes unreachable/half-dead, TCP eventually errors, allowing client reconnect logic to trigger.
47
+
48
+
49
+ # Resolution
50
+
51
+ Suggestion 1 has been implemented in the commit on which this issue file was added and the issue file is kept to further monitor the issue in case of any future reoccurence. The file may be removed in 3 month later if the issue never reoccured. Scheduled file detetion is 4th of Jun 2026