A

Autonomous Agents Knowledge Module

Autonomous Agents Dev Server Sessions — Knowledge Module

Autonomous Agents knowledge module — UI selectors, data model, and page states documenting Dev Server Sessions.

Available free v1.32.0 Browser
$ sidebutton install agents
Download ZIP

Dev Server Sessions

The contract for the dev server inside a live editing session: the project's own dev server runs on the agent VM and the user watches it through the portal while chatting with the agent. This module is what the dev-server turn of app_edit_session (see ops/) runs on — read it when the session is asked to start the dev server (the "Boot the project — start the dev server" chip, or any message asking for it) and whenever a running preview misbehaves. The boot turn does not read it: since v3 (KAN-64) the boot orients on the target and posts the ready report, the role persona is its only pack read, and nothing is synced, installed or started until the user asks.

Two lanes carry the work, and confusing them is the usual source of bugs. The live lane is the dev server: sub-second feedback, VM-local, gone when the VM is gone. The durable lane is git — a real commit pushed to the project's branch at every turn end. It survives spot reclaim, it is what the user can share, and for a repo project it is the only durable lane: the landing floor's auto-republish is switched off for projects that have an origin remote (SCRUM-1965), precisely so there is one answer to "where did my work go". The platform's per-turn autosave (SP-D, SCRUM-1937) mirrors uncommitted work to a scratch ref as a spot-reclaim safety net; it is not a substitute for the push.

Preview path (why localhost is not a limitation)

browser → portal (auth) → relay → agent daemon :9876 /api/preview/<port>/* → 127.0.0.1:<port>

Every hop above the daemon is authenticated, and the daemon hard-pins the upstream to 127.0.0.1. The /api/preview/ passthrough itself ships with SP-B/SP-C (SCRUM-1935/1936) — until those land the dev server is simply unreachable from outside the VM, which changes nothing about how the agent must start it. So:

  • Bind the dev server to the IPv4 loopback, explicitly. Verified on an agent VM: astro dev with no host flag listens on [::1]:4321 onlyss -ltn shows the IPv6 loopback, the startup banner still says "Local http://localhost:4321/", and curl http://127.0.0.1:4321/ is refused. The passthrough dials 127.0.0.1, so the preview is dead while every log line says the server is fine. Pass the flag: --host 127.0.0.1 (Astro, Vite), -H 127.0.0.1 (Next).
  • Never bind 0.0.0.0. The VM opens no new ports for previews; a public bind adds exposure and buys nothing.
  • The port must be reachable and stable — the passthrough addresses it by number, and an allowlist/range applies. Use the convention below, or the port the dispatch passed explicitly.

Port conventions

StackDev portDev command (loopback-bound)
Astro4321npm run dev -- --host 127.0.0.1
Vite / SvelteKit5173npm run dev -- --host 127.0.0.1
Next.js3000npm run dev -- -H 127.0.0.1
Otherread package.json scripts, then the framework defaultits own host flag

An explicit dev_port param always wins over the convention. Read package.json rather than guessing the script name — dev, start and preview mean different things per project, and preview serves a stale build.

Readiness is "answers HTTP", not "200 on /"

Poll curl -sS -o /dev/null -w '%{http_code}' http://127.0.0.1:<port>/ until it prints a real status; 000 means nothing is listening yet. Any status proves the server is serving — a project with no route at / (the landing kit is one: its pages are /example and /product-tour-example) answers 404 while being perfectly healthy, and a check that demands 200 on / would fail forever against it. A PID or a "ready in 250 ms" log line proves nothing: that is exactly what the IPv6-bind failure above also prints.

The entry route the dev-server turn names follows from the same fact: / is not necessarily a page. Use the dispatch's preview_path when it is set, otherwise /, otherwise the real first page from src/pages or the router — and curl it before naming it, so the preview does not open on a route that 404s.

HMR behind two proxies

The browser reaches the page through the portal and the daemon, so Vite-family HMR needs to be told what the browser sees, not what the server binds. Without this the page loads and the websocket then fails in a retry loop, which looks like "the preview is frozen".

// vite.config.* (Astro: the `vite` key in astro.config.*)
server: {
  host: '127.0.0.1',
  allowedHosts: ['app.sidebutton.com'],  // the Host header arriving through the relay
  hmr: { clientPort: 443, protocol: 'wss' },  // the browser connects over TLS on the public origin
  fs: { strict: true },                        // dev servers are dev-grade: keep the tree boundary
}
  • Keep it out of the user's committed config where the framework allows it. Every turn publishes the worktree, so a hard-coded app.sidebutton.com / clientPort: 443 in vite.config.* follows the project home and breaks the user's own npm run dev. Prefer the CLI flags and a local-only override (vite.config.local.*, an env-guarded block); commit the values only when there is no other way, and say so in the reply.
  • allowedHosts — Vite rejects unknown Host headers with a 403 blocked-request page; the proxied host must be listed.
  • hmr.clientPort / protocol — the client would otherwise dial the dev port directly on the public origin and never connect.
  • Treat the preview route as an untrusted-input surface: fs.strict on, no dev-only debug endpoints, nothing sensitive readable from the served tree.

The dev-server turn's report

Starting the dev server is a chat turn, not the boot, so its reply is conversation: no verdict token, no SUGGESTIONS: block required. What it must carry is the same set of facts the boot used to name, because the preview zone has nothing to show until they are true:

  1. Dev command as actually run — with the loopback host flag — and whether the server was started or reused.
  2. Port — the number the passthrough dials.
  3. Entry routecurl-verified, per the readiness rule above.
  4. Local URLhttp://127.0.0.1:<port><route>.

Failure is reported the same way the boot used to report it: the last ~40 lines of ~/dev-server.log and a plain statement that nothing answered — never a BOOT_FAILED token, which belongs to the boot turn alone.

The boot report itself is the workflow's contract (ops/app_edit_session.yaml, v3) and names no port: one status line (target · branch @ sha · ahead/behind · role), a 3–5-sentence kickoff, a SUGGESTIONS: block of 3–4 chips led by "Boot the project — start the dev server" for a checkout target, then SESSION_READY alone on the last line (BOOT_FAILED only when the target folder is missing). The status line is the role's ONLY slot in that report (KAN-63): step 2 is a parsed contract with no free slot, and a role line after the suggestions block stops the portal recognising the block at all. A session with no role, or a role that ships no skill://agents/<slug>.md, simply names none. Token-only matching: the gate never reads the prose, and display surfaces strip that line. Submitting the report is the readiness signal: the portal keys connected on the boot job completing plus its liveness probe, never on an artifact upload (SCRUM-1965), so the boot takes no screenshot and needs no chrome binary — and since v3 it starts no server either, so connected means the agent is oriented and listening, well under a minute on a warm VM. The preview appears once the dev-server turn has run.

The route list for the page switcher is Phase 2, not part of either report — Phase 1 sessions are single-page.

Session lifecycle

The session-open job completes on its boot turn — that is the design, not a failure. Chat then continues on the same still-alive Claude session, one turn per user message.

PhaseWhat the agent doesWhat the user sees
Bootorient on the target (one look: ls, README head, git status -sb — no network) → role persona → kickoff report with chips → SESSION_READY; no sync, no install, no server"preparing your workspace" → connected when the report lands (under a minute warm)
Dev server — on requestread this module → install deps only if missing → start detached, loopback-bound → verify it answers → resolve the entry route → reply with command, port, route, URLthe preview zone flips from "no dev server" to the live frame
Turnone user message = one turn; edit, commit, push, reply, end the turnlive preview updates + the reply + the save stamp
Idlenothing; the dev server keeps servinglive preview stays up
End / reclaimthe pushed commits on each touched checkout's branch
  • Never hold a turn open. No tailing logs, no waiting on the server, no background work you intend to "come back to". The reply reaches the user when the turn ends.
  • The dev server must survive turn end and the 60-minute session tidy: start it detached (setsid nohup … &), never as a foreground child of the turn.
  • Idle TTL (60 min, refreshed by every message) and the per-account session cap end sessions on purpose — held agents cost money. Ending is normal; the durable lane is what persists.
  • Reconnect is a fresh dispatch onto the same workspace, not a resurrection. A healthy server is usually still listening: probe the port and reuse it. Booting a second one either dies on EADDRINUSE or lands on a port the preview cannot reach.

Turn end: commit, push, stamp

The rule is uniform for every repo, every turn: commit with a real message, then push, in every checkout the turn touched. Not a branch-per-turn, not a scratch ref — the branch the checkout is on (main for a project session). Consequences the agent must internalise:

  • Every turn end is a publish. Leave the tree building and coherent; do not end a turn mid-refactor with the app broken if you can help it, and say so when you must.
  • A rejected push is rebased once, retried once, then reported. Concurrent sessions can share one main. git pull --rebase and push again — once. If it is still rejected, stop: say it in the reply, keep the commit local, and push it at the start of the next turn's push. Never git push --force, and never reset to origin to "fix" a rejection — the rejection means someone else's commits are on that branch.
  • Commit early and often, with real messages. wip / autosave tell the user nothing, and the message is what the save stamp and the ship surface show back to them.
  • The user sees the outcome as a stamp. Saved · <sha7> · pushed when the push landed, Saved (local) when it was rejected and the commit is riding into the next turn, not saved when the turn committed nothing. The reply should agree with the stamp — never claim a push that was rejected.
  • Never destroy commits. No git reset --hard, no git checkout -- ., no force-push — on a reconnect the local branch may carry commits that exist nowhere else. Undo by reverting forward.
  • Pull only on request, and only --ff-only. The boot syncs nothing (v3): a git pull inside the Claude turn moves HEAD after the Stop hook took its session-start baseline, so upstream commits get counted as the session's own work. The boot reports each checkout's ahead/behind as of the last fetch and offers a "Pull updates" chip when it is behind; the pull-repos job (ops/agent_pull_repos.yaml) moves checkouts between sessions. A pull that cannot fast-forward leaves the tree and says so, and a hard reset to origin at session start is exactly how a reconnect eats the previous turn's work.
  • Artifacts live outside every worktree, in the workspace's own artifacts/ folder. Screenshots, reports and datasets go in <workspace root>/artifacts/{{entry_path}}/artifacts, which the workflow creates before Claude starts. The Stop hook's artifacts lane uploads from exactly ONE directory — the first of <cwd>/artifacts, ~/workspace/artifacts, ~/artifacts that exists — and with the session's cwd at the workspace root (v3) that first candidate is the workspace's folder, whatever the workspace is called. A screenshot written into a checkout instead rides the next push into the project.

Never park credentials

  • The only credential an agent VM holds is sb_token in ~/.agent-env. The cloud MCP token is never copied onto a VM.
  • Nothing secret is written into the project worktree — no .env with real values, no tokens in config, fixtures or committed scripts. The worktree is published every turn, so a parked secret becomes a published secret.
  • Publish endpoints vend what they need server-side; if a task appears to need a new credential on the VM, that is a signal to stop and ask, not to paste one in.

Gotchas

  • Declaring ready off a PID or the startup banner. A started process is not a reachable one — verify with curl against 127.0.0.1 before reporting ready.
  • The IPv6-only bind. The single most likely reason a "healthy" session shows a dead preview. Check ss -ltn | grep <port>: [::1] is broken, 127.0.0.1 is right.
  • npm run preview instead of dev. It serves a build — no HMR, and the user's edits appear to do nothing.
  • A second dev server on a reconnect. Probe first; reuse a healthy one.
  • Starting the dev server at boot. The boot orients and reports; a server nobody asked for costs the cold-start minutes v3 removed from the boot and can collide with a reconnect's still-healthy one. Start it on the chip or a message, after reading this module.
  • Syncing at boot. git fetch/git pull inside the Claude turn moves HEAD past the Stop hook's baseline and misattributes upstream commits to the session. The "Pull updates" chip and the pull-repos job are the two doors.
  • Screenshotting in the boot turn. There is no screenshot step any more, and readiness never waits on chrome. Evidence captured later in a session still belongs in <workspace root>/artifacts/, never inside a checkout — the checkouts are pushed.
  • Committing without pushing. The commit dies with the spot VM. A turn that ends with an unpushed commit must say so.
  • allowedHosts forgotten. The preview renders Vite's "Blocked request" page, which reads like an app error rather than a config gap.