-
Notifications
You must be signed in to change notification settings - Fork 8
feat: OpenCode support, performance improvements, WebSocket fix #68
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
dgokeeffe
wants to merge
5
commits into
datasciencemonkey:main
Choose a base branch
from
dgokeeffe:pr/perf-opencode-websocket
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
1c46062
feat: OpenCode fork install, GitHub CLI, reduce keystroke latency
dgokeeffe 8735bbe
perf: reduce global lock contention in batch polling
dgokeeffe 5726c37
fix: detect real WebSocket vs Socket.IO long-polling fallback
dgokeeffe 029c9e6
refactor: extract gh install to shell script
dgokeeffe fa824ac
feat: add clean and create-pat Makefile targets
dgokeeffe File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| # Makefile for deploying Coding Agents to Databricks Apps | ||
| # | ||
| # Usage: | ||
| # make deploy PROFILE=daveok # full deploy (prompts for PAT interactively) | ||
| # make redeploy PROFILE=daveok # skip secret setup, just sync + deploy | ||
| # make create-pat PROFILE=daveok # auto-generate a 90-day PAT and store it | ||
| # make status PROFILE=daveok # check app status | ||
| # make logs PROFILE=daveok # tail app logs | ||
| # make clean PROFILE=daveok # remove app and secret scope | ||
|
|
||
| # Configuration | ||
| PROFILE ?= DEFAULT | ||
| APP_NAME ?= coding-agents | ||
| SECRET_SCOPE ?= $(APP_NAME)-secrets | ||
| SECRET_KEY ?= databricks-token | ||
|
|
||
| # Resolve user email and workspace path from the profile | ||
| USER_EMAIL = $(shell databricks current-user me --profile $(PROFILE) --output json 2>/dev/null | python3 -c "import sys,json; print(json.load(sys.stdin).get('userName',''))") | ||
| WORKSPACE_PATH = /Workspace/Users/$(USER_EMAIL)/apps/$(APP_NAME) | ||
|
|
||
| .PHONY: help deploy redeploy create-app setup-secret sync deploy-app status logs clean-secret clean create-pat | ||
|
|
||
| help: ## Show this help | ||
| @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-18s\033[0m %s\n", $$1, $$2}' | ||
|
|
||
| deploy: create-app setup-secret sync deploy-app ## Full deploy: create app, set secret, sync, deploy | ||
| @echo "" | ||
| @echo "Deployment complete! App URL:" | ||
| @databricks apps get $(APP_NAME) --profile $(PROFILE) --output json 2>/dev/null | python3 -c "import sys,json; print(json.load(sys.stdin).get('url','(pending)'))" | ||
|
|
||
| redeploy: sync deploy-app ## Redeploy: sync + deploy (skip secret setup) | ||
| @echo "" | ||
| @echo "Redeployment complete!" | ||
|
|
||
| create-app: ## Create the Databricks App (idempotent) | ||
| @echo "==> Checking if app '$(APP_NAME)' exists..." | ||
| @if databricks apps get $(APP_NAME) --profile $(PROFILE) >/dev/null 2>&1; then \ | ||
| echo " App '$(APP_NAME)' already exists, skipping create."; \ | ||
| else \ | ||
| echo " Creating app '$(APP_NAME)'..."; \ | ||
| databricks apps create $(APP_NAME) --profile $(PROFILE); \ | ||
| fi | ||
|
|
||
| setup-secret: ## Create secret scope and store PAT | ||
| @echo "==> Setting up DATABRICKS_TOKEN secret..." | ||
| @# Create scope if it doesn't exist | ||
| @if databricks secrets list-scopes --profile $(PROFILE) --output json 2>/dev/null | python3 -c "import sys,json; scopes=[s['name'] for s in json.load(sys.stdin).get('scopes',[])]; exit(0 if '$(SECRET_SCOPE)' in scopes else 1)" 2>/dev/null; then \ | ||
| echo " Secret scope '$(SECRET_SCOPE)' already exists."; \ | ||
| else \ | ||
| echo " Creating secret scope '$(SECRET_SCOPE)'..."; \ | ||
| databricks secrets create-scope $(SECRET_SCOPE) --profile $(PROFILE); \ | ||
| fi | ||
| @# Store the PAT - prompt if not provided | ||
| @if [ -z "$(PAT)" ]; then \ | ||
| echo " Enter your Databricks PAT (will not echo):"; \ | ||
| read -s pat_value && \ | ||
| echo "$$pat_value" | databricks secrets put-secret $(SECRET_SCOPE) $(SECRET_KEY) --profile $(PROFILE); \ | ||
| else \ | ||
| echo "$(PAT)" | databricks secrets put-secret $(SECRET_SCOPE) $(SECRET_KEY) --profile $(PROFILE); \ | ||
| fi | ||
| @echo " Secret stored in $(SECRET_SCOPE)/$(SECRET_KEY)" | ||
| @# Link secret to app resource | ||
| @echo " Linking secret to app resource 'DATABRICKS_TOKEN'..." | ||
| @curl -s -X PATCH \ | ||
| "$$(databricks auth env --profile $(PROFILE) 2>/dev/null | python3 -c "import sys,json; print(json.load(sys.stdin)['env']['DATABRICKS_HOST'])")/api/2.0/apps/$(APP_NAME)" \ | ||
| -H "Authorization: Bearer $$(databricks auth token --profile $(PROFILE) 2>/dev/null | python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")" \ | ||
| -H "Content-Type: application/json" \ | ||
| -d '{"resources":[{"name":"DATABRICKS_TOKEN","description":"PAT for model serving access","secret":{"scope":"$(SECRET_SCOPE)","key":"$(SECRET_KEY)","permission":"READ"}}]}' \ | ||
| >/dev/null | ||
| @echo " App resource linked." | ||
|
|
||
| sync: ## Sync local files to Databricks workspace | ||
| @echo "==> Syncing to $(WORKSPACE_PATH)..." | ||
| databricks sync . $(WORKSPACE_PATH) --watch=false --profile $(PROFILE) | ||
|
|
||
| deploy-app: ## Deploy the app from workspace | ||
| @echo "==> Deploying app '$(APP_NAME)'..." | ||
| databricks apps deploy $(APP_NAME) --source-code-path $(WORKSPACE_PATH) --profile $(PROFILE) --no-wait | ||
|
|
||
| status: ## Check app status | ||
| @databricks apps get $(APP_NAME) --profile $(PROFILE) | ||
|
|
||
| logs: ## Tail app logs | ||
| databricks apps logs $(APP_NAME) --profile $(PROFILE) | ||
|
|
||
| clean-secret: ## Remove secret scope (destructive) | ||
| @echo "==> Removing secret scope '$(SECRET_SCOPE)'..." | ||
| databricks secrets delete-scope $(SECRET_SCOPE) --profile $(PROFILE) | ||
|
mpkrass7 marked this conversation as resolved.
|
||
|
|
||
| clean: ## Remove app and secret scope (destructive) | ||
| @echo "==> Removing app '$(APP_NAME)'..." | ||
| @databricks apps delete $(APP_NAME) --profile $(PROFILE) 2>/dev/null && \ | ||
| echo " App '$(APP_NAME)' deleted." || \ | ||
| echo " App '$(APP_NAME)' not found or already deleted." | ||
| @echo "==> Removing secret scope '$(SECRET_SCOPE)'..." | ||
| @databricks secrets delete-scope $(SECRET_SCOPE) --profile $(PROFILE) 2>/dev/null && \ | ||
| echo " Secret scope '$(SECRET_SCOPE)' deleted." || \ | ||
| echo " Secret scope '$(SECRET_SCOPE)' not found or already deleted." | ||
|
|
||
| create-pat: ## Generate a 90-day PAT and store it as the app secret | ||
| @echo "==> Generating a 90-day PAT..." | ||
| @databricks tokens create --lifetime-seconds $$((90 * 24 * 60 * 60)) --comment "coding-agents (auto-generated)" --profile $(PROFILE) --output json \ | ||
| | python3 -c "import sys,json; print(json.load(sys.stdin)['token_value'])" \ | ||
| | databricks secrets put-secret $(SECRET_SCOPE) $(SECRET_KEY) --profile $(PROFILE) | ||
| @echo " PAT created and stored in $(SECRET_SCOPE)/$(SECRET_KEY)" | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -83,6 +83,7 @@ def handle_sigterm(signum, frame): | |
| "steps": [ | ||
| {"id": "git", "label": "Configuring git identity", "status": "pending", "started_at": None, "completed_at": None, "error": None}, | ||
| {"id": "micro", "label": "Installing micro editor", "status": "pending", "started_at": None, "completed_at": None, "error": None}, | ||
| {"id": "gh", "label": "Installing GitHub CLI", "status": "pending", "started_at": None, "completed_at": None, "error": None}, | ||
| {"id": "proxy", "label": "Starting content-filter proxy", "status": "pending", "started_at": None, "completed_at": None, "error": None}, | ||
| {"id": "claude", "label": "Configuring Claude CLI", "status": "pending", "started_at": None, "completed_at": None, "error": None}, | ||
| {"id": "codex", "label": "Configuring Codex CLI", "status": "pending", "started_at": None, "completed_at": None, "error": None}, | ||
|
|
@@ -252,10 +253,12 @@ def run_setup(): | |
| _run_step("micro", ["bash", "-c", | ||
| "mkdir -p ~/.local/bin && bash install_micro.sh && mv micro ~/.local/bin/ 2>/dev/null || true"]) | ||
|
|
||
| _run_step("gh", ["bash", "install_gh.sh"]) | ||
|
|
||
| # --- Content-filter proxy (must be running before OpenCode starts) --- | ||
| # Sanitizes requests/responses between OpenCode and Databricks | ||
| # (see OpenCode #5028, docs/plans/2026-03-11-litellm-empty-content-blocks-design.md) | ||
| _run_step("proxy", ["python", "setup_proxy.py"]) | ||
| _run_step("proxy", ["uv", "run", "python", "setup_proxy.py"]) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For using UV run, we'll need to update dependabot to ensure it's using uv instead of pip for package management |
||
|
|
||
| # --- Parallel agent setup (all independent of each other) --- | ||
| parallel_steps = [ | ||
|
|
@@ -487,7 +490,7 @@ def read_pty_output(session_id, fd): | |
| if session_id not in sessions: | ||
| break | ||
| try: | ||
| readable, _, errors = select.select([fd], [], [fd], 0.5) | ||
| readable, _, errors = select.select([fd], [], [fd], 0.05) | ||
|
mpkrass7 marked this conversation as resolved.
|
||
| if readable or errors: | ||
| output = os.read(fd, 4096) | ||
| if not output: | ||
|
|
@@ -569,7 +572,10 @@ def cleanup_stale_sessions(): | |
| warning_threshold = SESSION_TIMEOUT_SECONDS * 0.8 | ||
|
|
||
| with sessions_lock: | ||
| for session_id, session in sessions.items(): | ||
| session_snapshot = list(sessions.items()) | ||
|
|
||
| for session_id, session in session_snapshot: | ||
| with session["lock"]: | ||
| idle = now - session["last_poll_time"] | ||
| if idle > SESSION_TIMEOUT_SECONDS: | ||
| stale_sessions.append((session_id, session["pid"], session["master_fd"])) | ||
|
|
@@ -801,22 +807,31 @@ def get_output_batch(): | |
| outputs = {} | ||
| now = time.time() | ||
|
|
||
| # Step 1: Resolve session refs under global lock (fast dict lookups only) | ||
| resolved = {} | ||
| with sessions_lock: | ||
| for sid in session_ids: | ||
| if sid not in sessions: | ||
| continue | ||
| session = sessions[sid] | ||
| if sid in sessions: | ||
| resolved[sid] = sessions[sid] | ||
|
|
||
| # Step 2: Swap buffers under per-session locks (same pattern as get_output) | ||
| swapped = {} | ||
| for sid, session in resolved.items(): | ||
| with session["lock"]: | ||
| session["last_poll_time"] = now | ||
| buffer = session["output_buffer"] | ||
| output = "".join(buffer) | ||
| buffer.clear() | ||
| old_buffer = session["output_buffer"] | ||
| session["output_buffer"] = deque(maxlen=1000) | ||
| exited = session.get("exited", False) | ||
| timeout_warning = session.pop("timeout_warning", False) | ||
| outputs[sid] = { | ||
| "output": output, | ||
| "exited": exited, | ||
| "timeout_warning": timeout_warning | ||
| } | ||
| swapped[sid] = (old_buffer, exited, timeout_warning) | ||
|
|
||
| # Step 3: Join strings outside all locks | ||
| for sid, (old_buffer, exited, timeout_warning) in swapped.items(): | ||
| outputs[sid] = { | ||
| "output": "".join(old_buffer), | ||
| "exited": exited, | ||
| "timeout_warning": timeout_warning, | ||
| } | ||
|
|
||
| return jsonify({"outputs": outputs, "shutting_down": shutting_down}) | ||
|
|
||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| #!/bin/bash | ||
| set -e -u | ||
|
|
||
| # Install the latest GitHub CLI (gh) 2.x release to ~/.local/bin | ||
| # and create a wrapper that handles `gh auth login` for xterm.js PTY compatibility. | ||
| # | ||
| # Usage: bash install_gh.sh | ||
|
|
||
| INSTALL_DIR="${HOME}/.local/bin" | ||
| mkdir -p "${INSTALL_DIR}" | ||
|
|
||
| # --- Detect latest 2.x version from GitHub API --- | ||
| latest_json="$(curl -fsSL 'https://api.github.com/repos/cli/cli/releases/latest' 2>/dev/null)" || true | ||
|
|
||
| GH_VERSION="" | ||
| if [ -n "${latest_json:-}" ]; then | ||
| GH_VERSION="$(echo "${latest_json}" | grep -oEm1 '"tag_name":\s*"v(2\.[0-9]+\.[0-9]+)"' | grep -oE '2\.[0-9]+\.[0-9]+' || true)" | ||
| fi | ||
|
|
||
| if [ -z "${GH_VERSION}" ]; then | ||
| echo "ERROR: Could not detect latest gh 2.x release from GitHub API." >&2 | ||
| echo "Check your network connection or GitHub API rate limits." >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| echo "Installing gh v${GH_VERSION}..." | ||
|
|
||
| # --- Download and extract --- | ||
| TARBALL="/tmp/gh.tar.gz" | ||
| curl -fsSL "https://github.com/cli/cli/releases/download/v${GH_VERSION}/gh_${GH_VERSION}_linux_amd64.tar.gz" -o "${TARBALL}" | ||
| tar -xzf "${TARBALL}" -C /tmp | ||
| mv "/tmp/gh_${GH_VERSION}_linux_amd64/bin/gh" "${INSTALL_DIR}/gh" | ||
| rm -rf "${TARBALL}" "/tmp/gh_${GH_VERSION}_linux_amd64" | ||
| chmod +x "${INSTALL_DIR}/gh" | ||
|
|
||
| # --- Configure git protocol --- | ||
| "${INSTALL_DIR}/gh" config set git_protocol https 2>/dev/null || true | ||
|
|
||
| # --- Create wrapper script --- | ||
| # The wrapper intercepts `gh auth login` to pipe "Y" through the interactive | ||
| # prompt, which avoids arrow-key menus that break in xterm.js PTY sessions. | ||
| cat > "${INSTALL_DIR}/gh.wrapper" << 'WRAPPER' | ||
| #!/bin/bash | ||
| if [ "${1:-}" = "auth" ] && [ "${2:-}" = "login" ]; then | ||
| shift 2 | ||
| printf "Y\n" | ~/.local/bin/gh.real auth login -h github.com -p https -w --skip-ssh-key "$@" | ||
| exit 0 | ||
| fi | ||
| exec ~/.local/bin/gh.real "$@" | ||
| WRAPPER | ||
|
|
||
| mv "${INSTALL_DIR}/gh" "${INSTALL_DIR}/gh.real" | ||
| mv "${INSTALL_DIR}/gh.wrapper" "${INSTALL_DIR}/gh" | ||
| chmod +x "${INSTALL_DIR}/gh" | ||
|
|
||
| echo "gh v${GH_VERSION} installed to ${INSTALL_DIR}/gh" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.