diff --git a/README.md b/README.md index 87004ca..50ef022 100644 --- a/README.md +++ b/README.md @@ -301,7 +301,7 @@ Production uses `workers=1` (PTY state is process-local), `threads=16` (concurre 📁 Project Structure ``` -coding-agents-in-databricks/ +coding-agents-databricks-apps/ ├── app.py # Flask backend + PTY management + setup orchestration ├── app_state.py # Shared app state (setup progress, session registry) ├── app.yaml.template # Databricks Apps deployment config template diff --git a/app.py b/app.py index 514c4bc..71ac4f4 100644 --- a/app.py +++ b/app.py @@ -43,6 +43,7 @@ SESSION_TIMEOUT_SECONDS = 86400 # No poll for 24 hours = dead session CLEANUP_INTERVAL_SECONDS = 900 # Check for stale sessions every 15 min GRACEFUL_SHUTDOWN_WAIT = 3 # Seconds to wait after SIGHUP before SIGKILL +MAX_CONCURRENT_SESSIONS = int(os.environ.get("MAX_CONCURRENT_SESSIONS", "5")) # Logging setup logging.basicConfig(level=logging.INFO) @@ -604,7 +605,7 @@ def read_pty_output(session_id, fd): try: readable, _, errors = select.select([fd], [], [fd], 0.05) if readable or errors: - output = os.read(fd, 4096) + output = os.read(fd, 65536) if not output: # EOF — process exited break @@ -956,6 +957,11 @@ def configure_pat(): @app.route("/api/session", methods=["POST"]) def create_session(): """Create a new terminal session.""" + # Quick reject before forking a PTY (approximate — authoritative check below) + with sessions_lock: + if len(sessions) >= MAX_CONCURRENT_SESSIONS: + return jsonify({"error": f"Maximum {MAX_CONCURRENT_SESSIONS} concurrent sessions reached. Close an existing session first."}), 429 + data = request.get_json(silent=True) or {} label = data.get("label", "") try: @@ -997,6 +1003,15 @@ def create_session(): session_id = str(uuid.uuid4()) with sessions_lock: + # Authoritative check under the same lock as insertion — prevents + # TOCTOU race where two concurrent requests both pass the early check. + if len(sessions) >= MAX_CONCURRENT_SESSIONS: + os.close(master_fd) + try: + os.kill(pid, signal.SIGKILL) + except OSError: + pass + return jsonify({"error": f"Maximum {MAX_CONCURRENT_SESSIONS} concurrent sessions reached. Close an existing session first."}), 429 sessions[session_id] = { "master_fd": master_fd, "pid": pid, diff --git a/app.yaml b/app.yaml index e6bb8cd..5596e08 100644 --- a/app.yaml +++ b/app.yaml @@ -12,3 +12,5 @@ env: value: databricks-gpt-5-3-codex - name: CLAUDE_CODE_DISABLE_AUTO_MEMORY value: 0 + - name: MAX_CONCURRENT_SESSIONS + value: "5" diff --git a/docs/deployment.md b/docs/deployment.md index 09959da..36267c6 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -11,7 +11,7 @@ The simplest way — no CLI, no cloning, everything stays in the Databricks UI. 1. Go to **Databricks → Apps → Create App** 2. Choose **Custom App** and connect this Git repo: ``` - https://github.com/datasciencemonkey/coding-agents-in-databricks.git + https://github.com/datasciencemonkey/coding-agents-databricks-apps.git ``` 3. Click **Deploy** 4. Open the app — on first terminal session, paste a short-lived PAT when prompted @@ -30,8 +30,8 @@ If you prefer working from the terminal or need more control: ```bash databricks repos create \ - --url https://github.com/datasciencemonkey/coding-agents-in-databricks.git \ - --path /Workspace/Users//apps/coding-agents-in-databricks + --url https://github.com/datasciencemonkey/coding-agents-databricks-apps.git \ + --path /Workspace/Users//apps/coding-agents-databricks-apps ``` ### 2. Configure `app.yaml` @@ -56,7 +56,7 @@ No secrets or resources to configure. On first terminal session, paste a short-l ```bash databricks apps deploy \ - --source-code-path /Workspace/Users//apps/coding-agents-in-databricks + --source-code-path /Workspace/Users//apps/coding-agents-databricks-apps ``` > **Tip:** To update later, just `git pull` in the workspace repo and re-deploy. diff --git a/pyproject.toml b/pyproject.toml index 02cb9ca..774238c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "coda" -version = "0.16.7" +version = "0.17.0" description = "CoDA - Coding Agents on Databricks Apps" requires-python = ">=3.10" dependencies = [ @@ -9,8 +9,7 @@ dependencies = [ "simple-websocket>=1.0", "claude-agent-sdk", "databricks-sdk>=0.20.0", - "mlflow-tracing>=3.4", - "opentelemetry-exporter-otlp-proto-grpc", + "mlflow-skinny==3.10.1", "requests", "cryptography>=46.0.6", ] diff --git a/requirements.txt b/requirements.txt index b7f405c..46f445a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,7 @@ # This file was autogenerated by uv via the following command: # uv pip compile pyproject.toml -o requirements.txt +annotated-doc==0.0.4 + # via fastapi annotated-types==0.7.0 # via pydantic anyio==4.13.0 @@ -20,7 +22,7 @@ blinker==1.9.0 # flask # flask-socketio cachetools==7.0.5 - # via mlflow-tracing + # via mlflow-skinny certifi==2026.2.25 # via # httpcore @@ -36,7 +38,10 @@ click==8.3.1 # via # flask # flask-socketio + # mlflow-skinny # uvicorn +cloudpickle==3.1.2 + # via mlflow-skinny cryptography==46.0.6 # via # coda (pyproject.toml) @@ -45,19 +50,21 @@ cryptography==46.0.6 databricks-sdk==0.102.0 # via # coda (pyproject.toml) - # mlflow-tracing + # mlflow-skinny +fastapi==0.135.3 + # via mlflow-skinny flask==3.1.3 # via # coda (pyproject.toml) # flask-socketio flask-socketio==5.6.1 # via coda (pyproject.toml) +gitdb==4.0.12 + # via gitpython +gitpython==3.1.46 + # via mlflow-skinny google-auth==2.49.1 # via databricks-sdk -googleapis-common-protos==1.73.1 - # via opentelemetry-exporter-otlp-proto-grpc -grpcio==1.80.0 - # via opentelemetry-exporter-otlp-proto-grpc h11==0.16.0 # via # httpcore @@ -75,7 +82,9 @@ idna==3.11 # httpx # requests importlib-metadata==8.7.1 - # via opentelemetry-api + # via + # mlflow-skinny + # opentelemetry-api itsdangerous==2.2.0 # via flask jinja2==3.1.6 @@ -93,36 +102,25 @@ markupsafe==3.0.3 # werkzeug mcp==1.26.0 # via claude-agent-sdk -mlflow-tracing==3.10.1 +mlflow-skinny==3.10.1 # via coda (pyproject.toml) opentelemetry-api==1.40.0 # via - # mlflow-tracing - # opentelemetry-exporter-otlp-proto-grpc + # mlflow-skinny # opentelemetry-sdk # opentelemetry-semantic-conventions -opentelemetry-exporter-otlp-proto-common==1.40.0 - # via opentelemetry-exporter-otlp-proto-grpc -opentelemetry-exporter-otlp-proto-grpc==1.40.0 - # via coda (pyproject.toml) opentelemetry-proto==1.40.0 - # via - # mlflow-tracing - # opentelemetry-exporter-otlp-proto-common - # opentelemetry-exporter-otlp-proto-grpc + # via mlflow-skinny opentelemetry-sdk==1.40.0 - # via - # mlflow-tracing - # opentelemetry-exporter-otlp-proto-grpc + # via mlflow-skinny opentelemetry-semantic-conventions==0.61b0 # via opentelemetry-sdk packaging==26.0 - # via mlflow-tracing + # via mlflow-skinny protobuf==6.33.6 # via # databricks-sdk - # googleapis-common-protos - # mlflow-tracing + # mlflow-skinny # opentelemetry-proto pyasn1==0.6.3 # via pyasn1-modules @@ -132,8 +130,9 @@ pycparser==3.0 # via cffi pydantic==2.12.5 # via + # fastapi # mcp - # mlflow-tracing + # mlflow-skinny # pydantic-settings pydantic-core==2.41.5 # via pydantic @@ -142,13 +141,17 @@ pydantic-settings==2.13.1 pyjwt==2.12.1 # via mcp python-dotenv==1.2.2 - # via pydantic-settings + # via + # mlflow-skinny + # pydantic-settings python-engineio==4.13.1 # via python-socketio python-multipart==0.0.22 # via mcp python-socketio==5.16.1 # via flask-socketio +pyyaml==6.0.3 + # via mlflow-skinny referencing==0.37.0 # via # jsonschema @@ -157,6 +160,7 @@ requests @ git+https://github.com/psf/requests@bc04dfd6dad4cb02cd92f5daa81eb562d # via # coda (pyproject.toml) # databricks-sdk + # mlflow-skinny rpds-py==0.30.0 # via # jsonschema @@ -165,19 +169,24 @@ simple-websocket==1.1.0 # via # coda (pyproject.toml) # python-engineio +smmap==5.0.3 + # via gitdb +sqlparse==0.5.5 + # via mlflow-skinny sse-starlette==3.3.4 # via mcp starlette==1.0.0 # via + # fastapi # mcp # sse-starlette typing-extensions==4.15.0 # via # anyio - # grpcio + # fastapi # mcp + # mlflow-skinny # opentelemetry-api - # opentelemetry-exporter-otlp-proto-grpc # opentelemetry-sdk # opentelemetry-semantic-conventions # pydantic @@ -187,13 +196,16 @@ typing-extensions==4.15.0 # typing-inspection typing-inspection==0.4.2 # via + # fastapi # mcp # pydantic # pydantic-settings urllib3==2.6.3 # via requests uvicorn==0.42.0 - # via mcp + # via + # mcp + # mlflow-skinny werkzeug==3.1.7 # via # flask diff --git a/static/index.html b/static/index.html index bb7eecf..d27965e 100644 --- a/static/index.html +++ b/static/index.html @@ -142,6 +142,11 @@ border-color: rgba(100,150,255,0.25); box-shadow: 0 0 8px rgba(100,150,255,0.1); } + #session-count-label { + margin-left: auto; padding: 0 12px; + font-size: 11px; color: rgba(255,255,255,0.4); + white-space: nowrap; flex-shrink: 0; + } #toolbar .font-size-row { display: flex; align-items: center; gap: 4px; } @@ -375,6 +380,7 @@

General

+
@@ -383,6 +389,7 @@

General

+