Sidecars

Circuit

Reserve stable operation identities, record attempts and keep unresolved work on its existing leg.

0.1.0-alpha.3Alpha capabilityDeclared targets: Windows, Androiduk.capp.sidecar.solana-circuit

What it does

Circuit is a local operation ledger: reserve an identity before a side effect, record its attempts and keep unresolved work from being replaced on the same execution leg. Use it when a retry must recover the same operation rather than accidentally starting another.

A leg is your application's resource key, such as one invoice or position. The guard combines scopeKey and legKey; it is useful only when competing operations use the same stable keys. Circuit records authorization/evidence supplied by its caller. It does not grant permission, send a transaction or independently verify chain truth.

Use independently

Select uk.capp.sidecar.solana-circuit in local-store mode. It needs no wallet, RPC or Edge. Use window.capp.solana.circuit or the portable client.

Choose operation, scope and leg IDs before invoking the provider; persist those IDs with the business request. In particular, do not create a new operation ID in a retry handler. Omitting a leg removes this particular cross-operation pending guard.

Provider dependencies

ModeRequired serviceResponsibility
local-storeNoneApp/origin-scoped records; the application supplies observed outcomes.
external-adapterConfigured operation-ledger serviceThe service must implement identity, state and persistence semantics. A URL is not distributed durability.

Edge uses Circuit as part of its devnet flow. Direct users may pair it with Dispatch and Witness, or with a completely different application workflow. Circuit does not discover the relevant attempt by scanning another component.

Reserve before doing work

This module uses the portable client. It records a reservation only: there is no preflight, wallet prompt or submission. Invoke it from the application action that begins the business operation, and retain the same arguments for recovery.

js
import {createSolanaClient} from './solana-client.mjs';
const client = createSolanaClient(() => window.capp?.solana);

export async function reserveCircuitLeg({operationId, scopeKey, legKey, intentDigest}) {
  if (!operationId || !scopeKey || !legKey) {
    throw new Error('Supply stable operation, scope and leg IDs');
  }
  const status = await client.call('circuit', 'status');
  if (!status.ready || status.providerMode !== 'local-store') {
    throw new Error('This example requires a ready local Circuit store');
  }
  const input = {operationId, scopeKey, legKey};
  if (intentDigest !== undefined) input.intentDigest = intentDigest;
  const operation = await client.call('circuit', 'reserve', input);
  const trace = await client.call('circuit', 'trace', {operationId});
  return {operationId: operation.operationId, idempotent: operation.idempotent,
    state: trace.state, attempts: trace.attempts, stale: trace.stale};
}

Call it with {operationId: 'docs-order-1', scopeKey: 'docs-shop', legKey: 'invoice-1'}. The first result is reserved; the same call is idempotent. A different operation with the same scope and leg is refused until the original reaches a justified terminal state. An idempotent result might already be submitting or finalised: do not restart its workflow from the beginning.

State and attempt lifecycle

Current stateSupported next states
reservedprepared, preflighted, cancelled, failed
preparedpreflighted, cancelled, failed
preflightedauthority-pending, authorised, cancelled, failed
authority-pendingauthorised, cancelled, failed
authorisedsubmitting, cancelled, failed
submittingsubmitted, unknown, finalised, failed, expired
submittedunknown, finalised, failed, expired
unknownsubmitted, finalised, failed, expired
finalised, failed, cancelled, expiredTerminal; no transition to another state

Use beginAttempt() after actual authorization. It retains the message/transaction binding, creates or returns the exact attempt, and moves the operation to submitting. Do not substitute a bare state transition for recording the attempt.

After an attempt exists, identify it when transitioning submitted or uncertain work. Terminal transitions after submission require a 64-character SHA-256 evidenceDigest; when supplied, attemptState must equal state. The ledger checks the binding and digest shape, not whether the evidence is true. Only an authoritative observation should release a real pending leg. confirmed is an observed state, not a Circuit lifecycle state; use submitted with observedState: confirmed until the required terminal outcome is established.

Scope and limitations

Local persistence is scoped to this app and origin. It is not a distributed database or a cross-device lock. A timeout does not release an unresolved leg. pendingTtlSeconds affects the trace().stale flag only; pendingLocksExpire remains false.

The runtime serializes its requests and uses Web Locks when available. Do not infer a global lock across devices, other origins or contexts without that facility. Source status deliberately distinguishes localPersistence: true from durableOperations: false.

The local implementation retains at most 64 attempts and 512 timeline entries per operation, in addition to the configured operation/store limits. Capacity, corrupted state or a failed storage write is a stop-and-recover condition. Preserve records and restore storage rather than clearing a lock to make the UI proceed.

Application API

The owning interface is window.capp.solana.circuit. The portable client calls the same methods. This table describes main arguments, not a stricter typed guarantee than the current provider implements. Exact transaction/provider bindings must be preserved; unsupported state or arguments are rejected.

js
import {createSolanaClient} from './solana-client.mjs';
const client = createSolanaClient(() => window.capp?.solana);
const status = await client.call('circuit', 'status');

status

client.call('circuit', 'status', input)

Main input: No arguments required.

Read local-store health, capacity and pending-leg behaviour.

reserve

client.call('circuit', 'reserve', input)

Main input: operationId; optional scopeKey, legKey, intentDigest and position identity.

Reserve an idempotent operation. A conflicting identity or an unresolved operation on the same scoped leg is rejected.

read

client.call('circuit', 'read', input)

Main input: operationId.

Read the retained operation. A missing operation is an error, not an empty successful result.

beginAttempt

client.call('circuit', 'beginAttempt', input)

Main input: operationId, messageDigest; optional attemptId, transactionDigest, signature.

Reserve a submission attempt only after authorization. Conflicting bytes and existing pending attempts are rejected.

transition

client.call('circuit', 'transition', input)

Main input: operationId, state; attemptId after submission; evidenceDigest for a terminal post-submission result.

Advance a supported state transition. This records supplied evidence; it does not independently establish chain truth.

trace

client.call('circuit', 'trace', input)

Main input: operationId.

Read attempts, timeline, digest and staleness. Staleness never releases an unresolved leg automatically.

Troubleshooting

Symptom or errorNext step
LEG_PENDINGRead the reported owning operation and reconcile its attempt. Elapsed time is not an outcome.
ID_CONFLICTKeep scope, leg and supplied digests unchanged for the same operation/attempt.
STATECheck the transition table; authorization and terminal states are not interchangeable.
ATTEMPT or ATTEMPT_PENDINGUse the recorded attempt ID. Do not replace an unresolved attempt.
EVIDENCEObtain and retain actual outcome evidence before closing submitted work.
STORAGE_* or CAPACITYStop new effects, preserve the existing store and recover its ability to write.

Verify your integration

Test same-ID replay, same-leg contention, an altered intent digest, reload with a pending attempt, terminal evidence requirements and storage-write failure. Confirm that a stale unknown operation still owns its leg. For an external ledger, separately test its concurrent-client and restart behaviour.

Integration recipes

Composition · Devnet payment · Recovery · Typed client · Schemas

These schemas describe payload shape, not evidence of approval or successful execution. Match the version in your payload; older versions remain listed for compatibility.

Configuration reference

These are the selected package's configuration fields. String credential values and installation-specific defaults are never published here. A field's presence does not bypass its runtime policy.

FieldTypeMeaning and constraints
providerModeselectSelect the implementation this build will use. Configuring an endpoint does not verify its connection or behavior.. Choices: local-store, external-adapter
maxOperationsnumberMaximum local records. At the limit, new records stop; existing history is preserved. Increase the limit or archive and reconcile the workspace.. Bounds: 16 to 4096
pendingTtlSecondsnumberMarks an old record as stale in its trace. This never releases a pending lock.. Bounds: 10 to 86400
externalAdapterUrltextAbsolute HTTPS endpoint, or loopback HTTP for development. Keep private credentials on the backend; packaged configuration is visible to users.

Agent interfaces

These names are declared by the package for host/Agent integration, not JavaScript methods to invoke directly. The host must register a tool, grant its scope and enforce its approval policy. A manifest declaration alone does not prove availability in every Agent catalogue. Prefer the application API above when integrating your own app.

ToolDeclared riskApproval requirement
solana.circuit.statuslowmode-policy

Component changelog

baseline0.1.0-alpha.3Permalink

Circuit

Reserves operation IDs, tracks attempts and protects a pending execution leg from conflicting work.

Documentation snapshot, not a historical release date.
View component notes →
↑ ↓ to navigate · Enter to open · Esc to close. Search stays in your browser.