Skip to content

ACP agent

mj acp makes Mjolnir look like a coding agent to any program that speaks the Agent Client Protocol. The program starts mj acp instead of a harness such as codex or claude-agent-acp, and every session it creates is a Mjolnir session: it runs on a configured target, appears in mj sessions and the web viewer, is indexed for search, and can be steered, checkpointed, and resumed like any other.

This is for programs that drive an agent themselves — a scheduler, a bot, or an editor that accepts a custom agent command. If you drive agents by hand, the terminal surface is what you want.

Point the program’s agent command at mj acp:

mj acp --workspace <name> [--profile <id>] [--target <id>] [--bundle <id>]
[--expected-runtime-identity <id>] [--on-exit keep|suspend|destroy]
FlagMeaning
--workspace <name>The workspace the sessions are created in. Required: every session lives in a workspace the dashboard and the web viewer list. Without it mj acp exits at once, listing the workspaces and how to create one (mj workspaces create <name>). A name no workspace has is refused the same way at start, checked against the running daemon’s workspaces or, when no daemon is running, against the saved ones, so no daemon is started just to refuse it.
--profile <id>The profile whose account and harness run the session. Omitted follows your saved default.
--target <id>The target the session runs on: this machine, a container, an SSH host, or an EC2 instance. Omitted follows your saved default.
--bundle <id>The bundle to provision on a managed target. Without one, the working directory the client submits becomes the project, which is what a local target needs.
--expected-runtime-identity <id>Require a saved target runtime identity before loading the harness session or accepting work. A changed or unknown runtime fails visibly.
--on-exit <policy>What happens to the sessions this process created when it exits: keep (the default), suspend, or destroy. See When the program exits.

--workspace is required. The other flags are optional, and an omitted one resolves the same way mj new resolves it:

Terminal window
mj workspaces create editor
mj acp --workspace editor
mj acp --workspace editor --profile codex-work --target builder --bundle product

The command is listed in mj --help. It connects to your daemon and starts it if it is not running, so the first session may take a moment while the daemon comes up. mj api-info reports the daemon it reached.

To start a bundle’s primary repository at a recorded full commit on a new private branch:

Terminal window
mj acp --workspace town --profile codex-work --target builder --bundle product \
--at 0123456789abcdef0123456789abcdef01234567 \
--branch town/run-123

--at requires --bundle and always checks out the bundle’s primary repository; other repositories use their defaults. Omitting --branch leaves HEAD detached. --base <revision> sets a different diff base; it defaults to --at. Without --at, --branch names an existing branch to check out. See the create-a-session contract for failure, retry, and receipt semantics. session/new waits for preparation and reports a failure before a prompt can run. Its successful session ID is the Mjolnir session ID, usable with the HTTP session endpoint to read the retained at, branch and base. Ownership and --on-exit also apply to sessions whose preparation is still running or failed; creating a session through HTTP does not let another adapter attach to it.

To constrain the runtime, add --expected-runtime-identity <saved-runtime-id>. For initial discovery, omit the constraint, call session/new without sending a prompt, and read runtime through GET /api/v1/sessions/{session_id} using the returned Mjolnir session ID. Save a non-null runtime.id for later launches. Mismatch errors retain the session ID and the same ownership/exit policy. See resolved harness runtime for comparison scope, unknown installations, retained history, and upgrade behavior.

session/new creates an ordinary Mjolnir session and answers with its id, so the id the client sees is the id mj sessions prints. Nothing about the session is special because a program started it: it is durable, it is checkpointed, it appears in the viewer, and its transcript is searchable in the session index. Session lifecycle covers exploring, suspending, resuming, and exporting one.

The response waits until provisioning has finished and the worker is ready for the first prompt. A slow container or SSH launch can therefore keep session/new pending for several minutes. A failed launch returns an error with the session id and its cause; a launch still pending after ten minutes returns an error naming the session to inspect. Other ACP requests and consumer disconnects remain responsive during this wait.

The client’s id is also the only key it needs. A prompt may name only a session the same mj acp process created, so one client cannot drive sessions that happen to live in the same daemon.

ACP methodWhat this agent does
initializeAnswers ACP v1 and claims no optional capabilities.
session/newCreates a Mjolnir session and returns its id.
session/promptRuns one turn, emits the turn’s answer, and answers with a stop reason.
session/cancelInterrupts the turn; the prompt answers cancelled.
session/updateThe one notification this agent sends, carrying the turn’s final message as an agent_message_chunk.

These are deliberately outside it:

  • No authentication methods. Signing in belongs to the profile on the controller; run mj login --profile <id> there.
  • No fs/* or terminal/* requests to the client. The workspace, the shell, and the files belong to the session’s own worker on its target, so the client is never asked to supply them — which is what makes running somewhere other than the client’s machine work.
  • No session modes or configuration options. Model and reasoning effort are not selectable through this interface yet; set them on the session afterwards from a surface that can change them.
  • No streaming. The answer arrives as one message when the turn ends rather than token by token.

A prompt contributes its text. Attachments and resource links are ignored rather than refused, because a resource link names something in the workspace the session already runs in and its agent can read it directly. A prompt that carries no text at all is refused, since there would be nothing to run.

The turnThe stop reason the client sees
finishedend_turn
cancelled by the client, or interruptedcancelled
failed, hit a quota limit, timed out, or its session stoppedrefusal

Only a finished turn reports end_turn. A program that treats end_turn as success depends on that, so a failed or quota-limited turn is never dressed up as a finished one.

A turn that stops to ask the client a question is handled differently, because this adapter has no person to ask. Mjolnir interrupts the turn and answers the prompt with an error naming the question, so a program fails visibly with a reason instead of waiting for an answer that is never coming.

The adapter exits when the program closes its side of the pipe. Any turn still running is interrupted first, because nothing can watch or steer it any more. What happens to the sessions next is the exit policy’s choice, and the policy covers every session this mj acp process created, however its turns ended: finished, refused, failed, cancelled, or cut off when the pipe closed.

--on-exitEach session is leftUse it for
keep (default)exactly as it was, live on its targeta program whose sessions a person follows up on
suspendcheckpointed, with its worker and target released; mj resume --session <id> brings it backa scheduler that wants its runs kept but not running
destroyenvironment and recovery archive removed; conversation indexed in SessionWiki; legacy source branch kepta one-shot scheduler that takes its answer from the turn

A one-shot scheduler — one that starts mj acp for a single prompt, reads the answer, and exits — should use destroy, or suspend if a person may want to look at a run later. With keep, such a scheduler leaves a live session, worker, and workspace behind every run, and has no id left to clean them up with once the adapter is gone.

suspend and destroy wait for the daemon to finish, so the adapter can take a few minutes to exit after the pipe closes: a suspension checkpoints the workspace first. A program should close standard input and then wait for the process to exit rather than kill it. SIGTERM, SIGINT, and SIGHUP also run the policy; SIGKILL cannot, and leaves sessions as keep would. Under keep a signal ends the adapter where it stands, as it always has.

destroy waits for an interrupted turn to stop before it removes a session. A turn that does not stop within a minute is not destroyed, so no work is removed while it is still being written.

The adapter exits with status 0 when every session was retired as asked. When any was not — the daemon refused, the operation failed, or it did not finish in time — the adapter exits with a non-zero status and an error on standard error that names each such session, what went wrong, and the state it was left in. A suspension is refused for a clone whose Git work is not verified as pushed, since releasing it could lose that work; mj suspend --session <id> --acknowledge-unpublished-work releases it once you have checked. A failed suspension leaves the session live, and a session that was not destroyed is still there; either way mj sessions --session <id> shows it and mj suspend or mj destroy retries. A creation still in flight when the program leaves is waited for, so its session is retired too; a creation the daemon refused made no session, so there is nothing to retire. An adapter that created no session because session/new was refused, for example for an unknown workspace, exits with a non-zero status and that refusal on standard error. A refused request reaches the program as a JSON-RPC error whose message is the refusal itself.

Sessions kept or suspended are durable: a turn interrupted by the pipe closing leaves a session that mj can resume, and the work it had checkpointed is still there. See Durability and recovery.