Persistent project service
Run registered Linux projects in the background and inspect their status and operational logs.
Run these commands inside your Linux VM or multi-project devcontainer, as
the control account used during installation. For the test VM, that is ubuntu.
Exit any convoy project shell sessions and stop foreground convoy connect
processes first. Each project permits one execution owner.
You need CLI 0.74.1 and matching backend code and the administrator setup from Isolated projects on Linux. To create and control a container from your Mac, use Managed local environments. A normal single-project devcontainer must first be configured for the project-isolation launcher.
Start and inspect
With A and B already registered and their runtimes authenticated:
convoy project list
convoy service start -b
convoy service status
convoy service logs --followservice start confirms that the service and local control socket are ready. Projects
may still be connecting. service status reports each profile as connecting,
online, retrying, blocked, failed or stopped. Use --json for machine-readable
status. A project that cannot connect is listed under Warnings: with the fix, for
example a: failed · Project is in use by a private project shell. Stop it, then run service reload --profile a.
Warnings name a held project lease, a sandbox that did not start, a rejected API
key, an unreachable backend and a missing runtime. They also cover a runtime
that is not logged in or not root-owned, and those stay while the project is
online. The TUI shows the same warnings in the environment details and marks the
project with ! in the sidebar.
To check every precondition at once, run convoy project check a. It lists the
sandbox boundary, each runtime (installed root-owned, config folder, logged in),
the project lease and the backend key, with the fix for each failed check. It
changes nothing and works while the service is stopped. The TUI offers it as
[e] Check project in the project's Actions.
Trigger a task in project A and another in B through Convoy's web UI. Ask each agent to create a file in its own workspace and read it back. Check that both profiles are online and that their run events appear:
convoy service logs --profile a --follow
convoy service logs --profile b --jsonThese are operational logs: connection states, run lifecycle events and worker exits. Read the full agent conversation in Convoy's web UI. The service does not retain raw model output, backend error text or credentials.
Press Ctrl+C to close the log viewer. Close the VM terminal, reopen it, then run
convoy service status to confirm the service continued. Multiple log viewers
can run independently. convoy service start without --background runs in the
foreground; Ctrl+C there stops the service and its workers.
--background is the long form of -b.
Stop and restart
convoy service stop
convoy service logsStop waits for project workers to drain. The launcher allows up to 30 seconds before forcing its sandbox to exit. Registry-lock waits are bounded and cancellable. The service has a 35-second fallback for its own launcher children and reports an error if cleanup cannot be confirmed. Workspace files, runtime sign-in and browser state remain. Retained logs are readable after stopping. For host commands, filtering, recent output and the stopped-container limitation, see Diagnose a stopped service.
To restart every project, stop the service, then run convoy service start -b
again. Normal project registration changes do not require this restart.
Add and remove projects while running
With A and B running, register C in another terminal inside the same environment:
convoy project add c --profile-key c
convoy project list
convoy service logs --profile c --followAdd prepares C, saves its registration and asks the running service to connect it.
“Registered” does not mean “online.” A connection failure retains the registration
and files; inspect project list for status. A and B keep running. With no service,
add reports an offline registration and shows the start command.
For runtime login, run:
convoy project login c --runtime codexIt pauses only C, runs codex login --device-auth in C's private shell, then
reloads C and waits until it is online. Add --interrupt if C has active runs;
use --runtime claude-code for Claude Code. The TUI offers [i] Log in to Codex
and [o] Log in to Claude Code in the project's Actions.
For other work in the private shell, pause C yourself:
convoy service pause c --interrupt
convoy project shell c
exit
convoy service reload --profile c --interruptA and B continue running. Pausing retains C's registration and private files; it lasts until targeted reload or a service restart. Exit the shell before reloading, since it holds C's execution lease. Provider account management remains separate.
To remove C while preserving its files and private state:
convoy project remove c --yes --interrupt--yes confirms unregistration. --interrupt separately permits stopping C's live
connection and active work. Without it, removal refuses and reports the observed
active-run count. Even zero observed runs requires interruption permission because
new work can arrive. A and B continue. An independently opened project shell or
foreground connection must be stopped in its own terminal.
The service checks configuration every two seconds. Invalid configuration leaves
existing workers running. Changed or externally removed live connections show
pending until you explicitly apply their changes:
convoy service reload
convoy service reload --profile c --interruptThe targeted command can also retry a failed or kicked connection after you repair its configuration. It stops only the selected service connection. Profile aliases share one identity; status uses the first profile key in sorted order. Removing one alias preserves the worker if another remains.
Do not edit config from the desktop or an external editor at the same time as
project commands. If a command loses its service response, inspect project list
and service status before retrying; the operation may already have completed.
Upgrade the service
Stop before updating the installed CLI. On the existing test VM, install the new package into the same root-owned prefix used during setup:
convoy service stop
sudo npm install --prefix /opt/convoy ./useconvoy-cli-0.74.1.tgz
convoy --versionUse the candidate archive when testing an unpublished change. If upgrading from a launcher older than 0.67.2, refresh it using your saved installation paths below. For an existing 0.73.3 installation, this phase needs no launcher refresh. For older installations, use the installer below to keep the launcher current. Then start the service:
sudo python3 -I - <<'PYTHON'
import json
import pwd
import subprocess
from pathlib import Path
config = json.loads(Path("/etc/convoy-projects.json").read_text())
installer = Path(config["package_root"]) / "dist/templates/project-isolation/install.py"
subprocess.run([
"/usr/bin/python3", "-I", str(installer),
"--control-user", pwd.getpwuid(config["control_uid"]).pw_name,
"--workspace", config["workspace"],
"--package-root", config["package_root"],
"--node", config["node"],
"--state", config["state"],
], check=True)
PYTHON
convoy service start -bThe installer preserves the existing runtime acceptance settings when these paths are unchanged. No browser download is required. Rebuilt project images include the updated launcher.
Failures and recovery
- Already running: use
convoy service status. Duplicate starts are rejected. - Failed profile: check
convoy project list, stop the service, then runconvoy connect --profile afor the connection diagnostic. Check that runtime acceptance is enabled and the project-scoped API key remains valid. An open project shell or direct connection also holds the project's execution lease. - Kicked connection: the service reports blocked after exit code 3 and does not automatically take the connection back from another owner.
- Crash: exit code 1 retries three times after 1, 2 and 4 seconds, then stays
failed. Use
convoy service reload --profile a --interruptafter correcting the cause. - Service owner died: its launcher pipes close, which stops the workers. A new start recovers the stale socket using the kernel lock. It never kills an arbitrary PID from a stale file. A worker still draining may temporarily retain its execution lease.
- Unsafe state permissions:
~/.convoy/serviceand its parent.convoymust belong to the control account with mode 0700. Log and lock files require mode 0600. Symlinks and writable untrusted ancestors are rejected.
Service state lives in ~/.convoy/service, separate from project repositories.
Operational logs rotate across two files of at most 1 MiB each; the live API
retains the latest 1,000 entries and reports a gap if a viewer falls behind.
The private Unix socket has mode 0600 and is outside each project's filesystem
view. There is no public control port. The service supports up to 128 project
identities and profile keys of at most 80 characters.
Verify the environment
Before relying on a new VM or rebuilt image, test the full workflow above with
real agents. Confirm A and B can each edit their own files, cannot read each
other's private state, and stop after service stop. Check that a direct
connection cannot run concurrently with the service for the same project.
Process-fixture tests verify service mechanics; they do not certify a particular
VM's runtime or filesystem isolation.
For live-registration acceptance, keep real tasks running in A/B while adding C. Confirm C appears, becomes online after its runtime setup, and can complete a task. Verify removal without --interrupt refuses, then remove C with --interrupt and check A/B stayed connected and C's files remain. Repeat an identical add, repair an invalid config, test targeted reload and verify offline registration after stopping the service. Run these checks on the VM and the rebuilt distributed image.
Attach from multiple terminals
Run as the control account inside the execution environment:
# Terminal 1
convoy attach --profile a
# Terminal 2
convoy attach --profile b --jsonAttach shows a profile-health snapshot followed by execution changes. It does
not start the service. Ctrl+C detaches only that viewer. If the service restarts,
attach reconnects and reports the lost event window before showing a fresh
snapshot. Slow viewers disconnect rather than blocking agents or other clients.
For a host-managed Docker container, run the same command on your Mac with
--env dev. See Managed local environments.
Inspect execution and transcripts
Use actual IDs from each command's output in the next command:
convoy service runs list --profile a --limit 10 --json
convoy service runs get THREAD_ID --profile a
convoy service runs history THREAD_ID --profile a --limit 5 --json
convoy service runs logs THREAD_ID --profile a --message MESSAGE_ID --follow
convoy service runs cancel THREAD_ID --profile a --run RUN_IDList includes your project's execution threads, including group children, ordered
by recent activity. It shows execution status and whether messages are queued.
These are separate from a task's To Do/In Progress/Done status. History lists
assistant runs, newest first. Use continueCursor with --cursor on list or
history until isDone is true; an empty page does not necessarily mean the end.
Logs emits normalized stored events, including text, tool activity and usage when
the runtime supplies it. It excludes ephemeral token fragments. Oversized individual
events carry an explicit truncation marker; open the web conversation for their
full content. These transcripts are fetched from Convex and are not stored in the
service's operational logs. convoy service logs remains the operational-log view.
For resumable transcript output use --json. Each row has an order; resume with
--after LAST_ORDER. Follow ends when the selected run finishes. A backend or
connection failure exits visibly; repeat this read-only command to reconnect.
Ctrl+C leaves the run active.
Cancellation requires the exact active run ID. If it has ended and another run has started, cancellation fails without stopping the new run. A repeated cancellation with a new request ID may report that the selected run is no longer active. To check a lost command response, use the request ID and epoch reported by the client:
convoy service outcome REQUEST_ID --epoch SERVICE_EPOCHOutcomes are retained for at most ten minutes and 1024 mutations in one service
lifetime. unknown does not mean the command failed. Inspect state before issuing
a new command. Cancel also accepts --request-id for callers that track their own
IDs. No command automatically replays an uncertain mutation after reconnect.
convoy service bridge is the internal stdin/stdout transport used by the host
Docker and SSH clients (remote targets). Stdout
carries bounded versioned NDJSON only. It requires the same control-account access as other management commands; no TCP control port
or agent MCP management tool is exposed.
Two-terminal A/B acceptance
After updating the backend and installing the candidate CLI on your VM:
- Start the service and confirm A/B are online. Open two control-account terminals and attach one to A and the other to B.
- Send a real task to each project from the web app. In another terminal, run
service runs listfor each profile. Inspect a thread and paginate its history. - Follow A's selected message in one terminal and B's in the other. Confirm each
prints the correct run's text/tool events. Press Ctrl+C in A's viewer. B and both
workers must continue. Resume A with the last JSON
order. - Cancel one active run using its history
runId. B must continue. Reusing the old run ID after another run starts must not stop the newer run. - Keep an attach viewer open while pausing/reloading a profile from another terminal. Both viewers should observe the changed health. Stop/restart the service and confirm attach resnapshots; agents stop on service shutdown.
- From
convoy project shell a, try connecting to the control account's socket path with a Unix-socket client. Access must fail. The socket remains mode 0600 under the control account's private directory, and has no TCP equivalent.
Run the same exercise in the rebuilt multi-project devcontainer. Automated tests use real CLI subprocesses and a test backend. They do not replace this real-agent VM/container acceptance.
Viewer failures and polling
Plain output shows tables, run details and text/tool events. Use --json for
structured output. A truncated log event is marked with its bounded preview.
When list/history has another page, the CLI prints its --cursor argument.
An idle runs logs --follow backs off to one request every five seconds and
resets after new output arrives. A blocked output pipe detaches the client with
an error after 512 KiB is queued or output takes ten seconds to drain. Agents
continue running. A silent subscribed service is detected after 15 seconds.
Stop the old service before upgrading. If an incompatible service is still
running, use the previous CLI's convoy service stop, then start the new version
with convoy service start -b. There is no legacy protocol fallback.
The internal bridge drains outstanding replies when stdin ends. Incomplete frames fail, and lost replies identify requests with unknown outcomes. Do not blindly repeat a mutation after a lost reply; inspect state and its outcome first.
Run these commands from your host
The managed environment workflow provisions a
local container with convoy run --environment dev. Add --environment dev to
attachment, project and service commands to target it. Existing direct VM commands
keep their current behavior.
Interactive service browser
CLI 0.95.1 adds convoy tui to browse an existing service. Use
convoy tui --environment <name> to select a saved managed Docker target.
The sidebar lists connections and projects; d opens Details, r opens Runs,
Enter opens a thread's execution history, and q detaches without stopping agents.
In CLI 0.107.0, c on an environment opens Credentials. It manages the managed API
keys of that environment: add with a masked field, rotate, roll back, disable, delete
an unassigned key, and
assign each key to projects with a checklist you save explicitly. A project's Actions
also has Assign or unassign credentials, including native login. Changes apply after
you reload the project. Profile keys on your own machine stay in convoy keys.
Both stdin and stdout must be interactive terminals. For scripts or redirected
output, use convoy service status --json or
convoy service runs list --profile <key> --json instead. The TUI exits with code 1
and a plain diagnostic outside a terminal. It does not start a service or update
one automatically. Incompatible services show update instructions in Details.