calm-artist-7206
08/20/2026, 6:52 PMfresh-machine-37269
08/20/2026, 6:52 PMfresh-machine-37269
08/20/2026, 6:57 PMLocalAgentServerBackend (backends/local.py) just points at the already-running agent server. dispatcher.py uploads a tarball (the automation's preset code) and runs it, extracting into a run-isolated work dir: ~/.openhands/workspaces/automation-runs/{run_id}/ (see get_work_dir).
2. The repo clone happens inside the run. The preset tarball's generated script (presets/{plugin,prompt}/sdk_main.py) calls workspace.clone_repos(), which ultimately invokes the SDK's clone_repos() ā _clone_single_repo() (openhands/sdk/workspace/repo.py). That does a plain `git clone`:
⢠git clone --depth 1 [--branch <ref>] <url> <dest> for branch/tag refs (shallow)
⢠a full git clone + checkout <sha> for commit-SHA refs
So it's a fresh clone into the run's isolated workspace each time ā no worktree, no reuse of a prior clone.
3. Worktrees exist in the SDK but aren't used here. The SDK's StartConversationRequest has a worktree flag (default False) that would create a dedicated worktree under /tmp/conversation-worktrees/.... The automation's generated script never sets it, so automations never take that path.
The one place a "preexisting clone" is reused: the separate git-sync feature (git_sync/client.py, ensure_repo). But that's a different mechanism ā it maintains a persistent checkout of the config repo that stores automation definitions (prompts/plugins) so it can round-trip edits back to git. It clones once and reuses that working copy on subsequent sync cycles. It is not how a fired automation obtains the repo it's supposed to act on.
TL;DR ā local automations do a fresh, shallow-where-possible git clone inside the run's isolated workspace. No worktrees, no shared pre-existing clone.calm-artist-7206
08/20/2026, 7:19 PMfresh-machine-37269
08/20/2026, 7:19 PMAutomationRun row with a fresh random UUID (models.py, id = uuid.uuid4).
⢠dispatcher.py passes that run id into backend.get_work_dir(run_id), and local mode resolves it to:~/.openhands/workspaces/automation-runs/{run_id}/(base dir configurable via `WORKSPACE_BASE`/`AUTOMATION_WORKSPACE_BASE`, defaults to ~/.openhands/workspaces).
The workspace name is the run's UUID ā literally automation-runs/<uuid>. Two firings = two different UUIDs = two different directories. There's no reuse by automation name or by "first run".
It clones fresh each time.
⢠Inside the run, the preset script reads repos_config.json and calls workspace.clone_repos(repos_config) with no explicit target, so it clones into the run's own working_dir (that UUID-named folder).
⢠clone_repos() ā _clone_single_repo() runs a plain git clone (shallow --depth 1 for branch/tag refs, full clone for commit SHAs) into a repo-named subfolder under that directory.
So: run 1 ā clone into ~/.openhands/workspaces/automation-runs/<uuid-1>/<repo>/, run 2 ā fresh clone into ~/.openhands/workspaces/automation-runs/<uuid-2>/<repo>/. Nothing is persisted or reused between runs, and it never uses worktrees or a pre-existing clone.calm-artist-7206
08/20/2026, 7:24 PMfresh-machine-37269
08/20/2026, 7:27 PMprompt column is NULL (preset prompt automations set this).
⢠preset_metadata is NULL (preset automations populate this).
⢠entrypoint is python3 main.py (preset automations use .venv/bin/python main.py, since presets build their own venv).
⢠Their tarball upload names are slack-channel-monitor-all-joined / slack-monitor-openhands-ai.tar.gz ā not the prompt-automation-⦠or plugin-automation-⦠prefixes that preset_router.py assigns when creating presets.
So: custom SDK code, uploaded as a tarball, not a preset. (The third, "Agent Canvas weekly KPI Slack summary," is instead a prompt preset ā created with prompt-automation-⦠and a .venv/bin/python main.py entrypoint.)
One caveat: I'm reading from the sqlite export at /tmp/automations-export/automations.db, so this reflects that snapshot ā if the live DB differs it could be out of date.