Feature: External Session Registration API — let terminal agents register sessions without a workspace

Author: ryakerCreated Mar 29, 2026Updated Sep 11, 2026

Summary

VK is designed for humans to create tasks and launch agents from the UI. But a significant chunk of real-world AI coding work starts differently: from a terminal claude or gemini command, a parallel worktree, or a Zora task dispatched from a message queue. None of these sessions appear in VK today — the board is stale while the work is happening.

This issue proposes a minimal API addition that would let any external agent register itself with VK so it shows up in the session list and can be tracked.


Use case

A developer runs:

bash
cd ~/Dev/my-app
git checkout -b fix/issue-42
claude

Claude Code starts working. At this moment, VK knows nothing about this session. The board card sits in Backlog while the agent is already deep into the fix.

With this API, a global SessionStart hook (e.g. ~/.claude/settings.json) could call:

POST /api/sessions/external
{
  "runtime": "claude_code",
  "project_path": "/Users/ryaker/Dev/my-app",
  "branch": "fix/issue-42",
  "workspace_id": "optional-uuid-if-known"
}

And VK would display the session alongside workspace-based sessions.


Proposed API

Endpoint

POST /api/sessions/external

Request body

json
{
  "workspace_id": "uuid (optional — link to VK workspace if known)",
  "name": "string (optional — human-readable label)",
  "runtime": "claude_code | gemini | zora | unknown",
  "project_path": "/absolute/path/to/repo",
  "branch": "fix/issue-42",
  "pid": 12345
}

Response

json
{
  "session_id": "uuid",
  "workspace_id": "uuid | null"
}

Status update

PATCH /api/sessions/{session_id}
{ "status": "in_progress" | "in_review" | "done" | "blocked" }

(Or reuse the existing PUT endpoint with an additional status field.)


Proposed implementation

1. Migration — add source + external fields to sessions

sql
ALTER TABLE sessions ADD COLUMN source TEXT NOT NULL DEFAULT 'internal';
ALTER TABLE sessions ADD COLUMN external_runtime TEXT;
ALTER TABLE sessions ADD COLUMN external_project_path TEXT;
ALTER TABLE sessions ADD COLUMN external_branch TEXT;
ALTER TABLE sessions ADD COLUMN external_pid INTEGER;
ALTER TABLE sessions ADD COLUMN external_status TEXT DEFAULT 'in_progress';

source: 'internal' (VK-created, workspace-required) or 'external' (registered externally).

2. Model change — workspace_id becomes nullable for external sessions

rust
// crates/db/src/models/session.rs
pub struct Session {
    pub id: Uuid,
    pub workspace_id: Option<Uuid>,   // was: Uuid — now nullable for external sessions
    pub name: Option<String>,
    pub executor: Option<String>,
    pub source: String,               // new: "internal" | "external"
    pub external_runtime: Option<String>,
    pub external_project_path: Option<String>,
    pub external_branch: Option<String>,
    pub external_pid: Option<i64>,
    pub external_status: Option<String>,
    // ...
}

3. New request type

rust
// crates/server/src/routes/sessions/mod.rs
#[derive(Debug, Deserialize, TS)]
pub struct CreateExternalSessionRequest {
    pub workspace_id: Option<Uuid>,
    pub name: Option<String>,
    pub runtime: Option<String>,
    pub project_path: Option<String>,
    pub branch: Option<String>,
    pub pid: Option<i64>,
}

4. New handler

rust
pub async fn create_external_session(
    State(deployment): State<DeploymentImpl>,
    Json(payload): Json<CreateExternalSessionRequest>,
) -> Result<ResponseJson<ApiResponse<Session>>, ApiError> {
    let pool = &deployment.db().pool;

    // If workspace_id provided, verify it exists
    if let Some(ws_id) = payload.workspace_id {
        Workspace::find_by_id(pool, ws_id)
            .await?
            .ok_or(ApiError::Workspace(WorkspaceError::ValidationError(
                "Workspace not found".to_string(),
            )))?;
    }

    let session = Session::create_external(pool, &CreateExternalSession {
        workspace_id: payload.workspace_id,
        name: payload.name,
        runtime: payload.runtime,
        project_path: payload.project_path,
        branch: payload.branch,
        pid: payload.pid,
    }).await?;

    Ok(ResponseJson(ApiResponse::success(session)))
}

5. Route registration

rust
// In sessions::router()
.route("/external", post(create_external_session))

Frontend impact

External sessions would show in the sessions list with a visual indicator (e.g. a terminal icon vs. the VK robot icon). No launch/follow-up controls needed — external sessions are read-only from VK's perspective.

Minimal frontend change: filter source === 'external' sessions into their own section or badge them differently.


Why not a separate table?

Reusing sessions means:

  • External sessions appear naturally wherever sessions are listed
  • No join required for combined views
  • Less schema surface area
  • The source field makes the distinction clear

Prior art / motivation

I've built vk-bridge — a sidecar service that solves this today by maintaining its own session registry and mapping sessions to VK cards via the /api/remote/* endpoints. It works, but it's a workaround. A first-class external session API in VK would let vk-bridge (and any other integration) register sessions in a way VK's UI could actually display and track.

Happy to contribute a PR for this if the design direction sounds right.