Address recurring review feedback on the docker reconnect path:
- reconnectToContainer was 98 lines (over the 80-line repo limit). Split
into ensureContainerRunning (inspect + start + re-inspect) and
bringupAgentctl (health-check, find-or-create instance, transparent
re-handshake). The orchestration body now reads top-down at 49 lines.
- resolveDockerEndpoint and waitForAgentctlHealth grew real branches in
this PR (host-port lookup with container-IP fallback; 240×500ms
retry loop) but had no direct tests. Introduce narrow hostPortLookup
/ healthChecker interfaces, satisfied by *docker.Client and
*agentctl.ControlClient, so both functions are now driven by stubs
that exercise happy-path, fallback, retry-until-healthy, exhaustion,
and context-cancellation branches.
- Add direct unit tests for the docker package's pure helpers
(normalizeDockerHostIP, buildDockerPortBindings) and for
dockerAgentctlPortBindings — a regression in the published port
range or host IP would silently break container reconnect.
Backend:
- service_task_environments: fail closed when checkAnySessionRunning
errors so a transient DB hiccup can't bypass the running-session
guard and tear down a live container/sandbox; teardown is now truly
best-effort with errors.Join across container/sandbox/worktree, so a
failed Docker call doesn't permanently orphan the worktree on retry.
Adds tests for the running-check error path and the multi-resource
best-effort path.
- executor_execute: propagate GetTaskEnvironmentByTaskID errors instead
of silently launching a fresh environment that orphans existing
containers/worktrees; gate environment reuse on matching ExecutorType
so a profile switch doesn't pass stale PreviousExecutionID/container
IDs to the wrong backend.
- task_environment_handlers: reject malformed JSON on POST
/tasks/:id/environment/reset (still allow empty body) so a destructive
endpoint doesn't silently proceed with a bad payload.
- worktree (PushEnvironmentBranch): bound git push with a 30s context
timeout and disable interactive prompts via GIT_TERMINAL_PROMPT=0
so a stalled remote can't hang the request thread.
- manager_environment: explicitly call ensureClient() before
ContainerMgr() in DestroyContainer/GetContainerLiveStatus so a missing
Docker daemon surfaces as "docker unavailable" rather than the
generic "container manager not initialized".
- container: WorkingDir always uses the in-container /workspace path
rather than the host workspace path (Docker would happily start in an
unrelated directory in host bind-mount mode); bootstrap now logs the
prepare-script exit code so failures show up in container logs.
- docker/client: CreateContainerInteractive honors ContainerConfig.Entrypoint
so the same config struct produces consistent behavior on both creation
paths.
Frontend:
- executor-settings-button: extract useTaskEnvironment hook (component
shrinks below the 100-line limit); only treat 404 as empty env (other
errors no longer wipe a valid environment); track "loaded once" via a
ref so loadEnv doesn't depend on env state — that dep was creating a
new function identity on every successful poll, cancelling the
3-second interval and turning the popover into a tight loop hammering
/environment/live; copy-shell command uses sh instead of bash so it
works on user-built minimal images.
- task-reset-env-confirm-dialog: AlertDialogAction onClick calls
e.preventDefault() so Radix's auto-close doesn't dismiss the dialog
before the async reset completes (loading spinner now visible; close
is parent-controlled via onOpenChange).
- remote-credentials-card: track env method in selectedIds the same way
files/gh_cli_token are tracked, and let initialChoice match either
selectedIds OR envSecretId. First-time env-credential setup for an
agent that exposes both files+env was broken: clicking the env radio
never updated state, choice re-derived to "none", and the secret
picker disappeared before the user could pick anything.
- e2e/executor-reuse: select the acknowledge checkbox by label, not by
positional last(), so adding another checkbox to the dialog can't
silently target the wrong control.
Format pass on prettier-flagged files; gofmt fixed two whitespace
nits in lifecycle files.
Make the Docker executor work with a Dockerfile users can actually build,
without depending on a maintainer-shipped image:
- The container now launches a small bootstrap (`sh -c "$KANDEV_PREPARE_SCRIPT
; exec /usr/local/bin/agentctl"`) regardless of the image's ENTRYPOINT.
The agent CLI is no longer wired through container Cmd; agentctl receives
it later via its CreateInstance HTTP API.
- Host agentctl-linux binary is mounted into the container at
/usr/local/bin/agentctl via the existing AgentctlResolver. User-built
images don't have to bake it in.
- Default Dockerfile in the executor profile UI replaced with a working
node:22-slim + git + curl image — runs npx-based ACP agents (Claude,
Codex, OpenCode, Auggie) out of the box.
- lifecycleAdapter now exposes RequiresCloneURL / ShouldApplyPreferredShell
so the orchestrator can correctly identify containerized executors and
resolve the clone URL. Without this, applyRepositoryConfig short-circuits
on `e.capabilities == nil`, the cloneURL is never resolved, and the
prepare script fails with `repository '{{repository.clone_url}}' does
not exist`.
- applyRepositoryConfig also surfaces the resolved clone URL to script
metadata so the prepare script can interpolate {{repository.clone_url}}.
- docker.ContainerConfig gains an Entrypoint field, plumbed to the docker
client.
Verified end-to-end on a local stack with the new minimal image: the
container starts, bootstrap clones the GitHub repo, agentctl launches and
hands off to claude-acp inside the container.
* feat(web): add file browser with syntax-highlighted viewer
- Add file browser component in right panel to browse workspace files
- Open files in left panel tabs alongside Notes/Changes/Chat
- Color-coded dots for file types (TS, JS, Go, Python, etc.)
- Visual separator between main tabs and file tabs
- Folders sorted to top in file tree view
- LRU eviction for max 4 open file tabs
- Backend handlers for workspace file tree and content via WebSocket
* fix(web): refresh file tree on filesystem changes via fsnotify
- Subscribe to file change notifications from agentctl's fsnotify watcher
- Backend lifecycle manager now connects to file changes stream
- Publishes FileChangeNotified events to event bus
- WebSocket broadcasts workspace.file.changes to task subscribers
- Frontend file browser refreshes tree when changes detected
* perf(agentctl): use fsnotify instead of polling for git status
- Replace 2-second polling with fsnotify-triggered updates
- Add debouncing (300ms) to batch rapid file changes
- Git status updates now triggered by actual filesystem changes
- More efficient and responsive than periodic polling
* refactor(agentctl): consolidate fs events into single debounced trigger
- Single fsChangeTrigger channel for all filesystem events
- After 300ms debounce: update git status, files, and notify subscribers
- Removed per-file notifications in favor of single 'refresh' event
- Simpler and more efficient than separate triggers
* refactor(agentctl): simplify file API - use HTTP for tree/content
- Remove unused filesConn, fileTreeConn, fileContentConn from Client struct
- Convert RequestFileTree and RequestFileContent from WebSocket to HTTP GET
- Remove StreamFiles and CloseFilesStream (redundant with file changes stream)
- Keep only fileChangesConn for the debounced refresh notification
HTTP is simpler than WebSocket for request/response patterns.
* feat(web): switch to CodeMirror for file viewer
- Replace react-syntax-highlighter with @uiw/react-codemirror
- Add language extensions for JS/TS, Python, Go, Rust, Java, C++, CSS, HTML, JSON, Markdown, YAML
- Use VS Code dark theme for consistent look
- Enable code folding and read-only mode
- Better performance and features than syntax highlighter
* refactor: consolidate duplicated types and extract shared constants
- Re-export file types from process package in agentctl/client.go instead of duplicating
- Move OpenFileTab type to lib/types/backend.ts (shared by file-browser and page-client)
- Extract FILE_EXTENSION_COLORS constant for file type color indicators
- Remove console.log statement and unused imports in file-browser.tsx
- Add 'refresh' operation to FileChangeNotificationPayload type
* refactor(agentctl): move Close() method from acp.go to client.go
Close() is a general client cleanup method that closes all streams
(ACP, permissions, git status, file changes). It belongs in client.go
since acp.go should only contain ACP-specific communication logic.
* fix(web): remove unused cn import in file-browser
* refactor(agentctl): consolidate duplicated types into process/types.go
- Create process/types.go with all workspace-related types
- Remove duplicate GitStatusUpdate, FileInfo from workspace_tracker.go
- Remove duplicate GitStatusUpdate, FileInfo from api/workspace.go
- Re-export all types from agent/agentctl/client.go for convenience
Types are now defined once and imported everywhere.
* chore: remove package-lock.json, add to .gitignore
Project uses pnpm (pnpm-lock.yaml), not npm.
* fix(orchestrator): probe agent before returning stale execution state
When checking task.execution, the backend now probes the actual agent:
- For tracked instances: health check via agentctl
- For Docker: verify container is running
If the agent is not running but DB says 'running', the session is
marked as stopped and false is returned. This fixes the UI showing
agents as running after backend restart when they're actually dead.
Changes:
- Add IsAgentRunningForTask to AgentManagerClient interface
- Add IsContainerRunning to docker client
- Update GetExecution to verify agent is actually running
- Clean up stale sessions in database when agent not found
* fix(orchestrator): use agentctl status to verify agent is running
Simplified approach - probe agentctl's /api/v1/status endpoint to check
if agent process is running. Same check for both Docker and standalone.
- Add IsAgentRunning() helper to StatusResponse
- Simplify IsAgentRunningForTask to just use agentctl.GetStatus()
* fix(orchestrator): also verify agent running for in-memory executions
The previous fix only checked agent status when loading from DB. But if
an execution is cached in memory and the agent process stops (while
agentctl stays running), the stale state was still returned.
Now both paths verify the agent is actually running by probing agentctl.
If agent stopped, clean up in-memory state and update DB session.
* chore(web): remove unused react-syntax-highlighter
Switched to CodeMirror for code viewing, no longer needed.
* fix(tests): add IsAgentRunningForTask to mock agent managers
Add the new interface method to test mocks:
- mockAgentManager in scheduler_test.go
- SimulatedAgentManagerClient in orchestrator_test.go
---------
Co-authored-by: Kandev Agent <kandev@example.com>
- Add Docker reconciliation on lifecycle manager startup to find running containers
- Add container_id field to agent_sessions table for cleanup tracking
- Add GetRecoveredInstances to AgentManagerClient interface
- Sync executor cache with recovered Docker instances on startup
- Add task.execution WebSocket endpoint to check agent running state
- Fix Stop Agent button showing Start Agent after page refresh
- Ensure containers are not leaked when backend restarts
- Use acp-go-sdk for all ACP communication in agentctl
- Mark agent as completed when Prompt() returns successfully
- Update task state to COMPLETED when agent completes
- Update task state to FAILED when agent fails and retries exhausted
- Enhanced E2E test to verify agent actually creates files
- E2E test now validates full flow: task creation -> agent work -> file creation -> task completion