Skip to content

Connect Jcode (beta)

Guide (informative) · For: operators · Prereqs: Quickstart

Jcode joins a Cotal mesh as a lateral peer. The connector creates one private Jcode Harness API instance per seat, one Jcode session inside it, and exposes the normal cotal_* tool surface through Jcode’s documented stdio MCP configuration.

Beta means the supported path is deliberately narrow: a fresh private session, a fork of one of your own sessions (--resume), prompt injection, presence, managed start/stop, requested reasoning effort, and an attached TUI work. Features that do not preserve that private session’s mesh surface fail loud: exact-session continuation, --share-tools, and connector --opt values are not supported.

The connector is seeded with the Cotal CLI. Jcode’s released Harness API bridge is a Unix-socket surface, so Windows is not supported. Managed seats run on Linux, macOS, and the BSDs. Install Jcode 0.78.1 or later from its GitHub release and make the binary available as jcode on PATH:

Terminal window
jcode version --json
cotal spawn --agent jcode

If an older Cotal installation is missing the connector, run cotal ext seed --repair (or cotal ext add @cotal-ai/connector-jcode). This connector intentionally uses the released binary’s api-bridge command; it does not require a Rust checkout.

Terminal window
cotal spawn --agent jcode
cotal spawn reviewer --agent jcode -d
cotal spawn --agent jcode --model gpt-5.6-sol --prompt "Review the current change."
COTAL_DEFAULT_AGENT=jcode cotal spawn

A detached seat is managed normally: cotal ps, cotal attach, and cotal stop control the same process the connector starts. In a terminal, Jcode opens on the managed session. With piped output it stays headless; set COTAL_JCODE_TUI=1 or COTAL_JCODE_TUI=0 in the environment of the process building the launch to override that choice. For a detached spawn, that is the manager’s environment. The connector refuses any value outside the on/off spellings in Configuration when it starts.

Jcode’s stable integration surface is the Harness API: protocol-v1 NDJSON over a Unix socket. The connector launches a private instance with @1jehuang/jcode-sdk’s launchInstance() and attaches only to that instance’s own socket:

  • launchInstance() starts a private JCODE_HOME, runtime directory, daemon, and api-bridge; the connector holds the process handle first-hand and closes that instance with the Cotal seat. This gives each managed Cotal peer one owned session and prevents it from seeing or changing the operator’s live Jcode sessions.
  • Attaching to an operator-run jcode api-bridge shares the operator’s live session inventory. That is appropriate for a dashboard or editor integration, but not a managed Cotal seat: stop, prompt injection, and session selection could act on the operator’s work. The connector never attaches to an operator bridge.
  • A managed seat never updates its own binary. Jcode’s background updater restarts the process tree when it lands a release; that restart drops the seat’s TUI, which is the only connection the Jcode server counts as a client, and nothing re-attaches, so the server’s idle reaper takes the seat down five minutes later in the middle of a turn. The seat’s version is whatever is on PATH when you spawn it, and it stays that version for the seat’s life. Update deliberately, between seats, not under a running agent.

On a graceful stop and on a startup failure, the connector proves the private daemon tree is actually gone rather than trusting the SDK’s registry-keyed stop (which is a silent no-op when the servers.json socket path does not match verbatim): it reads the PIDs the private home itself records, sends a bounded SIGTERM, escalates survivors to an exact-PID SIGKILL, and reports a failed stop instead of a clean one if any recorded process survives. It never signals by name, so teardown can only ever reach the seat’s own tree.

A seat that dies without that teardown, from a manager restart or a kill past the grace window, leaves its Jcode server running. The server has a process group of its own and carries no COTAL_NAME, so a name-keyed reap does not reach it, and it holds the seat’s runtime directory until its own five-minute idle timer expires. Each launch records its identity nonce and its host process in the private home, and the seat’s next launch stops the tree that record names. The recorded host is the gate: while it is still alive the seat is still serving, so nothing is signalled and the second launch meets Jcode’s own runtime-directory lock instead.

The private Jcode home lives under <manager-workspace>/.cotal/jcode/. It is unique per space/name and is owner-only. Jcode’s own credential inheritance is used for the private instance, so provider logins work without copying its transcript/config tree into the seat. The spawned Jcode process does not inherit COTAL_* values or the Cotal launch-material pointer.

Because the home is keyed by space and name, a seat respawned under the same space, name, and manager workspace lands in the same home, and the connector automatically continues the non-archived Jcode session there that was recorded for the seat’s working directory and holds the largest transcript, since that is the session carrying the memory a restart would otherwise throw away. A seat spawned under a fresh name keys a different home and starts with an empty transcript, so keep the same name when you want a replacement seat to continue where the previous one stopped. This automatic continuation is a relaunch of the seat’s own private session; it is separate from --resume, which forks an outside session into the seat (below). The short socket alias the connector derives from that home is reclaimed at every launch, so a name a stopped seat used stays launchable.

cotal spawn --resume <id> forks session <id> from your own Jcode home (JCODE_HOME, or the default Jcode home) into the seat’s private home before the seat’s instance starts. The Harness API has no fork call, so the connector does what Jcode’s own split does: it writes a new session whose parent is the source and which carries the source’s messages, compaction state, system prompt, and model. The seat gets a new session id and its briefing as the first turn after that history. The source files are only read, so the source transcript is never appended to. A session id with no readable transcript, or one whose snapshot or journal holds fields Jcode cannot load, is refused before the seat launches; every carried message, content block, and compaction state is checked against Jcode’s own schema, and the refusal names the field. Jcode stores its counts as u64 and reads one only as a plain decimal integer, so a count above what a JavaScript number holds is copied byte for byte, and one above the u64 range or spelled with a fraction or an exponent (1e3, 1.0) is refused. The source may be live: the connector reads it until two reads agree and every journal line is newer than the snapshot, so a Jcode checkpoint caught mid-way is neither lost nor applied twice, and a session that keeps changing is refused with a request to retry. A seat relaunched under the same name continues its fork rather than forking again, without reading the source, which may since have been deleted. The seat’s briefing is recorded separately from the fork, so a first launch that fails after forking still briefs the seat on its next launch. The seat records the source session id, its title, and a SHA-256 of the snapshot and journal it read, prints them when it forks, and the manager reads that record into the seat’s resume document, so cotal ps --wide shows them. The manager keeps a title of at most 1024 characters, so a source with a longer title is refused before the seat launches.

Connector diagnostics are written both to the spawning terminal and to an owner-only <private-home>/logs/connector-<timestamp>-<pid>.log, so a failed launch remains inspectable after the manager’s launch error scrolls away. Public startup failures stay scrubbed to allow-listed codes rather than arbitrary Harness API messages.

Credential mirroring is mandatory for managed Jcode seats: each launch atomically refreshes the allowlisted Jcode, provider-config, and external-login destinations, and removes a destination when its source login was removed. Cleanup addresses only that explicit inventory; transcripts, MCP configuration, logs, and other private-home state are untouched. Copy, mkdir, and removal walk the parents with O_NOFOLLOW, then publish, create, or unlink the leaf through the pinned parent rather than through a path the kernel re-walks. Replacing a walked directory with a symlink cannot write, create, or delete a namesake outside the private home.

Two mechanisms provide that pin. On Linux the leaf is named /dev/fd/<fd>/<name>, the openat and unlinkat equivalent Node does not expose, after /dev/fd/<fd>/. is proven to traverse. macOS mounts /dev/fd but has no subpath namespace under a descriptor, so there the connector pins the parent as the process working directory instead: a single-component name resolves from that directory’s inode and no ancestor is walked again. Entering by path is verified rather than trusted, because chdir takes a path: the entered directory’s inode must equal the inode of the descriptor opened a moment earlier, and a mismatch is refused by name. The working directory is restored on every exit, including the refusing ones. If neither pin is available, the connector throws a named error and mirrors nothing. Once a pin holds, ENOENT on a child means the mirror path is absent.

There is no credential-free opt-out today because the private instance must reproduce the operator’s current provider-login state rather than silently start with stale or partial authorization.

If a provider failure closes the private Harness API connection during a mesh-driven turn, the connector leaves that turn’s inbox batch unacknowledged and opens one bounded recovery window for a private replacement connection to the same session. The window lasts 60 seconds from the close, and stopping the broken private tree counts against it. A transient launch or attach failure retries inside that window, so a loaded host gets the same result as a fast one without creating an unbounded connector relaunch loop. The seat reports waiting while it reconnects, then redrives that unacknowledged batch only after the session attaches. Each failed replacement must be proven stopped before another launch. A permanent Harness refusal, including an invalid request, missing session, protocol mismatch, missing binary, or socket permission denial, ends the seat immediately; another launch cannot change it. An unprovable teardown, the recovery window expiring, or a second disconnect after a successful replacement also ends the seat. When a turn failed on a Harness error and no turn has succeeded since, whether the host or the TUI owned it, the connector line that ends the seat this way also names that error as last turn error: <message>, so the manager’s seat reaped: line carries it without a read of the seat’s private log. The reap line keeps only the first 240 characters, so that field comes before the error of a failed recovery, which then reads recovery error: <message>. An unrecognized Harness SDK error code remains transient by default and retries inside the same bounded window; new permanent codes must be added to the explicit classifier and its exact-count regression.

Jcode currently supports stdio MCP servers. The connector writes only its own cotal entry to the private JCODE_HOME/mcp.json; it starts a stdio MCP bridge for that entry and relays its calls to the host’s one MeshAgent. The Jcode/MCP child receives a per-launch relay capability, but not the Cotal broker credential or its launch-material pointer. Jcode also overlays project .jcode/mcp.json, .mcp.json, and .claude/mcp.json; a managed launch refuses a workspace containing any of those files, because one could replace the cotal bridge or add tools that were not explicitly shared. Operator MCP configuration is isolated in the private home and project MCP configuration is not supported yet.

Before the seat joins the mesh, the host runs a mandatory Jcode turn that calls cotal_orientation. Jcode loads MCP tools asynchronously; its first turn can use the pre-MCP tool snapshot immediately before Jcode rebuilds that snapshot. The host repeats the identical proof once in that case. A second absence fails the launch, so a bridge that never comes up remains a loud failure rather than an agent that is present but mute. The persona is already in that transcript as a no-reply message; the spawn --prompt is not submitted until after join. While the proof is in flight the connector log names pre-join readiness and the bound. That in-flight line only means startup reached the gate; it is not a hang, a missing prompt, or a provider refusal by itself. After the turn, a second line names the outcome and is what separates those cases: orientation proved; joining with no spawn --prompt or joining, then submitting the spawn --prompt when the proof passed; provider refusal when the provider rejected the turn; timeout when the bound fired. A genuine hang that never returns and never hits the bound has no outcome line. A one-line log with no route line is not that signal. The proof itself is bounded to the same three minutes the connector declares to the manager (readinessTimeoutMs is the exported JCODE_READINESS_TIMEOUT_MS). Tests may shorten the host bound through COTAL_JCODE_READINESS_TIMEOUT_MS; that override is not an operator setting and does not change the window the connector declares to the manager. The bound is a call-observation deadline: the host proves readiness on the orientation tool_done event as it arrives, without waiting for turn_done. If that call never arrives inside the window the host exits readiness_timeout and never joins, rather than working invisibly. That teardown does not wait for the in-flight turn: it kills the private Jcode tree and discards whatever that turn had generated. Nothing from it is recoverable; inspect the seat connector log for the timeout outcome, then spawn again. The timeout line names whether the orientation call was observed. A seat that already made the call joins even if that proof turn is still open; the host does not destroy a functional session solely because turn_done has not arrived. The manager’s wait can still report uncertain when join itself is slow after a passing proof; that is not a cleanup verdict, and it is not the same as a host readiness_timeout. Use cotal attach <name> or cotal ps to inspect an uncertain launch. The host then waits for the mesh connection and presence bind to complete before it delivers a notice that the bootstrap orientation predates the join and that a new orientation is live context. During a broker outage, it stays waiting and sends no connected notice.

A seat launched with a spawn --prompt gets that notice as a no-reply append; the prompt is still the seat’s first driven turn. A seat launched with no spawn --prompt has nothing else to schedule a turn after join, so the notice is delivered as the seat’s first driven turn instead of a no-reply append: the same dispatch boundary and one-shot rule the startup prompt uses, so the seat’s final startup state is never an unread append.

A refused post-join notice sent as a no-reply append is logged without ending the joined session. When the notice is instead the startup turn (no spawn prompt), a refusal on that turn is handled the same way any other startup-prompt failure is: logged and retried, never fatal to the seat. The startup prompt (or, with no spawn prompt, the notice standing in for it) stays pending while the native session is busy or its bridge reconnects. Once the host invokes the request, it consumes that prompt and does not retry it after an ambiguous error or close. This prevents a second submission; it cannot prove whether the first request executed.

The startup prompt excludes the automatic inbox. Messages buffered before it run in the following turn, including ordinary channel traffic held in dnd. Quiet-channel traffic remains available only through an explicit inbox pull. The connector log names a startup prompt waiting on in-flight steering and a turn deferred because native state changed during that wait.

A spawned seat publishes run boundaries, assistant text, reasoning, and tool starts and ends on events.<owner>.<actor>. Tool arguments and results are not published. The channel and grant rules are the same as the other connectors; see Connect Claude Code for how to grant and read one.

Jcode’s live Harness API reports token-sized text and reasoning deltas, but that stream cannot be read again after a host crash. The connector therefore reads Jcode’s native append-only session journal under the seat’s private home. The journal supplies a durable byte cursor and is keyed by the Jcode session id, which is also the AG-UI thread id. A restarted seat continues from the cursor stored in its event write-ahead log and does not republish records already acknowledged.

If Jcode checkpoints its journal, the connector validates the saved session snapshot and ends the interrupted event observations with RUN_ERROR. Open tool observations end before this error; this does not claim that their executions completed. The seat stays up while the next journal is absent, including during a long tool call. The reader keeps its previous cursor until it can read the new journal from its beginning. It does not reconstruct missing output from the snapshot. A tool result whose start was not observed also ends the run with RUN_ERROR, not an invented tool start or an unpaired end. Like every published run error, both carry only the fixed message run failed: the connector’s codes jcode_journal_fold and jcode_tool_start_missing do not leave the seat. On restart, open tools are restored from the event WAL.

A missing journal without a valid snapshot for the same session remains an emitter failure. Malformed cursors, invalid complete records and filesystem access refusals are not checkpoints.

When the seat’s mesh connection drops and the endpoint is rebuilding it, event publishing waits until the connection is live again and then publishes the queued records in order. The seat stays up through the outage. If the seat is stopped before the connection returns, the wait ends and the connector log records AG-UI emitter stopped. The unpublished records stay in the journal behind the stored cursor, and the next start publishes them. Any other emitter failure stops the seat with exit code 1 on a space that requires events. On any other space the seat keeps running without events.

The journal records settled message blocks rather than live deltas. Text and reasoning therefore arrive per persisted block, and tool activity arrives when Jcode persists the tool-use and result blocks. User prompt text is not republished onto the event channel. The launcher sets COTAL_EVENTS by default; pass --no-events to opt out.

For a foreground launch, the TUI opens as soon as the session is ready, before the readiness turn, so it streams boot activity instead of leaving the terminal blank. Presence still begins only after the readiness proof passes. An inbound peer message then wakes a Harness API turn. A directed message that arrives while the Harness session is busy (a Cotal-owned run(), a TUI-owned turn, or an advisory idle pulse between tool rounds of a still-open Cotal-owned run) enters Jcode’s session-owned soft-interrupt queue. Ambient channel traffic stays buffered for the next turn. The host marks presence working while the session is busy, publishes activity naming automatic queue depth and age while anything remains uncommitted, and acknowledges every initial or soft-interrupted inbox id only after that containing turn succeeds. A failed Cotal-owned turn or private Harness replacement leaves those ids unacknowledged for mesh redelivery. When the Harness reports the failure itself, such as a provider rate_limit, the host relays its error code as the presence condition, so the roster and cotal ps read waiting (rate_limit for 2m). A turn the TUI owns is covered too: its failure arrives as an unsolicited Harness error frame, and the host relays that the same way. A code outside the closed vocabulary reads failed with the native code in condition.source. The next turn clears it when it starts, whether the host or the TUI owns that turn. The host also records every work event of its session, such as a token or a tool call, as presence activeAt, whether the host or the TUI owns the turn. A turn that stopped advancing therefore shows the age of its last event, · active 40m ago, beside a heartbeat that is still fresh. cotal_inbox pulls only buffered quiet ambient from that host-owned queue; its shared optional peek argument is supported, so peek: true shows those messages without clearing them.

A run turn rides the message that starts a Cotal-owned turn. It counts as shown once the Harness accepts that message, so the seat can cotal_yield it during the turn that carries it. A send the Harness never accepts leaves it unshown, and the next turn carries it again.

--model is passed to Jcode’s session-level Harness API model selector. Jcode validates the model against the active provider, and an accepted selection becomes the session pin and the seat’s model label. The connector reports the provider route actually serving that model to presence, and cotal ps --wide and --json show it as provider. The connector does not require RuntimeInfo.model to echo that pin immediately because the runtime field can temporarily report the previous model after selection.

Model startup refusals are named without exposing provider output: model_prefix_rejected means a provider/model value was supplied where the Harness API requires a bare id, model_refused means Jcode rejected that bare id, and model_mismatch means a requested variant could not be tied to one active provider route for the selected model. private_state names a different step: the seat’s private home, its credential mirror, or its short socket alias could not be prepared.

Stored sessions have their own refusals. sessions_enumeration_failed means listing the home’s prior sessions killed the harness. sessions_unwritable means the home’s sessions/ directory exists but will not take a write: the harness would accept the seat and die only while persisting its first session, so the connector refuses before that launch and names the directory and the errno. Fix the directory’s permissions on the seat’s private state and start again; the connector never repairs or widens them itself. A missing sessions/ directory is a first launch and is left alone.

cotal models --agent jcode reads the declared catalog from the operator Jcode home’s config.toml: each provider with model_catalog = true, its [[providers.<name>.models]] ids, and any declared reasoning_efforts. This is the same config Jcode copies into a private managed instance. The command fails loud when the file is unreadable, malformed, or enables a catalog without model entries.

The listed effort tiers are display declarations, not Jcode runtime capabilities. Jcode’s named model config does not assign effort support per model. A named provider profile enables it through provider configuration or Jcode’s model-family detection. cotal models prints that caveat inline as variants (declared, not provider-verified) beside each configured tier list, so it cannot be missed by reading only the model rows. Providers can reject a tier the file names, so launch remains the authority: Jcode applies the requested value and a provider rejection ends the launch. --refresh does not turn this local declaration into a live probe.

The Harness API can set a requested effort but cannot read an effective effort back. Its runtime identity reports provider, model, and routes only; no reply or event carries the applied tier. Cotal therefore records the accepted request and does not relabel it as an observed effect.

--variant is the session’s reasoning effort, applied after the model and before the seat’s first turn, so a seat never serves a turn at an effort nobody chose. A persona’s variant: is the default and --variant overrides it, the same way model: and --model work:

Terminal window
cotal spawn --agent jcode --model gpt-5.6-sol --variant high

Which tiers exist depends on the provider, profile, and model. The connector does not carry a copy of those rules. After model selection it uses the accepted session pin with Jcode’s runtime provider and route catalog to verify one active provider route, then passes the tier to that route. For a variant-only launch, where there is no requested pin, the runtime model identifies the selection. A duplicate model id on another route cannot receive the setting by accident. A rejected tier ends the launch with a safely parsed accepted ladder when Jcode supplies one. A verified route with no reasoning-effort surface also ends before the first turn, but reports unsupported capability instead of suggesting another tier. Arbitrary provider rejection text stays private. Omit --variant to keep Jcode’s configured default.

If the mandatory readiness turn receives a provider invalid_request refusal for a model id or reasoning-effort value, the launch diagnostic names only the provider error code and rejected value. Other provider response text remains scrubbed, so an external observer/UI can correct connector-visible input without exposing private harness output.

The following fail loud before a new session is provisioned where the manager can preflight them, or at connector launch as a backstop:

  • Exact-session continuation: a Cotal seat owns a new private Jcode instance. Attaching it to a session an operator or another seat still owns would violate that ownership boundary. Use --resume, which gives the seat its own fork.
  • Tool sharing: Jcode resolves its MCP configuration from several global and project sources. The connector owns a private configuration containing only cotal, rather than claim a chosen subset can be safely merged.
  • Launch options: the connector does not map arbitrary flags/config into the Harness API.
  • Containers: the current deploy image does not bundle Jcode, so there is no containerized Jcode connector today.

The private home protects against accidental sharing and stale session selection; it is not an OS-user isolation boundary. A hostile process running as the same user can still read that user’s files or inspect another same-user process. Use OS/container isolation where peers must be mutually hostile.

The model can receive remote peer messages and Jcode is an autonomous coding harness. Treat its provider credentials, filesystem access, and network capability as the privileges of the OS user running the seat. Cotal’s spawn capability governs who may create a seat; it is not a sandbox for what a model can be persuaded to do after creation.