c0ce55ff9d
* fix(gateway): snapshot agentctl status and env id on session subscribe Late-subscribing clients (page reload, task switch, WS reconnect) miss the agentctl_ready and task_environment_id events fired during the original launch. Without them, the env-routed shell terminal gate stays stuck and the panel hangs forever on "Connecting terminal...". - appendSessionStateMessage now carries task_environment_id so the frontend can seed environmentIdBySessionId for the active session. - New appendAgentctlStatusMessage snapshots current agentctl readiness via a bounded Health probe and emits session.agentctl_ready / _starting to flip the client gate out of `undefined`. - GetOrEnsureExecutionForEnvironment now publishes AgentctlStarting to mirror EnsureWorkspaceExecutionForSession and keep gate transitions monotonic. * fix(web): drop shell-mode agentctl gate to unblock env-routed terminals The shell terminal gate required agentctlStatus.isReady to flip to ready before opening the WS, but that event may have fired before the client subscribed (page reload, task switch, local-executor task switch). The lazy create-on-terminal-connect path on the backend never fires either, because the WS never opens — chicken-egg. Backend env handler already waits for remote workspace readiness server-side (waitForRemoteExecutionReadyWithTimeout), so the client just needs the routing keys (env id + session id). Agent-mode terminals still gate on readiness — the agent process must be wired before passthrough I/O is meaningful. Extract computeCanConnect helper to keep PassthroughTerminal under the complexity ceiling. * test: cover terminal-hang on subscribe snapshot and env-routed gating Adds a backend unit test asserting appendSessionStateMessage carries task_environment_id (the deterministic RED for the env-map seed bug), plus E2E scenarios for the user-facing repros: cold load, kanban switch, hard reload, sidebar switch, and a local-executor task switch that used to poison the terminal for every subsequent task. Adds an expectTerminalConnected helper to SessionPage to detect the "Connecting terminal..." overlay. * fix(repo): heal task_environments on boot and enforce one env per task Live DB audit on the user's machine surfaced two corrupt-data classes that manifest as the user-facing "terminal doesn't appear / Failed to fetch terminals: 503" symptoms: - 15 worktree-mode envs with worktree_path set but workspace_path empty. GetOrEnsureExecutionForEnvironment returns ErrSessionWorkspaceNotReady forever, the env terminal handler 503s. - 3 tasks with two task_environments rows (race in lazy create — pairs 4-6 s apart, both ready, distinct workspace_path). healTaskEnvironmentWorkspacePaths backfills workspace_path = worktree_path on the affected rows, and healDuplicateTaskEnvironments collapses each duplicated task to its most recently updated env (re-pointing any sessions still on the loser, then dropping the loser row). ensureTaskEnvironmentTaskUniqueIndex adds a UNIQUE index on task_environments(task_id) so a future race fails loud. Order matters: heal must run first to drop duplicates before the unique index is created. * fix(repo): reject empty workspace_path on worktree envs The orchestrator's persistTaskEnvironment update branch previously left workspace_path untouched, so an env created with empty workspace_path would stay corrupt forever — terminals 503 with ErrSessionWorkspaceNotReady. Tighten the repository boundary: CreateTaskEnvironment refuses worktree-mode inserts without workspace_path, and UpdateTaskEnvironment refuses to clear it back to empty. A future writer regression now fails loud at the boundary instead of producing a row that breaks the env terminal handler. * fix(executor): refresh paths on env update and dedupe by task The persistTaskEnvironment update branch only mutated AgentExecutionID + Status, so envs created earlier with an empty workspace_path stayed permanently broken across launches. Refresh worktree + workspace fields on update via a shared computeWorkspacePath helper used by both branches. Concurrent launches for the same task previously raced inside this function — each saw existingEnv == nil and each called CreateTaskEnvironment, producing duplicate rows ~4-6 s apart in the live DB. Hold a per-task mutex (mirroring sessionLocks) and re-fetch the env inside the critical section so siblings reuse the row the first launch persisted. The repo's unique index on task_environments(task_id) is the durable guarantee; the lock just closes the window before the constraint trips. * test(repo): cover task_environments heal, validation, unique index Repository unit tests for the new heal steps and writer guards: - HealTaskEnvironmentWorkspacePaths_BackfillsEmpty / LeavesPopulatedAlone / Idempotent - HealDuplicateTaskEnvironments_KeepsMostRecent (asserts session re-link to winner) / NoOpWhenSingle - EnsureTaskEnvironmentTaskUniqueIndex_BlocksFutureDuplicates - CreateTaskEnvironment_RejectsEmptyWorkspaceForWorktree / AllowsNonWorktreeWithEmptyWorkspace - UpdateTaskEnvironment_RejectsClearingWorkspaceForWorktree Tests live in the sqlite package so they can exercise the unexported heal* helpers directly, mirroring how the boot path calls them. * fix: address PR review feedback - helpers.go: drop the 2 s blocking Health probe in appendAgentctlStatusMessage. sendSessionData runs in the WS read loop, so the probe was delaying every subscribe/focus ACK by up to 2 s. Use GetWorkspaceStream() != nil as the cached readiness signal — the stream manager only attaches it after Health succeeds, so its presence is a free, non-blocking proxy. Subsequent waitForAgentctlReady events still correct the snapshot if it picks the wrong one. - manager_execution.go: publish AgentctlStarting INSIDE createExecution, before spawning waitForAgentctlReady. The previous order published it after createExecution returned, which let the goroutine race past on fast Health and deliver Ready before Starting. Drop the now-redundant publishes in EnsureWorkspaceExecutionForSession and the env lazy-create path. - task_environment.go: replace the local executorTypeWorktree constant with models.ExecutorTypeWorktree (canonical) so a future rename of the type is caught by the compiler. - base.go: heal_workspace_path now respects the kandev path layout — task-dir-mode envs (.../.kandev/tasks/<name>/<repo>) get Dir(worktree_path) as workspace_path, mirroring computeWorkspacePath in the orchestrator. Plain worktree mode unchanged. - heal_test.go: tighten unique-constraint assertion to require a UNIQUE error string so the test fails loudly if a different DB error masks the constraint violation. Add task-dir-mode heal coverage.