Skip to content

Concepts

This page defines the vocabulary vise uses across the CLI, the API and the event stream. Everything here reflects what v0.1.0 does, not a roadmap.

A session is one run of an agent against one prompt. It is created with a POST /sessions (or vise sessions create) carrying three things:

  • input: the prompt.
  • agent: the harness to run, an optional model hint, optional instructions prepended to the prompt, and a (currently unused) list of MCP servers.
  • environment: where the agent works. See Environments.

Session IDs look like ses_01k5c3ab7ke8xz7t2m9qn4rw6d: a ses_ prefix and a 26-character, time-ordered identifier, so IDs sort by creation time.

pending ──claim──▶ running ──finish──▶ completed
│ failed
│ cancelled
└── lease expires ──▶ failed ("lease expired")
Status Meaning
pending Queued, waiting for a host to claim it
running Claimed by a host, which holds a lease on it
completed The agent finished its turn; stop_reason says how (for example end_turn)
failed Workspace preparation or the agent run failed (error has the message), or the host’s lease expired
cancelled Cancellation was requested and the host stopped the agent

Scheduling is first-in, first-out: a host’s claim takes the oldest pending session regardless of harness or environment. POST /sessions/{id}/cancel cancels a pending session immediately; on a running one it sets cancel_requested, and the host stops the agent when it sees the flag on its next heartbeat. Terminal sessions cannot be cancelled.

Everything that happens in a session is appended to its event log with a monotonically increasing seq. GET /sessions/{id}/events?after_seq=N pages through it, and GET /sessions/{id}/events/stream tails it over Server-Sent Events, closing with a done event once the session is terminal (and, for a session that opened a PR, once the PR has merged or closed).

Three kinds of payload appear in the log:

  • ACP session updates from the harness: agent_message_chunk, agent_thought_chunk, tool_call, tool_call_update and plan, wrapped as { "sessionId": ..., "update": { "sessionUpdate": ..., ... } }.
  • Permission audit records: whenever the agent asks for permission, the host auto-approves the first offered option and records both the request and the chosen option as an event. vise sessions watch renders these as [permission auto-approved].
  • PR tracking transitions written by the server after the session finishes: pr_state_changed and checks_state_changed, each with from and to.

A host is a machine you have enrolled to run sessions. Enrolling (vise hosts create <name>, or POST /hosts) returns a bearer token with a vhost_ prefix exactly once; the server stores only its SHA-256 hash. vise-host presents that token on every request and the server records the host’s last_seen_at.

vise-host is a single loop: claim a session, run it, repeat, sleeping two seconds (--poll-interval) when there is nothing to claim. It runs natively rather than in a container because it spawns the agent installed on that machine.

A claim gives the host a 60-second lease on the session. While the session runs, the host heartbeats every 20 seconds to extend the lease, and the heartbeat response carries the cancel_requested flag. The server sweeps leases every 15 seconds; a session whose lease lapses (because the host crashed or lost connectivity) is marked failed with the error lease expired, so no session stays running forever.

Every session gets a directory under the host’s temp directory at vise-sessions/<session-id>. For github_repo sessions the repository is cloned inside it; for self_hosted sessions it is an empty workspace folder. Workspaces are deleted when the session finishes unless the outcome is uncommitted_changes or the host runs with --keep-workspaces. Kept workspaces are marked with a .keep file so the host’s startup cleanup leaves them alone.

A harness is the agent runtime the host launches. The harness-to-command mapping lives in the host, not the server; v0.1.0 hardcodes two:

Harness What runs Needs
claude-code (default) Claude Code over ACP, via npx -y @agentclientprotocol/claude-agent-acp@latest claude installed and authenticated on the host, plus Node for npx
echo A built-in fake that echoes the prompt back as agent_message_chunk events Nothing

The host talks to the agent over the Agent Client Protocol: it initializes the agent, opens an ACP session rooted at the workspace, sends one prompt turn (instructions, a blank line, then the input), forwards every session update as an event, and treats the agent’s stop reason as the session’s stop_reason. For github_repo sessions the GitHub credential is passed to the agent process as GH_TOKEN and GITHUB_TOKEN, so git push and gh pr create work inside the checkout.

The environment says where the agent works:

  • self_hosted: an empty scratch directory on the host. No clone, no credentials, no outcome detection. Useful for smoke tests with echo.
  • github_repo: repo is required as owner/name; base_branch is optional and defaults to the repository’s default branch. The host asks the server for a GitHub credential, clones the repository, and refreshes the credential while the session runs. The CLI switches to this environment whenever --repo is given.

Hosts never hold a long-lived GitHub secret of their own. They call POST /hosts/sessions/{id}/credentials and the server answers with whichever credential it is configured with:

  • With a GitHub App (VISE_GITHUB_APP_ID and VISE_GITHUB_APP_PRIVATE_KEY_PATH), the server mints a short-lived installation token scoped to the session’s repository.
  • With a personal access token (VISE_GITHUB_PAT), the same token is handed to every session.

The server uses the same credential for its own PR tracking reads. If both are set, the App wins.

When a github_repo session’s agent run succeeds, the host inspects the checkout and reports one of five outcomes on POST /hosts/sessions/{id}/finish:

Outcome How it is detected
no_changes Still on the base branch at the base commit, working tree clean
uncommitted_changes Dirty working tree, or commits that were never pushed (locally-only work). The workspace is kept
pushed_no_pr The current branch is pushed and matches the remote, but no open PR has it as head
pr_opened The branch is pushed and an open PR has it as head; pr_url is set
pr_updated A follow-up session pushed to the head branch of the PR it was addressing

self_hosted sessions have no outcome.

A session whose outcome is pr_opened becomes a tracking root. The server’s poller visits it every 60 seconds (VISE_PR_POLL_INTERVAL_SECS) until the PR merges or closes, and records a derived snapshot on the session:

  • pr_status.state: review_pending, changes_requested, approved, merged, closed, or sync_error when the PR could not be read (persistent 403 or 404; retried every tick and cleared once a fetch succeeds). The state is reduced from GitHub’s review list: the latest review per reviewer wins, an outstanding request for changes beats approvals, and approvals on an older commit are stale. Raw review comments are never stored.
  • pr_status.checks: pending, passing or failing, from the check runs on the head commit. Absent when the head commit has no check runs.
  • pr_status.last_synced_at: when the snapshot was last refreshed.

Every change is appended to the root session’s events as pr_state_changed or checks_state_changed, which is what lets vise sessions watch on a finished session keep tailing until the PR is merged or closed.

POST /sessions/{id}/follow-up (or vise sessions follow-up) creates a new session that addresses review feedback on an existing session’s PR:

  • It inherits the parent’s agent configuration unless the request overrides it.
  • Its environment is the same repository with base_branch set to the PR’s head branch, so its pushes update the same PR.
  • Its input is composed server-side at creation time: the current review threads with file and line context, the names of failing checks, and any extra instructions you pass.
  • parent_session_id points at the session it was spawned from. Follow-ups can chain, but PR tracking always lives on the root session that opened the PR, resolved by walking the parent chain.

Every host and session belongs to exactly one workspace, the tenant boundary in the schema. Host names are unique within a workspace, and a host only ever claims sessions from its own workspace. The migrations seed a single workspace named default, and vise-server puts everything there: out of the box vise is single-tenant, there are no workspace routes in the API, and nothing in the CLI or host flow mentions workspaces. The column is there so a hosting layer built on vise-core can keep many tenants in one database.

vise-server is one process with four jobs, backed by Postgres:

  1. The HTTP API on port 3000: the /sessions routes the CLI uses and the /hosts routes hosts use, with Swagger UI at /docs. The openapi/openapi.json document is committed and the Rust client is generated from it.
  2. Scheduling: claims hand out pending sessions FIFO with row locking, so concurrent hosts never claim the same session.
  3. The lease sweeper: every 15 seconds, fails sessions whose lease lapsed.
  4. The PR tracker: polls PRs as described above.

It applies its own database migrations on startup, so a release image or binary needs nothing beyond DATABASE_URL and a GitHub credential.

Method and path Who calls it Purpose
GET /sessions CLI List sessions
POST /sessions CLI Create a session
GET /sessions/{id} CLI, host Fetch one session
GET /sessions/{id}/events CLI Page through events
GET /sessions/{id}/events/stream CLI Tail events over SSE (not in the OpenAPI document)
POST /sessions/{id}/cancel CLI Request cancellation
POST /sessions/{id}/follow-up CLI Spawn a follow-up session
POST /hosts CLI Enroll a host; returns the token once
GET /hosts CLI List hosts
POST /hosts/claim host Claim the next pending session
POST /hosts/sessions/{id}/heartbeat host Extend the lease; learn of cancellation
POST /hosts/sessions/{id}/events host Report a batch of events
POST /hosts/sessions/{id}/credentials host Obtain a GitHub credential
POST /hosts/sessions/{id}/finish host Record the terminal status and outcome

The /hosts/claim and /hosts/sessions/* routes require a host bearer token. The /sessions routes are unauthenticated in v0.1.0; the installer binds the API to 127.0.0.1 only, and you should keep it that way unless you put something in front of it.