@volter/supercode-client
v0.3.60
Published
Headless state and control layer for Volter Harness-powered coding-agent frontends
Readme
@volter/supercode-client
@volter/supercode-client is Volter Harness's framework-neutral, presentation-free state
and control layer for coding-agent frontends. It is the reusable seam between
Volter Harness's session/runtime machinery and products such as an editor, desktop
app, web client, or terminal dashboard.
It deliberately ships no components, CSS, icons, Markdown renderer, layout, or product copy. A consumer owns all visuals. The package owns the semantics that must stay identical across those visuals.
Why this is separate from @volter/supercode-harness-sdk
@volter/supercode-harness-sdk is the low-level Node transport. It starts
supercode harness serve and exposes discovery, persisted-session, transfer,
and managed-runtime primitives. It intentionally does not decide which session
is active, recover a follower, pair tool calls with results, serialize
concurrent actions, reconcile streamed output with persisted history, or
project capabilities into safe UI actions.
@volter/supercode-client does those things. It accepts the low-level client by
injection and has no Node imports of its own, so the same controller contract
can be hosted behind HTTP/SSE, WebSocket, Electron IPC, or another transport.
import { SupercodeHarnessClient } from '@volter/supercode-harness-sdk';
import { SessionWindowCache, SupercodeController } from '@volter/supercode-client';
const harness = new SupercodeHarnessClient({ cwd: projectRoot });
const mirrorCache = new SessionWindowCache({ maxEntries: 12 });
const agent = new SupercodeController({
client: harness,
workspace: projectRoot,
mirrorCache, // share this instance across short-lived controllers
// A host with its own machine-wide session/activity stream can set
// inventorySubscriptions: false on seeded one-session mirror controllers.
policy: 'yolo', // a host policy decision, not a browser-controlled value
ownsClient: true,
});
const unsubscribe = agent.subscribe(() => render(agent.getSnapshot()));
await agent.initialize();
await agent.dispatch({ type: 'start', harness: 'grok' });
await agent.dispatch({ type: 'send', text: 'Build a timer.' });
if (agent.getSnapshot().availableActions.steer) {
await agent.dispatch({ type: 'steer', text: 'Keep the API; simplify the UI.' });
}Observing a runtime owned by another host
Use a pristine controller with ownsClient: false to observe an already-started
ManagedRuntime. Do not initialize it. This mode performs no discovery, follower
creation, reconciliation, or service calls; all controller commands are refused.
The host retains launch/environment/MCP configuration, input delivery, and runtime
and client ownership. Closing the observer removes only its listeners.
const observer = new SupercodeController({ client: harness, workspace: projectRoot });
await observer.observeRuntime(existingRuntime, {
maxMessages: 2000,
maxAuxiliaryEntries: 0,
retainRequestPayloads: false,
});
await existingRuntime.sendInput(rawInput);
await observer.recordAcceptedInput(rawInput); // only after delivery succeeds
const snapshot = observer.getSnapshot();
await observer.close(); // existingRuntime and harness remain owned by the hostRaw accepted input is neither trimmed nor sent again. Repeated accepted inputs remain separate turns; runtime user echoes are deduplicated against the last completed message. An acceptance arriving after a terminal event remains recorded without reopening the completed turn; closing the observer cannot turn successful delivery into a reported delivery failure. Events accepted before close remain in history, and later events cannot mutate it.
The default limits are 2,000 sealed message/status entries plus one open assistant
and 2,000 auxiliary entries. Set maxAuxiliaryEntries: 0 to ignore tool/reasoning
payloads. retainRequestPayloads defaults to true; false retains request kind and
status but no payload/options. Response envelopes are never retained in this
observation-only mode. Notices and requests remain timestamped typed entries;
the embedding product supplies any user-facing status wording.
Borrowed events update the same controller store synchronously. After an open
stream's initial insertion/lookup, subsequent unread deltas avoid per-token history
scans and snapshot materialization; new entries still cost proportional to retained
history. Immutable snapshot
materialization is proportional to retained history on the next getSnapshot();
a subscriber reading on every token pays that cost on every token. Polling hosts
can read at their existing cadence without subscribing to every event.
A native embedding host may also inject repository package discovery without pulling Node process/filesystem dependencies into the universal controller:
import { SupercodeAgentPackageClient } from '@volter/supercode-client/agent-package';
const packages = new SupercodeAgentPackageClient({ command: supercodeBinary });
const agent = new SupercodeController({
client: harness,
workspace: projectRoot,
agentPackageLoader: packages.loader(),
});The package and any discovery error are published independently as
snapshot.agentPackage and snapshot.agentPackageError; a malformed optional
package cannot turn healthy session inventory into a client outage.
The controller implements the external-store contract used by React's
useSyncExternalStore, but does not depend on React:
const snapshot = useSyncExternalStore(
agent.subscribe,
agent.getSnapshot,
agent.getSnapshot,
);Before a user chooses a project or conversation, a product can use the separate read-only catalog. It discovers each native store independently, then owns the machine-wide index and activity subscriptions. This avoids coupling global navigation to whichever controller currently owns a runtime:
import { SupercodeSessionCatalog } from '@volter/supercode-client';
const catalog = new SupercodeSessionCatalog({
client: harness,
harnesses: ['claude-code', 'codex'],
limit: 30,
});
const unsubscribeCatalog = catalog.subscribe((snapshot) => renderInbox(snapshot));
await catalog.initialize();
await catalog.loadMore(30);On servers supporting harness.v1.sessions.index.resize, loadMore expands the existing
retained index: no new discovery scan or index watcher. Calls are serialized, and the atomic
resize snapshot is reconciled with notifications that arrive while its response is pending.
It reads the index's current known state; pending filesystem invalidations still arrive on
the unchanged watcher cadence. Topics and activity evidence are retained. Explicit refresh
continues to rediscover; it also recovers revision gaps or a bounded pending-event buffer overflow.
Old SDKs/servers (missing method / JSON-RPC -32601) retain discovery-based pagination;
other resize errors reject without changing the requested page size. A live window includes
one lookahead row, so visible limits above 2,047 use discovery and detach the bounded index
instead of silently truncating larger inventories. This optimizes the existing catalog API,
not the fleet tool's native parser or its worker lifecycle.
A multi-conversation host can use SessionControllerPool to keep one primary runtime durable while
opening other persisted sessions as bounded mirrors. It shares the transcript-window cache, makes
concurrent selection latest-wins, closes superseded and passive controllers, and retains a
continuation once it owns a runtime. The product still decides which catalog row was selected and
how selection failures are presented:
const conversations = new SessionControllerPool({
primary: agent,
createController: ({ descriptor, tailMessages, mirrorCache }) => new SupercodeController({
client: harness,
workspace: descriptor.cwd ?? projectRoot,
ownsClient: false,
autoObserve: false,
initialInventory: { harnesses: agent.getSnapshot().harnesses, sessions: [descriptor] },
inventorySubscriptions: false,
mirrorView: { tailMessages, maxMessageChars: 16_000, includeSubagents: false },
mirrorCache,
}),
});
await conversations.select({ key: opaqueKey, descriptor });
render(conversations.activeController().getSnapshot());Parent/child session inspection has the same headless boundary. SessionFamilyInspector owns
bounded child discovery, bounded transcript reads, cancellation, and latest-selection-wins races;
the embedding product owns titles, row projection, and whether the result appears in a drawer,
popover, or page:
const family = new SessionFamilyInspector({
client: harness,
keyForDescriptor: (descriptor) => opaqueKey(descriptor.locator),
});
await family.open(parentKey, parentDescriptor);
await family.select(parentKey, childKey);
renderSubagents(family.getSnapshot());SessionFamilyInspector retains maxChildren children per explicit page.
Its snapshot exposes hasMore and loadingMore; await family.loadMore()
retains the next page through the existing discovery cursor. Overflow rows and
boundary duplicates are handled without losing children. Selection still loads
only the chosen child; closing or opening another family invalidates pending pages.
Ownership boundary
Volter Harness owns:
- local harness inventory, authentication posture, runtime capabilities, and repair guidance;
- project-scoped session discovery over JSONL, SQLite, and future stores;
- machine-wide, progressively discovered session catalogs with native index-gap recovery and reconciled lifecycle evidence;
- stable opaque session keys (never ambiguous bare session IDs);
- passive transcript load/follow with sequence-gap recovery and retry;
- live-message delivery receipts plus a revisioned interoperability-control report preserved for the embedding UI;
- opt-in, host-side configuration of the small set of native harness controls
that affect Volter Harness workflows; disabled unless the embedding host passes
allowHarnessConfiguration: true; - bounded passive mirror reads (120 trailing messages, 16k characters per text field, child transcripts excluded) so a viewport never transports an entire recursive session archive;
- a bounded transcript-window cache that lets hosts paint previously opened chats immediately while their native transcript refresh continues;
- latest-wins multi-conversation controller ownership that disposes passive mirrors, retains controlled continuations, and never lets a superseded transcript replace a newer selection;
- optional inventory subscriptions, so an embedding host with one authoritative machine-wide index can keep seeded one-session mirrors off the redundant scan lane;
- an explicit
initialInventoryconstructor seed, so those hosts can reuse already trusted harness/session descriptors without proxying or intercepting client methods; - normalized, loss-preserving conversation projection, including tool call/result pairing and context-message classification;
- an explicit full-session load path alongside the bounded passive projection, including system context, lineage, parse diagnostics, and nested subagents;
- start, persisted resume, genuine live-process attach, branch, explicit live detach, shared terminal attachment, interrupt, and protocol-response semantics;
- typed load, import, export, translation, and cross-harness handoff utilities resolved through opaque session keys;
- one serialized mutation queue, workspace generations, stale-event rejection, and streamed/persisted completion reconciliation;
- immutable snapshots, capability-derived available actions, structured errors, runtime requests, and a lossless normalized-event side channel.
The embedding product owns:
- HTTP/IPC routes, authentication, origin checks, rate limits, and which host
policy (
defaultoryolo) is allowed; - where the controller lives. For an always-on agent it should live on the host/server, not in a browser tab;
- panel placement, collapse/persistence, icons, colors, fonts, Markdown, composer layout, history presentation, warning copy, and preferences;
- whether context/reasoning entries are visible and how approval requests are rendered.
Node hosts that need the same stable identity while building a synchronous machine-wide inventory
can import sessionReconnectIdentitySync from @volter/supercode-client/node. It returns
the exact same scs_<sha256> value as the browser-safe asynchronous helper without adding Node
crypto to the default client entry point.
Harness-native sign-in
Authentication is a separate host-neutral lifecycle because a native coding harness must remain
the only process that handles its credentials. HarnessAuthenticationController selects the
verified method through the harness service, owns timeout/cancellation/verification, and publishes
an immutable presentation-safe snapshot. The embedding product supplies only a visible execution
adapter:
import { HarnessAuthenticationController } from '@volter/supercode-client';
const authentication = new HarnessAuthenticationController({
client: harness,
cwd: projectRoot,
environment: isRemote ? 'headless' : 'local_browser',
host: {
async launch(plan) {
const terminal = await terminalHost.launch(plan.launch);
return {
wait: () => terminal.wait(),
cancel: () => terminal.cancel(),
close: () => terminal.close(),
};
},
},
});
authentication.subscribe(renderAuthentication);
await authentication.authenticate('codex');The full structured launch is delivered only to the trusted host adapter. getSnapshot() omits
the command and environment, so it is safe to project into a browser UI. Credentials, OAuth
callbacks, and token storage remain entirely native to Claude Code or Codex.
Semantic model
ACP is a protocol substrate, not the complete product contract. Volter Harness also supports persisted JSONL/SQLite discovery, passive following, native resume, translation, branch/handoff, and terminal launch instructions. The controller therefore exposes a typed Volter Harness superset and derives actions from honest capabilities.
Connection modes never collapse into one another:
| Mode | Meaning | Can send? |
|---|---|---:|
| none | No selected transcript or owned runtime | no |
| mirror | Passively following persisted history written elsewhere | only into a live peer |
| control/start | Controller owns a newly-created runtime | yes |
| control/resume | Controller started a new process from persisted state | yes |
| control/attach | Controller joined a genuinely reachable live endpoint | yes |
| control/branch | Controller owns a new session bootstrapped from another | yes |
| control/reduce | Controller owns a continuation bootstrapped from a verified reversible reduction | yes |
In particular, ACP support does not imply live-process attachment. Attachment
is offered only when attach_existing_process is true and the adapter exposes
attachManagedRuntime. Persisted discovery does not guess reachability. A
trusted host may enrich a discovered descriptor with live_endpoint only
after proving that endpoint owns the same harness-native session; alternatively
it may supply baseUrl explicitly in the attach action. Without either, the
controller keeps attach unavailable even when the harness supports it.
A mirror of a session that is running right now is the one place a send
happens without owning a runtime. Such a session arrives with messageDoor,
the door the mail router reaches it by (runtime, native, hook or
stored); availableActions.send is then true, connection.messaging is
'live_peer', and dispatch({ type: 'send', text }) hands the text to that
session instead of running a turn. Nothing is echoed into the conversation and
the turn is untouched — the message becomes visible when the followed
transcript catches up, which can take seconds. snapshot.delivery records the
hand-off (deliveredToBus) so a view can render "sent to the live session"
until then; a refusal (not_live, identity_mismatch, delivery_failed)
surfaces as a normal controller error. Attachment stays unavailable: a live
peer is messageable, not joinable.
Views persist FrontendSession.identity, never the controller-scoped key or
the reversible locator fingerprint. The identity is an opaque SHA-256 value.
After a host restart, one declarative
restore { identity, connection: 'observe' | 'attach' | 'resume' } command asks the
controller to reselect the exact session and, when requested and still proven
reachable, join its live endpoint — or, with resume, take control of it again
from its persisted state (a host that owned the runtime and restarted). The
session is looked up by identity in the current inventory, refreshing it once
if needed, so a host needs no discovery of its own. Selection ordering and attach safety stay
inside Volter Harness rather than being reimplemented by each view.
Leaving or sharing control is explicit. detach applies only to
control/attach; it closes this frontend and returns to the mirror without
touching the owner runtime. openTerminal applies to editor-owned
start/resume/branch/reduce runtimes and returns structured opaque-receipt attachment
instructions without stopping or resuming anything. Both frontends then drive
one Volter Harness-hosted runtime. A single overloaded “release” operation is
intentionally absent.
Raw terminal display is an optional peer capability, not controller state.
Hosts that want the fleet tool-style tmux discovery, creation, capture, and embedded
screen attachment use @volter/supercode-terminal; the controller keeps
owning the semantic transcript and runtime. This separation lets a frontend
show both views without treating ANSI screen bytes as canonical messages or
pulling native PTY dependencies into browser-only/controller consumers.
Turn state is independent from inventory/operation state:
idle— input may be accepted when capabilities allow it;running— a submitted turn is active;interrupting— an interrupt was requested but terminal confirmation has not arrived;reconciling— terminal completion arrived and persisted history is being loaded; new input remains disabled so reconciliation cannot erase it.
A mirrored session reports turn state too, so a frontend can show that the
agent writing the transcript is working. It comes from the live-runtime
registry's reconciled state — never from transcript growth: while that
session's registered Volter Harness runtime owns a turn the state is running, and
it returns to idle when the turn ends. A session with no registered
Volter Harness runtime stays idle; a harness running outside Volter Harness leaves no
receipt, so nothing authoritative can say whether it is working, and the
controller does not guess. A mirrored turn is reported, never a gate: it
belongs to whoever drives that runtime, so it never disables starting,
branching, or detaching a chat of your own — only a turn this controller owns
does that.
For editor-owned Grok ACP runtimes, an explicit interrupt also rotates the native process after persisted reconciliation and resumes the same session. Grok can acknowledge cancellation while a background tool is still alive; rotation fences any late deltas from that cancelled process out of the next turn. A genuine shared attachment is never terminated by this safeguard.
Conversation projection
The source NormalizedSession remains authoritative. conversation is a
render-friendly but loss-preserving projection:
messageentries retain role, original structured content, extracted text, metadata, andvisibility: conversation | context;toolentries pair calls/results by ID and retain raw arguments/content;reasoning,request, andnoticeare distinct semantic entries;- unknown/native runtime events are never guessed into lifecycle state and are
still available through
subscribeEvents.
No source messages are deleted. Products may hide visibility: context, but
the controller preserves it for inspection and lossless reconciliation.
activeSession exposes the complete last-loaded persisted form, including its
recursive subagents; it is null for a brand-new runtime until that harness
has persisted and reconciliation has loaded it. Live deltas appear immediately
in conversation, so consumers should not mistake activeSession for a
real-time streaming model.
A send action may include typed { id, kind, label, detail } context items. The
controller bounds and wraps them in a reversible transport envelope while the
visible conversation retains the user's clean prompt. This keeps editor
selection or diagnostic context out of presentation text without relying on a
product-specific prompt convention. The projected message retains that context,
so a host can recognize an exact kind: "work-item" reference later without
parsing prose.
Every snapshot also includes taskPlan, a normalized read-only projection of
the active session's native planning protocol. Codex update_plan, Claude task
create/update calls, and OpenCode todo writes map to the same pending /
in-progress / completed / cancelled vocabulary; unknown native values remain in
residue instead of being guessed. deriveTaskPlan(session) exposes the same
pure projection for persisted sessions that are not currently active.
Protocol request entries retain a small resolution after response so a view
can show what was selected even though the request is removed from the pending
queue. Native response payloads remain inside the controller.
Session transfer and handoff
State-machine commands use dispatch(). Operations which return a document or
launch plan are separate typed methods so transient artifacts do not pollute
the durable UI snapshot:
const source = agent.getSnapshot().activeSessionKey;
const raw = await agent.loadSession(source);
const artifact = await agent.translateSession(source, 'codex');
const handoff = await agent.handoffSession(source, 'claude-code');
// handoff.launch and handoff.materialize are structured launches, never shell.handoff.artifact.target_harness names the artifact's actual wire format. Grok
has no import command: its artifact is Grok's own transcript, materializeSession
writes the store entry, and the id it returns replaces {materialized_session_id}
in handoff.launch.
The complete set is loadSession, importSession, exportSession,
translateSession, and handoffSession. They share the controller's FIFO
queue, structured error state, workspace generation, and full-locator lookup.
Repository-native agent packages are exposed from a separate, read-only subpath so a frontend can load portable instructions, capability requirements, and contributions without depending on Volter Harness's internal TOML parser:
import {
SupercodeAgentPackageClient,
selectContributions,
} from '@volter/supercode-client/agent-package';
const packages = new SupercodeAgentPackageClient({ cwd: projectRoot });
const agentPackage = await packages.load();
const docked = agentPackage
? selectContributions(agentPackage.contributions, { region: 'dock' })
: [];See docs/agent-packages.md for the checked-in
.supercode/package.toml format and contribution envelope.
Presentation code sees opaque session keys; storage paths and SQLite selectors
remain inside the trusted controller.
Reversible reduction is a state-machine command because it immediately starts and bootstraps the chosen target harness:
await agent.dispatch({ type: 'reduce', sessionKey: source, targetHarness: 'codex' });
const receipt = agent.getSnapshot().reductionReceipt;
// receipt is present only after the service reloaded, verified, and inverted
// the durable sidecar/log/view bundle.availableActions.reduce is false when the adapter lacks reduceSession, the
source is unavailable, a controller-owned turn is active, or no target runtime
can start. There is no client-side approximation.
Concurrency and recovery invariants
- All public mutations and internal completion reconciliation run through one FIFO queue.
- A workspace change increments a generation before closing old resources; late discovery, follow, and runtime events are ignored.
- The controller never marks a turn idle until completion reconciliation has finished or conclusively cannot load a persisted locator.
- A follower ending or throwing retries with bounded exponential backoff until selection/workspace changes or the controller closes.
- Snapshot references are stable between revisions and replaced atomically on every revision.
- Session actions use opaque controller keys mapped to complete locators; identical native session IDs in different harnesses/stores cannot collide.
- Closing a view does nothing to the controller. Only explicit controller
close, workspace change, detach, terminal handoff, or replacement closes owned resources.
Security posture
The package does not expose a network listener. A host must enforce its own authorization and origin policy. The execution policy is fixed in controller construction; it is intentionally absent from browser-dispatchable commands so an untrusted caller cannot upgrade itself to YOLO mode.
Structured terminal launches remain { program, arguments, cwd, env }. The
package never manufactures a shell string. Rendering/copying a platform-specific
command is a host responsibility.
Non-goals
- A competing coding harness or IDE.
- React/Vue/Svelte components.
- A generic design system.
- Terminal emulation or keystroke injection.
- Claiming that persisted resume attaches to an already-running process.
- Hiding unsupported operations behind optimistic UI.
Durable tracked delivery
Hosts can opt into a separate deliveryJournal for each resident/native profile.
createFileDeliveryJournal(path) from ./node provides atomic, fsynced storage.
A send action with deliveryId deduplicates admission and records queued,
dispatching, delivered and native turn completion/failure/cancellation separately.
deliveryReceipt(id) omits prompt/context/image payloads.
After starting or resuming a controller, call resumePendingDeliveries() to drain
restored queued prompts. Dispatching or delivered records become uncertain on
restart and are never automatically replayed. Persistence failure prevents tracked
input; more than 10,000 retained records refuses new admission. The host owns
journal placement, exclusive controller ownership and archival policy. Native
turn events remain the authority for completion. This tracks transport delivery;
it does not perform scheduling or replace a native agent loop.
