The Status Socket
What the agent says while it runs, and the commands it accepts, so that something other than the IntelliJ plugin can listen: a VS Code or Eclipse extension, a build script that nudges the agent when it has finished writing class files, a log shipper.
The agent's own tests hold this document to the code (ProtocolContractTest),
so a field or command listed here is one the agent has.
Finding the port
The agent listens on 127.0.0.1 only, on an ephemeral port unless
statusPort=N is passed, and writes the port number as plain text to a port
file:
| Where | When |
|---|---|
the path given as portFile=... |
always, when given |
<hybris>/.reclazz/agent.port |
SAP Commerce, no portFile given |
<working directory>/.reclazz/agent.port |
everything else, no portFile given |
The IntelliJ plugin also writes and reads .idea/reclazz/agent.port for a
run configuration it injected the agent into. The file is written whole
(temporary file and rename), so a reader never sees half a number, and it is
removed when the agent stops.
The stream
One JSON object per line, UTF-8, newline-terminated. The first line on
connecting is a CONNECTED event; after that every status line the agent
prints, and a HEARTBEAT every 10 seconds when nothing else is said.
{"level":"RELOAD","message":"Reloaded com.acme.OrderService (12ms)","timestamp":"2026-09-08T09:12:44.120Z"}
| Field | Present | Meaning |
|---|---|---|
level |
always | one of the levels below |
message |
always | the text of the line, JSON-escaped, at most 4096 characters |
timestamp |
always | ISO-8601, UTC, when the line was emitted |
type |
structural reloads | structural, so a listener can count them apart |
version |
CONNECTED only |
the protocol version, 1 |
agent |
CONNECTED only |
the agent's release, such as 1.1.1 |
Levels: CONNECTED, HEARTBEAT, INFO, OK, WARN, ERROR, RELOAD,
STRUCTURAL_RELOAD, COMPILE.
Fields are only ever added, never renamed or removed, and a listener that
ignores fields it does not know keeps working; version changes only if a
field's meaning does. The agent keeps at most 5 clients and drops events for
a client that stops reading rather than blocking a reload on it.
Commands
A client may send one line at a time, at most 512 characters; anything not
listed here is ignored without an answer, and a line that never ends closes
the connection. Every answer arrives as ordinary INFO lines on the stream,
to every client, except requested BUILD receipts, RELOAD state, DOCTOR observations and VERIFY results, which go only to the requesting connection.
| Command | Answer |
|---|---|
DIAGNOSE <class> |
why the class did or did not reload last time: the class file the bytes came from, the outcome, what a restart would change |
PENDING |
restart-required changes plus manual-pause state and pending counts |
HEALTH |
how the session is going: reloads, failures, latency, watched directories, a reload that is still running |
BUILD <state> [request=<token>] [owner=<token>] |
hold class files when a build starts; accept the complete captured output only after success; failure keeps the hold. States are case-insensitive; request= is literal and its token is echoed unchanged. An unknown argument makes the entire command ignored |
VERIFY <token> <class> <sha256> |
read the latest exact-byte reload receipt for one class; requester-only structured result (below) |
DOCTOR <token> |
read bounded JVM, session, watcher and capability evidence; requester-only result (below) |
RELOAD <action> request=<token> |
pause/resume/status automatic reload admission; requester-only correlated state (below) |
SCAN |
look at the watched directories now, instead of on the file watcher's next poll; what changed is reloaded as usual. Send it when a build has just finished |
Nothing a client sends makes the agent load, reload or run anything it would not have on its own.
Pausing automatic reload
Send RELOAD pause request=my-request and wait for the requester-only INFO
message RELOAD_STATE <token> <action> paused=<true|false> pendingClasses=<count> pendingActions=<count> buildHold=<none|named|legacy>.
Actions are exactly pause, resume, status (lower-case). The mandatory
request token uses 1–64 ASCII letters, digits, underscores or hyphens.
Invalid commands or an unavailable listener produce no acknowledgement.
Pause is an agent-wide manual switch: any local client can resume it; it is
not a per-client lock. Disconnect does not resume. Repeated pause/resume is
safe. status only reads state; PENDING also reports the pause state and
pending counts alongside its existing restart-required entries. HEALTH
reports a manual pause. Counts cover observed, not-yet-admitted distinct class
paths and automatic action keys, not every unobserved disk change. A capture
already reading bytes may return its entries to these pending maps afterward.
Class files, source compilation and resource actions observed while paused
are retained/coalesced. SCAN still observes files but cannot bypass pause.
Resume schedules a scan and eligible pending work using current output. Its
acknowledgement is an admission-state snapshot, not reload completion;
use VERIFY and application checks for completion. A missing acknowledgement
leaves the outcome uncertain: query status before retrying. Old agents ignore
this command; clients must not treat timeout as success.
Manual pause is independent of BUILD ownership. Resume cannot unlock active or failed builds. A successful owner's BUILD ok still validates/captures output and releases that build hold, while a manual pause keeps changes pending; unreadable output still retains ownership. A later build start/failure continues to block class admission. BUILD holds retain their existing class-file scope.
Work admitted before pause may finish, including captured class batches waiting for a request boundary and compilation/framework callbacks already running. Pause does not interrupt a batch, roll back changes, prevent an application classloader from first loading new disk output, or freeze application requests. A pause during class capture postpones the unadmitted batch. No selective application, rollback or transactional reload is implied.
Holding a build until it succeeds
The IntelliJ plugin sends BUILD started at build start and BUILD ok
only when compilation finishes without errors or cancellation. It sends
BUILD failed otherwise. Files from a failed build remain held and join
those from the next successful build. SCAN alone does not release a hold.
On success the agent waits for its watcher to finish scanning, captures
all pending class bytes, then accepts that batch. A newer start or failure
invalidates an unfinished capture. Already accepted bytes may finish
reloading while the next build is held. This controls admission to reload;
it does not make JVM redefinitions or framework refreshes transactional.
A read or scan failure leaves the entire build held for another BUILD ok.
A missing result or disconnected client never releases output. After five
minutes there is one warning; HEALTH shows the held count and start time.
After verifying that the output directory contains a successful build,
BUILD ok can also recover a hold left by an IDE that closed mid-build.
Unowned (legacy) commands describe one ordered build stream per agent. They
cannot alter a named hold, and a named builder cannot take a legacy hold.
Agents receiving no BUILD signals keep their usual behavior.
For a whole Gradle or Maven build, install the hold before invoking the
build tool. A classes.doFirst hook runs after its compilation dependencies
and is too late to protect them. Sending bytes is not proof that the hold has
been installed: wait for a receipt before starting the compiler.
An optional request=<token> suffix asks for an acknowledgement. Tokens are
1–64 ASCII letters, digits, underscores or hyphens; an invalid suffix causes
the entire command to be ignored. After the build listener returns, only the
requesting connection receives an INFO event whose message is exactly
BUILD_ACK <token> <state> (state lower-case). For started/failed the hold is
installed before this receipt. For ok it confirms scheduling the capture,
not completion of capture or reload. There is no receipt when the listener
is unavailable or throws. Legacy BUILD commands still work without receipts when no named hold exists.
Use the terminal wrapper in the MCP jar for either build tool; see mcp-server.md. It requires this receipt support, so an older agent refuses the wrapped build before any compiler runs. A missing receipt is a failure, never permission to proceed. If an ok receipt is lost, its result is uncertain: the agent may already have accepted output; verify live behavior rather than reporting success.
Build ownership
Add owner=<token> together with request=<token> to protect a named build.
Both tokens use 1–64 ASCII letters, digits, underscores or hyphens. Options may
appear in either order; duplicate or invalid options are ignored as a whole.
Only started acquires a free named hold. A different owner, or an unowned
command, cannot change it with started, ok or failed. Successful requests receive
BUILD_ACK <token> <state> owner=<owner>; ownership conflicts or completion
without an active named hold receive BUILD_REJECTED <token> <state> owner=<owner>.
An unowned request rejected by a named hold receives BUILD_REJECTED <token> <state>.
Replies go only to the requesting connection. An unavailable listener produces
no receipt, including when an older listener cannot enforce named ownership.
Ownership survives failure and disconnect. Reuse the same owner for recovery: start another complete build, then signal ok only after successful compilation. Scan or read failure retains ownership too. The ok receipt precedes asynchronous capture; another owner's start is rejected until that capture is accepted. Already accepted batches can finish reloading while the next build is held. There is no timeout unlock or force takeover. If the owner token is lost, restart the application/agent to reset the hold.
Choose a unique owner per independent build. This is cooperative correlation, not authentication: sharing a token deliberately shares ownership. It cannot isolate output from a process that writes without acquiring the hold. Legacy unowned builders still require a single ordered builder. An unowned ok can recover only an unowned hold after a verified successful build. Attachment plugins do not automatically wrap terminal compilation. New wrappers require an exact owner-correlated receipt and refuse to compile against older agents.
Nudging the agent from a build
On macOS the JDK's file watcher polls on a two-second cycle, and SCAN
saves that wait. The IntelliJ plugin sends it after every build; a Gradle
or Maven build can do the same.
Gradle, after the classes are written:
tasks.named("classes") {
doLast {
val portFile = rootProject.file(".reclazz/agent.port")
if (portFile.exists()) {
java.net.Socket("127.0.0.1", portFile.readText().trim().toInt()).use {
it.getOutputStream().write("SCAN\n".toByteArray())
}
}
}
}
From a shell, after mvn compile:
printf 'SCAN\n' | nc 127.0.0.1 "$(cat .reclazz/agent.port)"
Verifying exact compiled bytes
With matching current agent/MCP jars, send VERIFY <token> <class> <sha256>.
VERIFY is uppercase; token is 1–64 ASCII letters/digits/underscores/hyphens.
Class is a dot-separated Java binary name, up to 256 characters, including $
for nested classes; identifier-ignorable control characters are rejected.
SHA-256 is exactly 64 lower-case hex digits, calculated from the compiled class
file the caller expects to have been applied. The agent never reads a path
supplied by this query. Invalid commands or an unavailable verifier get no reply.
The requester receives an INFO event with message equal to
VERIFY_RESULT <token> <json>. Parse the event first, then parse the JSON suffix.
Existing readers can continue treating message as text. The nested object has
these string fields:
| Receipt member | Meaning |
|---|---|
| requestId | The unchanged request token; check it before accepting a result |
| sessionId | Random ID of this agent verifier; changes on initialization |
| className | The requested binary name |
| expectedSha256 | The requested hash |
| observedSha256 | Hash of the latest captured reload input, or empty if absent |
| status | One of the states below |
| source | Origin path, bounded to 200 characters |
| detail | Outcome or reason, bounded to 200 characters |
| completedAt | ISO UTC batch completion time; empty before completion or if absent |
Only applied is positive evidence. It means the expected input bytes matched, a successful reload outcome was recorded, the class was uniquely loaded before the attempt, and the entire batch returned, including deferred redefinition and framework refresh, with no WARN/ERROR on the applying thread. A warning in any member conservatively prevents success for the other members too.
A superclass-changing save illustrates this distinction: eligible method bodies
may run while the original superclass remains. Its warning makes the matching
receipt unverified, including when a dependent method stays pinned to its old
body. A class-level blocker instead produces failed. Neither means that the
requested hierarchy was applied. PENDING retains the session's restart concerns;
a later ordinary save can have an applied receipt without clearing earlier
superclass notes. Check current live behavior as well as the receipt; retained
notes are not an exact diff of the latest source. See
production reload reporting.
running: matching bytes are being applied; no completion yet.failed: the matching attempt reported failure or the batch threw; detail says why.unverified: matching attempt finished without sufficient evidence: absent outcome, unloaded/ambiguous class, duplicate input name, or a batch warning/error.mismatch: the latest attempt used different bytes; an older success cannot certify a newer edit. Inspect observedSha256 and source.not_observed: no retained receipt in this session. This includes eviction, restart, classes never reloaded, and untracked mutations invalidating old proof.
The ledger retains at most 512 latest class receipts, not an operation history. It observes external class-file and autoCompile batches; hashes come from the captured input, never from a file rewritten while a reload is running. Starting a new attempt supersedes old proof even if that attempt later fails. A failed compiler that leaves files held creates no receipt for those new bytes.
This is a point-in-time receipt for one class, not a transaction across classes, a full snapshot of all classloaders, or a business-behaviour test. It cannot prove that an identical-byte recompile caused a new reload, or that another agent or instrumentation tool has not subsequently changed the JVM. Keep one builder per agent; verify each changed class with a bounded polling deadline, keep sessionId consistent, then exercise the endpoint or application test. No reply, disconnect, old-agent timeout or a non-applied state must ever be treated as success.
Doctor observation
DOCTOR <token> accepts a token of 1–64 ASCII letters, digits, underscores or
hyphens. The requesting connection receives an INFO event whose message is
DOCTOR_RESULT <token> <json>. Parse the event, match the token, then parse the
JSON suffix. No build, scan, reload or hold change is performed.
status:observed carries requestId, detail, sessionId (the same process
identity used by VERIFY), agentVersion, string pid, javaVersion, vmName,
and target JVM workingDirectory. A working directory is not a repository root.
watcherState is starting, watching, stopped or unavailable. buildHold is none,
named, legacy or unavailable; owner tokens are never disclosed. Boolean
buildOwnershipSupported, verifySupported and scanSupported describe wired
handlers, not capabilities inferred from a version string. Before wiring, support
can be false and the verification session empty.
watchedDirectoryCount counts currently valid registrations whose directories
still exist. watchedDirectories samples at most eight normalized absolute paths,
each at most 256 characters; longer paths are omitted. watchSampleTruncated
explicitly marks omitted entries. unwatchableDirectoryCount reports refused
registrations. Registrations can include class, source and resource directories;
this sample does not prove coverage of a particular compiled output. The snapshot
is observational, not atomic with watcher or build changes.
reloadConfirmed is always false. Observation is neither readiness nor reload
proof. Metadata strings are bounded to 512 characters and the JSON receipt to
3900 characters so the enclosing 4096-character message limit cannot truncate it.
If evidence cannot be produced within these bounds, status:unavailable includes
only correlation and a detail message. Older agents can give no response; clients
must report unavailable rather than assume support. The existing loopback socket
trust boundary still applies.
Client-side batch verification
MCP reclazz_verify_batch composes 1–32 existing VERIFY queries on one connection.
There is no batch agent command and no atomic snapshot. Every item has its own
correlation token; the client requires the same sessionId across validated
receipts and one total time budget. It preserves earlier receipts and marks
remaining items unavailable on connection/evidence failure. Cancelling the MCP
request closes only that observation connection; the running agent's reload and
build ownership are unaffected. See MCP batch and cancellation.
Canonical source
This full page is generated from docs/protocol.md. Edit the repository source and rebuild; do not edit generated HTML.