Skip to content

Dashboard, metrics & wire format

This page documents the WebSocket messages that flow on the agent↔orchestrator and orchestrator↔orchestrator (peer-to-peer) channels, plus the wire format invariants every tier obeys.

These messages enable job-level concurrency control. When an agent discovers a job belongs to a concurrency group (from the workflow definition), it reports to the orchestrator, which decides the action.

Authoritative source: packages/engine/src/protocol/messages/orchestrator-agent.ts

Direction: Agent -> Orchestrator

Agent reports that a job belongs to a concurrency group, requesting permission to proceed.

FieldTypeRequiredDescription
type"job.concurrency.report"YesMessage discriminator
messageIdstringYesUnique message ID
runIdstringYesExecution run ID
jobIdstringYesJob ID within the run
groupstringYesConcurrency group name

Direction: Orchestrator -> Agent

Orchestrator acknowledges concurrency report with action to take.

FieldTypeRequiredDescription
type"job.concurrency.ack"YesMessage discriminator
requestIdstringYesCorrelation ID from the report
actionenumYesOne of: proceed, wait, cancel
reasonstringNoHuman-readable reason for the action
runIdstringNoRun identifier echoed back on unsolicited slot-release wake-ups (see note)
jobIdstringNoJob identifier echoed back on unsolicited slot-release wake-ups (see note)

The orchestrator sends job.concurrency.ack in two situations: (1) as a direct response to a job.concurrency.report — requestId correlates to the report and action is proceed, wait, or cancel; and (2) as an unsolicited follow-up after the agent received wait. When a slot in the concurrency group is released (by completion, cancellation, or superseding), the orchestrator picks the FIFO-next queued waiter and sends { action: 'proceed' } to the agent still parked on its second waitForConcurrencyAck call. runId and jobId are present in this case so the agent can sanity-check the wake-up matches its job.

These messages support periodic metrics push from agents to the orchestrator. The orchestrator aggregates agent metrics and exposes them via its Prometheus scrape endpoint.

Authoritative source: packages/engine/src/protocol/messages/orchestrator-agent.ts

Direction: Agent -> Orchestrator

Periodic metrics push from agent to orchestrator. Each metric includes its type, value, labels, and optional histogram buckets.

FieldTypeRequiredDescription
type"agent.metrics"YesMessage discriminator
messageIdstringYesUnique message ID
agentIdstringYesAgent identifier
metricsMetricEntry[]YesArray of metric data points
timestampnumberYesUnix timestamp (milliseconds)

Each MetricEntry:

FieldTypeRequiredDescription
namestringYesMetric name
typeenumYesOne of: counter, histogram, gauge, upDownCounter
valuenumberNoMetric value (for counters and gauges)
labelsRecord<string, string>NoMetric labels
buckets[{ le: number, count: number }]NoHistogram bucket boundaries and counts
countnumberNoHistogram observation count
sumnumberNoHistogram observation sum

Authoritative source: packages/engine/src/protocol/messages/orchestrator-agent.ts — agentMetricsSchema

Authentication messages for the agent <-> orchestrator WebSocket connection.

Authoritative source: packages/engine/src/protocol/messages/orchestrator-agent.ts

Auth request sent by agent to orchestrator when connecting.

FieldTypeRequiredDescription
type"auth.request"YesMessage discriminator
tokenstringYesAuthentication token
protocolVersionnumberYesProtocol version (positive integer)

Auth success response sent by orchestrator to agent.

FieldTypeRequiredDescription
type"auth.success"YesMessage discriminator
connectionIdstringYesAssigned connection ID

Auth failure response sent by orchestrator to agent.

FieldTypeRequiredDescription
type"auth.failure"YesMessage discriminator
reasonstringYesHuman-readable failure reason

Authoritative source: packages/engine/src/protocol/messages/orchestrator-agent.ts — agentAuthFailureSchema

Join messages carry join protocol v2. A new orchestrator proves it holds a join token without sending the token or its secret. An existing orchestrator answers with its own proof and the configuration bundle sealed to a one-time key of the new orchestrator. A relay sees the token’s routing part, both public keys, both nonces, both proofs and the ciphertext. None of these lets it read or change the bundle. A join.request that carries a token field is refused with join_protocol_v1_removed.

Authoritative source: packages/engine/src/protocol/messages/join.ts

New orchestrator -> existing orchestrator (via Platform relay or direct peer)

Section titled “New orchestrator -> existing orchestrator (via Platform relay or direct peer)”

Sent by a new orchestrator to request cluster config from an existing orchestrator.

FieldTypeRequiredDescription
type"join.request"YesMessage discriminator
messageIdstringNoCorrelation ID for Platform relay routing (injected by Platform to match response to correct joiner)
joinProtocol2YesJoin protocol version
routingstringYesThe token’s base64url routing part, exactly as it appears in the token
joinerPublicKeystringYesThe new orchestrator’s one-time X25519 public key (DER SPKI, base64)
joinerNoncestringYes32 random bytes, base64
joinerProofstringYesHMAC-SHA256 over the request transcript, lowercase hex

Existing orchestrator -> new orchestrator (via Platform relay or direct peer)

Section titled “Existing orchestrator -> new orchestrator (via Platform relay or direct peer)”

Response from an existing orchestrator with the sealed config bundle, or a refusal.

FieldTypeRequiredDescription
type"join.response"YesMessage discriminator
messageIdstringNoCorrelation ID echoed from join.request for relay routing
successbooleanYesWhether the join was successful
joinProtocol2NoJoin protocol version (on success)
serverPublicKeystringNoThe existing orchestrator’s one-time X25519 public key (DER SPKI, base64)
serverNoncestringNo32 random bytes, base64
serverProofstringNoHMAC-SHA256 over the response transcript, lowercase hex
encryptedBundlestringNoBase64 AES-256-GCM sealed config bundle (on success)
errorstringNoError message (on failure)
errorCodestringNoWhy the join was refused (see below). Absent on an internal failure
errorCodeProduced byMeaning
join_protocol_v1_removedPlatform, orchestrator, routeThe request carries the join token (join protocol v1). Upgrade kici-admin
join_protocol_unsupportedNoneNot sent at protocol 4: every orchestrator answers join protocol v2. kici-admin join still names the upgrade when it reads this code
invalid_requestPlatform, orchestrator, routeThe request is malformed
invalid_tokenOrchestrator, routeNo live join token matches the request’s routing part and proof
token_expiredOrchestrator, routeThe matching join token has expired
token_already_usedOrchestrator, routeAnother joiner consumed the join token
org_mismatchPlatformThe routing part names an org other than the relaying connection’s own org
no_targetPlatformThe target pool holds no orchestrator
relay_timeoutPlatformThe target orchestrator did not answer within 30 seconds

“Route” is POST /api/v1/cluster/join on the existing orchestrator, the direct transport of kici-admin join --peer. It answers 200 on success, 400 for invalid_request, 401 for invalid_token, token_expired and token_already_used, 426 with Upgrade: kici-join-v2 for join_protocol_v1_removed, and 500 for an internal failure.

The Browser ↔ Platform protocol (browser-side WebSocket auth, log subscriptions, status fan-out) is documented in the internal docs.

Orchestrator <-> Orchestrator messages (peer-to-peer)

Section titled “Orchestrator <-> Orchestrator messages (peer-to-peer)”

This layer carries cluster coordination messages between orchestrator instances via direct WebSocket connections on /ws/peer. These messages never transit the Platform tier — see Communication Topology.

Authoritative source: packages/engine/src/protocol/messages/peer.ts

Peer authentication uses a 4-message ECDH handshake with mutual authentication (mutual-v2). The receiving orchestrator (the server, on /ws/peer) sends peer.hello, the connecting orchestrator (the client) replies with peer.hello.response, then the client sends an encrypted peer.auth.request, and the server replies with an encrypted peer.auth.response. Both messages after the hello exchange use the handshake key K_hs. No credential or join token crosses the wire: each side sends a proof bound to the handshake transcript. The client accepts nothing until the server’s proof verifies. After an acceptance both sides switch to the application key K_app, which needs the shared credential or token as well as K_hs.

Sent by the server when a client connects. Provides the server’s ephemeral ECDH public key, a nonce and the authentication schemes it accepts.

FieldTypeRequiredDescription
type"peer.hello"YesMessage discriminator
ephemeralPublicKeystringYesServer’s ephemeral X25519 public key (base64, DER SPKI)
noncestringYesBase64-encoded 32-byte random nonce (HKDF salt)
authSchemesstring[]NoSchemes the server accepts (mutual-v2). A client refuses a hello without mutual-v2 and sends nothing that depends on a secret

Sent by the client in response to peer.hello. Provides the client’s ephemeral ECDH public key. After this exchange, both sides derive K_hs and the transcript hash.

FieldTypeRequiredDescription
type"peer.hello.response"YesMessage discriminator
ephemeralPublicKeystringYesClient’s ephemeral X25519 public key (base64, DER SPKI)

Sent by the client after the ECDH handshake. Encrypted with K_hs. The client proves that it holds its credential (credential mode) or its join token (token mode, first join). The server closes a connection that has not authenticated within 15 seconds. A request that carries proof or token, or no scheme, is refused with the reason Mutual peer authentication required.

FieldTypeRequiredDescription
type"peer.auth.request"YesMessage discriminator
instanceIdstringYesClient’s cluster instance ID
protocolVersionnumberYesProtocol version
schemeenumYesmutual-v2
modeenumYescredential or token
clientProofstringYesHex HMAC-SHA256 proof over the handshake transcript
tokenRoutingstringConditionalThe join token’s base64url routing part (token mode only; never the token)
softwareVersionstringNoSoftware version of the client (logged)
roleenumNoRole the client declares: coordinator or worker; the proof covers it

Response to a peer authentication request. Encrypted with K_hs.

FieldTypeRequiredDescription
type"peer.auth.response"YesMessage discriminator
acceptedbooleanYesWhether authentication succeeded
instanceIdstringNoServer’s cluster instance ID (present on every acceptance)
serverProofstringNoHex proof that the server holds the same credential or token (present on every acceptance)
sessionCredentialstringNoIssued credential for future connections (token mode); the server proof covers it
rolestringNoRole the server grants, from the credential row or the join-token row (present on acceptance)
reasonstringNoRejection reason (if not accepted)
softwareVersionstringNoSoftware version of the server (logged)
agentsPeerAgentSummary[]NoServer’s connected agent inventory (present when accepted=true)
scalerCapacityScalerCapacitySummary[]NoServer’s scaler capacity (present when accepted=true)
capabilitiesPeerCapabilitiesNoServer’s feature capabilities (present when accepted=true)

Sent every 30 seconds by each peer. Carries agent inventory, scaler capacity, and Raft consensus state. This is the primary mechanism for routing decisions — the coordinator uses peer heartbeats to determine which peers can handle a job’s label requirements.

FieldTypeRequiredDescription
type"peer.heartbeat"YesMessage discriminator
instanceIdstringYesSender’s cluster instance ID
termnumberYesCurrent Raft term
leaderIdstring or nullYesKnown Raft leader’s instance ID
drainingbooleanYesWhether the sender is gracefully shutting down
agentsPeerAgentSummary[]YesConnected agent inventory
capabilitiesPeerCapabilitiesYesFeature flags (S3 log access, log routing override)
scalerCapacityScalerCapacitySummary[]NoOn-demand backend capacity for routing decisions
configVersionnumberNoShared config version for sync detection
registryVersionnumberNoRegistry version for cross-orchestrator registration sync
clusterSettingsVersionnumberNoShared cluster-settings version for the coordinator-to-worker settings pull
timestampnumberYesUnix timestamp (milliseconds)
hostnamestringNoMachine hostname (os.hostname())
osReleasestringNoOS kernel release (os.release())
totalMemoryMbnumberNoTotal system memory in MiB
memoryUsedMbnumberNoUsed memory in MiB
memoryAvailableMbnumberNoAvailable memory in MiB
cpuCountnumberNoNumber of logical CPUs
uptimeSecondsnumberNoSystem uptime in seconds
nodeVersionstringNoNode.js version
runningAsUserstring or nullNoUsername of the OS user running the orchestrator
runningAsUidnumber or nullNoUID of the OS user running the orchestrator
versionstringNoOrchestrator version (e.g., "0.0.1")

PeerAgentSummary:

FieldTypeRequiredDescription
agentIdstringYesAgent identifier
labelsstring[]YesCapability labels
activeJobsnumberYesCurrently running jobs
maxConcurrencynumberYesMaximum concurrent jobs
platformstringYesOS platform (e.g., linux)
archstringYesCPU architecture (e.g., x64, arm64)
mandatoryLabelsstring[]YesTaint gate inherited from the spawning scaler. A static agent sends [], which is “no gate”
scalerNamestring or nullNoBackend that spawned this agent, or null for a static agent. Groups the peer’s agents in diagnostics

PeerCapabilities:

FieldTypeRequiredDescription
s3LogAccessbooleanYesWhether this peer has direct S3 log access
logRoutingOverrideenumNoOne of: direct, coordinator (overrides default routing)

ScalerCapacitySummary:

FieldTypeRequiredDescription
namestringNoScaler backend name (e.g., worker-bare-metal)
typestringNoScaler backend type (e.g., bare-metal, container)
labelSetsstring[][]YesLabel sets this backend provisions
maxAgentsnumberYesMaximum agents for this backend
activeCountnumberYesCurrent active agent count
spawnsOnLocalHostbooleanNoWhether this backend spawns its agents on the peer’s own host
labelSetMandatoryLabelsstring[][]YesTaint gate per label set, index-aligned with labelSets; an empty entry is “no gate”. An entry whose length does not match routes nothing

The object is strict: a frame carrying any other field fails to parse.

Raft leader election vote request. Sent by candidates during elections.

FieldTypeRequiredDescription
type"raft.vote.request"YesMessage discriminator
termnumberYesCandidate’s current term
candidateIdstringYesCandidate’s instance ID
lastLogIndexnumberYesCandidate’s last log index
lastLogTermnumberYesTerm of candidate’s last log entry

Response to a Raft vote request.

FieldTypeRequiredDescription
type"raft.vote.response"YesMessage discriminator
termnumberYesVoter’s current term
voteGrantedbooleanYesWhether the vote was granted
voterIdstringYesVoter’s instance ID

Raft leader heartbeat (no log entries — KiCI uses Raft for leader election only, not log replication).

FieldTypeRequiredDescription
type"raft.append.entries"YesMessage discriminator
termnumberYesLeader’s current term
leaderIdstringYesLeader’s instance ID

Sent by the coordinator to a peer when no local agent can handle a job. Contains the full resolved job configuration so the peer can dispatch without re-resolving.

FieldTypeRequiredDescription
type"job.reroute"YesMessage discriminator
messageIdstringYesUnique message ID for ACK correlation
jobIdstringYesPre-allocated job ID. The sending coordinator allocates it before reroute so its execution_runs / execution_jobs rows reference the same id the receiving peer will dispatch under.
runIdstringYesExecution run identifier
deliveryIdstringYesOriginal webhook delivery ID
routingKeystringYesProvider routing key
event, actionstring, string or nullYesWebhook event type and action
payloadRecord<string, unknown>YesFull webhook payload
jobNamestringYesJob to execute
workflowNamestringYesWorkflow containing the job
runsOnLabelsstring[][]YesLabel sets the job requires
excludeLabelsstring[]NoLabels that the dispatched agent must NOT have
runsOnPatternsLabelMatcher[]NoGlob/regex include matchers the receiving peer’s agent labels must satisfy, applied on top of the exact runsOnLabels prefilter. Absent reads as []; a pure-pattern job carries its whole selector here
excludePatternsLabelMatcher[]NoGlob/regex matchers that disqualify a candidate agent. Absent reads as []
triedConnectionsstring[]YesInstance IDs already tried (loop prevention)
maxHopsnumberYesMaximum allowed hops (default: 3)
spawnRetry{ maxAttempts: number, backoffMs: number }YesSpawn-retry budget the receiving worker applies to the job: agent spawns it attempts (>= 1) and the wait after a failed one (ms, >= 0)
coordinatorIdstringYesInstance ID of the run coordinator
jobConfigRecord<string, unknown>NoResolved job config (steps, rules, matrix, etc.)
repoUrlstringNoRepository clone URL
refstringNoGit ref (branch name)
shastringNoCommit SHA
providerstringNoProvider type (e.g., github)
providerContextRecord<string, unknown>NoProvider-specific context (e.g., installationId)
sourceTarUrlstringNoPre-signed .kici/ source tarball download URL (cache hit)
sourceTarDigeststringNoSHA-256 of the source tarball’s own bytes, for integrity verification
depsUrlstringNoPre-signed dependency tarball URL (cache hit)
depsHashstringNoSHA-256 of the dependency tarball bytes
cloneTokenstringNoPre-resolved clone token for workers without provider credentials
workflowCloneTokenstringNoPre-resolved clone token for the workflow repository of an organization-wide (global) job
encryptedSecretsstringNoEncrypted secrets envelope (AES-256-GCM with session key)
encryptedNamespacedSecretsstringNoEncrypted namespaced secrets envelope
requestIdstringNoTrace ID for distributed tracing
traceIdstringNoAdditional trace context

LabelMatcher: { kind: "exact", value: string } or { kind: "regex", source: string, flags: string }. Globs are converted to regex at compile time, so the wire only ever carries these two forms.

Acknowledgment of a reroute request. The coordinator tries the next peer if rejected.

FieldTypeRequiredDescription
type"job.reroute.ack"YesMessage discriminator
messageIdstringYesID of the job.reroute being acknowledged
acceptedbooleanYesWhether the peer accepted the job
reasonstringNoRejection reason (if not accepted)

Sent by the worker orchestrator back to the coordinator as the agent reports job-level and step-level state changes. The coordinator uses this to drive its run-level status tracking, update GitHub check runs, and track per-step progress.

The kind discriminator decides which ExecutionTracker call the receiver makes:

  • kind: "job" → onJobStatus(runId, jobId, state, ...) — drives run transitions (running → success/failed/…). stepIndex/stepName are unused for this kind.
  • kind: "step" → onStepStatus(runId, jobId, stepIndex, stepName, state, ...) — persists execution_steps rows.
FieldTypeRequiredDescription
type"job.progress"YesMessage discriminator
kind"job" | "step"YesWhether this update is a job-level or step-level transition
runIdstringYesExecution run identifier
jobIdstringYesJob identifier (matches the pre-allocated jobId from job.reroute)
jobNamestringYesHuman-readable job name
stepIndexnumberYesZero-based step index (used only when kind="step")
stepNamestringYesHuman-readable step name (used only when kind="step")
stateenumYesFull ExecutionJobStatus for kind="job"; the step-state subset (running/success/failed/skipped) for kind="step"
timestampnumberYesUnix timestamp (milliseconds)
dataRecord<string, unknown>NoOptional state-specific data

Sent by the coordinator back to the worker once it has applied a terminal job-level job.progress (kind: "job") to its run/job rows. The worker uses it to prune the matching record from its durable terminal-status outbox, so a status that has been durably observed by the coordinator is delivered exactly once. (runId, jobId) is the dedup key; state is carried for debuggability.

FieldTypeRequiredDescription
type"job.progress.ack"YesMessage discriminator
runIdstringYesExecution run identifier
jobIdstringYesJob identifier (matches the acknowledged job.progress)
stateenumYesThe applied ExecutionJobStatus

Sent by a coordinator to the coordinator or worker that holds the job’s agent. Used for fail-fast propagation and user-initiated cancellation.

FieldTypeRequiredDescription
type"peer.job.cancel"YesMessage discriminator
runIdstringYesExecution run identifier
jobIdstringNoSpecific job to cancel; omit to cancel every job of the run the receiver tracks
reasonstringYesHuman-readable cancellation reason
forcebooleanNoWhen true, force-cancel immediately without waiting for hooks

Graceful shutdown announcement. Peers remove the sender from their registry immediately upon receiving this message.

FieldTypeRequiredDescription
type"peer.leaving"YesMessage discriminator
instanceIdstringYesInstance ID of the leaving peer
termnumberYesCurrent Raft term for leader identification

Log and cache relay (coordinator-worker topology)

Section titled “Log and cache relay (coordinator-worker topology)”

Log chunk relay from worker to coordinator. Batched log lines from agent execution, forwarded when the worker does not have direct S3 log access.

FieldTypeRequiredDescription
type"peer.log.chunk"YesMessage discriminator
runIdstringYesExecution run ID
jobIdstringYesJob ID within the run
stepIndexnumberYesZero-based step index
linesLogLineEntry[]YesArray of log line entries

Each LogLineEntry:

FieldTypeRequiredDescription
textstringYesLog line text
timestampnumberYesUnix timestamp (milliseconds)
streamenumYesOne of: stdout, stderr

Authoritative source: packages/engine/src/protocol/messages/peer.ts — peerLogChunkSchema

Cache upload request from worker to coordinator. Worker agent needs an upload URL for cache storage.

FieldTypeRequiredDescription
type"peer.cache.upload.request"YesMessage discriminator
messageIdstringYesUnique message ID
runIdstringYesExecution run ID
jobIdstringYesJob ID within the run
cacheTypeenumYesOne of: source, deps
hashstringYesContent hash for cache keying
sizeBytesnumberYesSize of the artifact in bytes

Authoritative source: packages/engine/src/protocol/messages/peer.ts — peerCacheUploadRequestSchema

Cache upload response from coordinator to worker. Pre-signed upload URL for the worker to upload directly to object storage.

FieldTypeRequiredDescription
type"peer.cache.upload.response"YesMessage discriminator
messageIdstringYesUnique message ID
runIdstringYesExecution run ID
jobIdstringYesJob ID within the run
uploadUrlstringYesPre-signed URL for direct S3 upload

Authoritative source: packages/engine/src/protocol/messages/peer.ts — peerCacheUploadResponseSchema

When an operator collects a debug bundle that spans the cluster, a coordinator fans a collection request down the peer mesh and each peer assembles and streams back the ZIP for its own subtree.

Ask a peer to assemble and stream back the log bundle for its downstream subtree.

FieldTypeRequiredDescription
type"peer.logs.collect.request"YesMessage discriminator
messageIdstringYesUnique message ID
logWindowHoursnumberYesHow many hours of logs to gather
includeCoordinatorMeshbooleanYesLoop guard: false on every downstream request so the coordinator mesh never echoes
selectionFleetSelectionYesWhich downstream nodes to gather

Each FleetSelection:

FieldTypeRequiredDescription
allbooleanYesGather every downstream node; ignores the id lists below
agentIdsstring[]NoSpecific agent IDs to gather (defaults to empty)
workerInstanceIdsstring[]NoSpecific worker instance IDs to gather (defaults to empty)

Authoritative source: packages/engine/src/protocol/messages/peer.ts — peerLogsCollectRequestSchema

One base64-encoded frame of a peer’s subtree ZIP, streamed back to the requesting coordinator.

FieldTypeRequiredDescription
type"peer.logs.collect.chunk"YesMessage discriminator
messageIdstringYesUnique message ID (matches the request)
seqnumberYesZero-based, non-negative frame sequence number
isLastbooleanYesWhether this is the final frame
dataB64stringYesBase64-encoded ZIP frame

Authoritative source: packages/engine/src/protocol/messages/peer.ts — peerLogsCollectChunkSchema

Sent when a peer fails to assemble or stream its subtree bundle.

FieldTypeRequiredDescription
type"peer.logs.collect.error"YesMessage discriminator
messageIdstringYesUnique message ID (matches the request)
messagestringYesFailure reason

Authoritative source: packages/engine/src/protocol/messages/peer.ts — peerLogsCollectErrorSchema

Config reload request forwarded from one orchestrator to a specific peer when an operator calls POST /admin/config/reload with a target parameter. The receiving peer executes a local reload and replies with peer.config.reload.response.

FieldTypeRequiredDescription
type"peer.config.reload"YesMessage discriminator
messageIdstringYesUnique message ID
drainbooleanNoWhether to drain in-flight work before reloading

Authoritative source: packages/engine/src/protocol/messages/peer.ts — peerConfigReloadSchema

Response from the target peer carrying the reload result fields.

FieldTypeRequiredDescription
type"peer.config.reload.response"YesMessage discriminator
messageIdstringYesUnique message ID
successbooleanYesWhether the reload succeeded
versionnumberNoNew config version after reload
errorsstring[]NoError messages if reload failed
restartRequiredstring[]NoConfig fields that require a restart to take effect
fieldsChangedstring[]NoConfig fields that were changed
scalerobjectNoThe target’s scaler config reload: outcome, and plan or errors (see peer.scaler.reload.response)

Authoritative source: packages/engine/src/protocol/messages/peer.ts — peerConfigReloadResponseSchema, scalerReloadAnswerSchema

Cluster-settings pull request: a DB-less worker asks its first connected coordinator for the current worker-settings snapshot after a coordinator advertised a clusterSettingsVersion (on peer.heartbeat) ahead of its own. Every coordinator reads the same cluster_settings row, so any coordinator answers. The coordinator replies with peer.clusterSettings.response.

FieldTypeRequiredDescription
type"peer.clusterSettings.request"YesMessage discriminator
messageIdstringYesUnique message ID

Authoritative source: packages/engine/src/protocol/messages/peer.ts — peerClusterSettingsRequestSchema

Cluster-settings pull response: the coordinator resolves the worker-relevant settings snapshot from its cluster settings and replies with it plus the current version. The snapshot is a typed, concrete object (not an open key/value bus) — each worker-consumed cluster knob is a field on it.

FieldTypeRequiredDescription
type"peer.clusterSettings.response"YesMessage discriminator
messageIdstringYesUnique message ID
versionnumberYesCurrent cluster-settings version
settingsWorkerClusterSettingsYesWorker-settings snapshot (e.g., agentTokenTtlMs)

Authoritative source: packages/engine/src/protocol/messages/peer.ts — peerClusterSettingsResponseSchema, workerClusterSettingsSchema

Request forwarded from a coordinator to the node an operator targeted with kici-admin scaler orphans --target. The node answers from its own host and its own tracking with peer.scaler.orphans.response.

FieldTypeRequiredDescription
type"peer.scaler.orphans.request"YesMessage discriminator
messageIdstringYesUnique message ID
action"list" | "stop"YesList the live Firecracker VMs, or stop some of them
vmIdsstring[]Nostop only: the VM IDs the operator approved

Authoritative source: packages/engine/src/protocol/messages/peer.ts — peerScalerOrphansRequestSchema

Response from the target node. vms answers a list, and results answers a stop. When ok is false, the response carries the node’s error instead.

FieldTypeRequiredDescription
type"peer.scaler.orphans.response"YesMessage discriminator
messageIdstringYesMatches the request
okbooleanYesWhether the node answered the request
errorstringNoWhy the node could not answer
firecrackerScalersstring[]NoThe node’s Firecracker scaler names; empty when it runs none
vmsobject[]NoEach live VM: vmId, scaler, pid, startedAt, ageSeconds, chrootDir, status, trackedBy, reason
resultsobject[]NoEach stop result: vmId, outcome, pid, detail

A VM status is orphaned, unverified or tracked. A stop outcome is stopped, tracked, unverified, not-live, not-found or error. Only stopped signalled a process.

Authoritative source: packages/engine/src/protocol/messages/peer.ts — peerScalerOrphansResponseSchema

The orchestrator that received kici-admin scaler reload (without --single) sends this message to each connected peer, coordinators and workers alike, so each one re-reads its own scaler config. A peer accepts it from coordinators only: the incoming peer connection checks that the sender’s credential is a coordinator’s.

FieldTypeRequiredDescription
type"peer.scaler.reload.request"YesMessage discriminator
messageIdstringYesUnique message ID

Authoritative source: packages/engine/src/protocol/messages/peer.ts — peerScalerReloadRequestSchema

What the peer’s reload did. outcome is applied, rejected, not-configured or unreachable. A rejected reload applied nothing.

FieldTypeRequiredDescription
type"peer.scaler.reload.response"YesMessage discriminator
messageIdstringYesMatches the request
outcomestringYesWhat the reload did on this peer
planobjectNoapplied only: scaler names per bucket (added, updated, unchanged, retired, resurrected) and changed global limits
errorsstring[]Norejected only: why the file was refused
detailstringNoWhy the peer could not reload (for example, a request from a peer that is not a coordinator)

Authoritative source: packages/engine/src/protocol/messages/peer.ts — peerScalerReloadResponseSchema, scalerReloadPlanSchema

A coordinator that forgot a departed peer (kici-admin peer forget) sends this message to each connected sibling coordinator, so each one drops the peer from its own live peer registry. A peer accepts it from coordinators only.

FieldTypeRequiredDescription
type"peer.forget.request"YesMessage discriminator
messageIdstringYesUnique message ID
instanceIdstringYesThe departed peer’s instance ID
acknowledgeBackstopbooleanNoThe operator acknowledged that the forget may switch the event-provision backstop back on

Authoritative source: packages/engine/src/protocol/messages/peer.ts — peerForgetRequestSchema

What the sibling did with the request. outcome is forgotten, not-found, connected, recent, acknowledgement-required or error. Every outcome except forgotten keeps the peer.

FieldTypeRequiredDescription
type"peer.forget.response"YesMessage discriminator
messageIdstringYesMatches the request
outcomestringYesWhat the forget did on this coordinator
detailstringYesThe reason, readable by an operator

Authoritative source: packages/engine/src/protocol/messages/peer.ts — peerForgetResponseSchema

Broadcast to every peer when an agent token is revoked so each peer can close its own in-flight agent WS connections authenticated by that token. The originating peer kicks its local connections first (via the admin revoke route), then fans this message out over the encrypted peer mesh.

FieldTypeRequiredDescription
type"peer.agent-token.revoke"YesMessage discriminator
tokenIdstringYesThe agent_tokens.id whose in-flight agent WS must be kicked
senderInstanceIdstringYesOriginating peer’s instance ID (for cross-cluster log correlation)

Authoritative source: packages/engine/src/protocol/messages/peer.ts — peerAgentTokenRevokeSchema

Forwarded by a worker to the coordinator that owns the run when the worker’s scaler emits a provisioning event correlated to a queued job (e.g. a failed agent spawn). Workers have no database, so they cannot persist provisioning failures themselves — the coordinator’s ExecutionTracker writes the event to the provisioning log and the dispatch queue’s last-error column.

FieldTypeRequiredDescription
type"scaler.event"YesMessage discriminator
runIdstringYesExecution run ID
jobIdstringYesJob ID within the run
agentIdstringYesThe scaler-managed agent ID the event is about
eventTypeenumYesScaler event type (a ScalerEventType enum member)
detailstringYesHuman-readable detail, including any captured spawn stderr tail
timestampMsnumberYesEvent timestamp in epoch milliseconds
finalbooleanNoRetry verdict on a scaler.failed for a job under the worker’s spawn-retry budget: false while attempts remain, true on the last one. Absent on a repeated report of one failed spawn and on every other relay

Authoritative source: packages/engine/src/protocol/messages/peer.ts — peerScalerEventSchema

See Multi-Orchestrator Architecture for rerouting protocol, loop prevention, and failure modes.

These messages implement the Platform-routed kici run remote flow. On this path the CLI does not connect to the orchestrator: it authenticates to the Platform. The Platform proxies each control message to the customer’s orchestrator over the dashboard-direction WebSocket and relays the response back to the CLI. Every request carries an actor principal (so the orchestrator can authorize the operation) and a requestId (so the Platform can correlate the matching response).

Authoritative source: packages/engine/src/protocol/messages/dashboard.ts

The orchestrator also serves the same handlers over HTTP at /api/v1/test/* for a direct run, authenticated with an orchestrator admin token. Those routes accept the request bodies below minus type, requestId, actor and routingKey, which the orchestrator sets itself.

The control plane is five request/response pairs. Requests travel Platform -> Orchestrator (members of dashboardPlatformToOrchSchema); responses travel Orchestrator -> Platform (members of dashboardOrchToPlatformSchema). On every response the optional error string carries a failure reason when the operation could not be served.

Request a presigned URL for uploading the working-tree overlay tarball.

FieldTypeRequiredDescription
type"test.relay.uploads.init"YesMessage discriminator
requestIdstringYesCorrelation ID for the response
actorActorPrincipalYesAuthenticated principal initiating the run
routingKeystringYesProvider routing key
shastringNoContent hash of the overlay being uploaded
fileCountnumberNoNumber of files in the overlay
compressedSizenumberNoCompressed overlay size in bytes

Trigger a remote test run using a fixture payload (mirrors the orchestrator’s TestTriggerInput).

FieldTypeRequiredDescription
type"test.relay.trigger"YesMessage discriminator
requestIdstringYesCorrelation ID for the response
actorActorPrincipalYesAuthenticated principal initiating the run
routingKeystringYesProvider routing key
fixtureIdstringYesFixture identifier
eventTestEventYesSimulated event (type, action, branch, payload)
workflowNamestringNoSpecific workflow to test (all if omitted)
uploadIdstringNoUpload ID returned by test.relay.uploads.init.response
cliPublicKeystringNoCLI ephemeral X25519 public key used to encrypt the overlay tarball
inlineLockFilestringNoLock file sent inline instead of via upload
fullRepobooleanNoWhether the upload is the full working tree (not an overlay)
checkModeenumNoRun mode for idempotent steps, relayed onto the dispatch event. Omitted means apply
targetHostTargetSelectorNoHost narrowing from kici run remote --target, intersected with each runsOnAll job’s matched roster. Omitted for webhook runs
dispatchInputsRecord<string, string>NoRaw kici run remote --input KEY=VALUE pairs, relayed verbatim. The orchestrator validates, coerces and defaults them against the matched workflow’s lock descriptor
secretsRecord<string, string>NoPlaintext secrets for the run
encryptedSecretsstringNoEncrypted secrets blob
encryptedSecretsKeystringNoKey wrapping the encrypted secrets blob
acceptsMultipleRunsbooleanNoThe caller follows every run in the response’s runs, so a fixture that matches several workflows gets one run per workflow. Without it the orchestrator answers as for a caller that follows only runId

TestEvent: { type: string, action?: string, targetBranch: string, sourceBranch?: string, payload: Record<string, unknown>, changedFiles?: string[] }

Request a snapshot of a run’s status.

FieldTypeRequiredDescription
type"test.relay.run.status"YesMessage discriminator
requestIdstringYesCorrelation ID for the response
actorActorPrincipalYesAuthenticated principal
runIdstringYesExecution run ID

Request the next chunk of a run’s logs from a cursor.

FieldTypeRequiredDescription
type"test.relay.run.logs"YesMessage discriminator
requestIdstringYesCorrelation ID for the response
actorActorPrincipalYesAuthenticated principal
runIdstringYesExecution run ID
cursornumberYesLines of the sorted stream already delivered; read when logCursor is absent
logCursorstringNoOpaque per-file cursor: a prior nextLogCursor, or the empty string to start. When present, the orchestrator reads from it and ignores cursor

Request cancellation of a run.

FieldTypeRequiredDescription
type"test.relay.cancel"YesMessage discriminator
requestIdstringYesCorrelation ID for the response
actorActorPrincipalYesAuthenticated principal
runIdstringNoRun ID to cancel
branchstringNoCancel the active run for this branch

Presigned upload URL plus the ephemeral encryption key the CLI uses to encrypt the overlay.

FieldTypeRequiredDescription
type"test.relay.uploads.init.response"YesMessage discriminator
requestIdstringYesID of the request being responded to
uploadIdstringNoUpload record ID
signedUrlstringNoPresigned PUT URL for the overlay tarball
publicKeystringNoOrchestrator public key for overlay encryption
expiresInnumberNoURL validity window in seconds
errorstringNoFailure reason (when the URL could not be minted)

Acknowledge a triggered run.

FieldTypeRequiredDescription
type"test.relay.trigger.response"YesMessage discriminator
requestIdstringYesID of the request being responded to
runIdstringNoDeprecated: read runs. The first run of runs, or the one run when the request did not set acceptsMultipleRuns
runsTriggeredRun[]NoEvery run the trigger created, one per matched workflow. Absent from an orchestrator that predates the field; read runId then
statusenumNoOne of: accepted, rejected
reasonstringNoRejection reason (if rejected)
jobIdsstring[]NoIDs of the jobs created for the run
warningsstring[]NoUser-visible warnings on acceptance (e.g. a skipped non-test bound context)
errorstringNoFailure reason

TriggeredRun: { runId: string, workflowName: string, jobIds?: string[] }

A run-status snapshot.

FieldTypeRequiredDescription
type"test.relay.run.status.response"YesMessage discriminator
requestIdstringYesID of the request being responded to
runIdstringNoExecution run ID
statusstringNoRun status
jobsarrayNoPer-job status: { jobId, jobName, status, exitCode?, errorMessage?, durationMs? }. durationMs is the job’s duration in milliseconds; when it is absent, the CLI shows the duration as unknown
failureReasonstring | nullNoWhy the run failed, when it failed before or outside any job (an init failure records no job). Absent from an orchestrator that predates the field
donebooleanNoWhether the run has reached a terminal state
errorstringNoFailure reason

The next log chunk and the cursors to read on from.

FieldTypeRequiredDescription
type"test.relay.run.logs.response"YesMessage discriminator
requestIdstringYesID of the request being responded to
linesstring[]NoLog output lines
nextCursornumberNoCursor for the next test.relay.run.logs request
nextLogCursorstringNoOpaque per-file cursor for the next request; present when the request sent logCursor
lineJobNamesstring[]NoThe job that wrote each line, index for index with lines. Present when the request sent logCursor; absent from an orchestrator that predates the field
donebooleanNoWhether the log stream is exhausted
errorstringNoFailure reason

Acknowledge a cancellation.

FieldTypeRequiredDescription
type"test.relay.cancel.response"YesMessage discriminator
requestIdstringYesID of the request being responded to
cancelledbooleanNoWhether the run was cancelled
errorstringNoFailure reason

KiCI defines custom close codes in the 4000-4999 range (reserved for application use by RFC 6455) alongside standard codes.

CodeConstantMeaningSent by
1000—Normal closure. Sent by the connecting side when it disconnects on purpose — an agent or orchestrator shutting down its uplink, or any client (agent, orchestrator uplink, or peer) abandoning a handshake that failedClient
1001WS_CLOSE_GOING_AWAYServer shutdown or browser navigating away. The orchestrator also sends it to an agent that reaches the ownership-violation threshold (5 frames for jobs it does not own within 60 seconds)Server
4001WS_CLOSE_UNAUTHORIZEDClient failed authentication. Also sent when the Platform times out an unauthenticated connection, when a client’s API key is revoked mid-session, on an operator force-disconnect, and when a dashboard session loses its org membership or exceeds its connection limit. On the orchestrator peer channel it closes a rate-limited, unauthenticated, or role-mismatched peerServer
4002WS_CLOSE_AUTH_TIMEOUTAuth timeout expired. The orchestrator also sends it to an agent that authenticates but does not send agent.register in time. On the orchestrator peer channel it closes a peer that does not finish authenticating in timeServer
4003WS_CLOSE_INVALID_MESSAGEInvalid or unparseable message received. The orchestrator also sends it to an agent whose first message is not agent.register, that exceeds the message rate limit, or that registers an agent ID already bound to a different token. The Platform sends it to an orchestrator whose frame is not JSON or has no type, or whose auth message is invalid. It also closes an orchestrator that exceeds the rate limit, sends an invalid cluster_name, or sends five invalid frames within 60 seconds. The Platform dashboard channel sends it when a browser’s first message is not auth.request. The peer channel sends it for a malformed handshake frameServer
4004WS_CLOSE_HEARTBEAT_TIMEOUTHeartbeat timed out (180 seconds)Server
4005WS_CLOSE_PROTOCOL_ERRORProtocol-level error: an unsupported protocol version, or, on the Platform channel, an auth message on an authenticated connection, or any other message before authentication. On the orchestrator peer channel it closes a peer whose handshake breaks the protocol or whose authentication scheme lacks mutual authenticationServer
4006WS_CLOSE_INTERNAL_ERRORUnexpected internal server errorServer
4010WS_CLOSE_AGENT_AUTH_FAILEDAgent token authentication failed. The orchestrator also sends it to every agent connected under a token that is revoked or whose time-to-live expires. At registration, it also sends it when the agent’s labels exceed the token’s label scope, when an ephemeral token is bound to a different agent ID, or when a non-root agent presents a privileged-root token. On the Platform-orchestrator channel the same code is also sent as a safety fallback when every source in a source.register is rejected for missing S3 log storage in a multi-orchestrator poolServer
4011WS_CLOSE_CLUSTER_NAME_CONFLICTReserved constant; no Platform code path emits this today. Platform accepts N connected orchestrators per (org_id, cluster_name) and the dashboard listing dedupes by cluster name—
4020WS_CLOSE_PLAN_LIMITOrganization has reached its plan limit. The Platform sends it when it refuses a new orchestrator connection over the org’s limit, or closes an excess one. A coordinator sends it when it refuses a worker join or drains a connected worker because the org is over its orchestrator ceiling (coordinators and workers combined)Server
4030WS_CLOSE_REBALANCEConnection rebalancing: a newly-joined Platform instance asked peers holding excess connections to shed some; the orchestrator reconnects immediately and load redistributes evenlyServer
4031WS_CLOSE_DISPATCH_ACK_TIMEOUTA dispatched job went unacknowledged past its deadline; the orchestrator requeues the job and disconnects the unresponsive agentServer
4032WS_CLOSE_SUPERSEDED_BY_RECONNECTThe same agent registered on a newer socket, so the older one is a ghost. The orchestrator retires it at registration time, before the OS or the WebSocket ping reaps it, because a half-open socket carries the same agent id and its later close would tear down the live registration. The agent is already connected on the newer socket and does not reconnectServer

Authoritative source: packages/engine/src/ws/close-codes.ts

All protocol messages are validated at runtime using Zod discriminated unions. Each WebSocket layer has direction-specific schemas that prevent parsing messages from the wrong direction.

Orchestrator-Agent layer:

  • Incoming (Orchestrator receives): agentToOrchestratorMessageSchema — parses agent.register, agent.status, job.status, job.reject, job.ack, log.chunk, step.status, job.heartbeat, agent.log, job.concurrency.report, config.ack, cache.upload.request, cache.upload.complete, cache.user.restore.request, cache.user.save.request, cache.user.save.complete, provenance.upload.request, provenance.upload.complete, provenance.upload.defer, artifacts.upload.request, artifacts.upload.complete, artifacts.download.request, event.emit, scaler.claim-credentials, agent.api.request, agent.metrics, auth.request, fleet.bundle.chunk, fleet.bundle.error, step.approval-request
  • Outgoing (Orchestrator sends): orchestratorToAgentMessageSchema — validates job.dispatch, job.cancel, register.ack, job.concurrency.ack, cache.upload.response, cache.user.restore.response, cache.user.save.response, provenance.upload.response, artifacts.upload.response, artifacts.upload.complete.ack, artifacts.download.response, event.emit.response, scaler.claim-credentials.response, agent.api.response, auth.success, auth.failure, fleet.logs.request, step.approval-resolved, rate.limit.warning

Peer-to-peer layer:

  • Bidirectional: peerToPeerMessageSchema / peerFromPeerMessageSchema — parses peer.hello, peer.hello.response, peer.auth.request, peer.auth.response, peer.heartbeat, job.reroute, job.reroute.ack, job.progress, job.progress.ack, peer.job.cancel, raft.vote.request, raft.vote.response, raft.append.entries, peer.log.chunk, peer.cache.upload.request, peer.cache.upload.response, peer.config.reload, peer.config.reload.response, peer.scaler.orphans.request, peer.scaler.orphans.response, peer.scaler.reload.request, peer.scaler.reload.response, peer.forget.request, peer.forget.response, peer.clusterSettings.request, peer.clusterSettings.response, peer.logs.collect.request, peer.logs.collect.chunk, peer.logs.collect.error, peer.leaving, peer.agent-token.revoke, scaler.event

The upstream layers (orchestrator↔KiCI and browser↔KiCI) have their own discriminated unions.

Usage example:

import { agentToOrchestratorMessageSchema } from '@kici-dev/engine';
// Parse and validate an incoming message
const message = agentToOrchestratorMessageSchema.parse(JSON.parse(rawData));
// TypeScript narrows the type based on the 'type' discriminator
switch (message.type) {
case 'agent.register':
handleRegister(message.agentId, message.labels);
break;
case 'job.status':
handleStatus(message.runId, message.jobId, message.status);
break;
}

KiCI propagates a requestId (UUIDv4) through the entire webhook processing pipeline for end-to-end observability. The trace ID enables correlating all log lines, database operations, and protocol messages belonging to a single webhook event.

  1. Origin: Generated at webhook ingestion — either by KiCI (for WS-relayed webhooks) or by the orchestrator’s direct HTTP endpoint (for independent/hybrid mode)
  2. Pipeline propagation: Carried through AsyncLocalStorage (ALS) during synchronous webhook processing: webhook.relay -> processWebhook -> trigger matching -> execution start -> job dispatch
  3. Queue persistence: When no agent is immediately available, the requestId is persisted in the dispatch_queue PostgreSQL table alongside the job payload
  4. Queue restoration: When a queued job is drained (agent becomes available), the requestId is read from the database and restored into a new ALS scope via requestContext.run()
  5. Agent dispatch: Included in the job.dispatch message so the agent can log it during execution
  6. Completion callbacks: Stored in the ExecutionTracker’s in-memory RunState at execution start, then passed back to onExecutionComplete and onStepStatusForward callbacks. The callback handlers in server.ts and standalone.ts restore ALS context via requestContext.run() so that check run updates and upstream forwarding are logged with the correct trace ID.

The app.service field identifies which KiCI tier produced a log line (values: platform, orchestrator, agent). Set process-wide at startup via setServiceName() from @kici-dev/shared. This field is independent of container naming and works in all deployment models (containerized, bare-metal, Firecracker). For forwarded agent logs flowing through orchestrator stdout, the service field is preserved from the agent’s original JSON output.

A dashboard request reaches the orchestrator without a pre-flight check: every orchestrator at protocol 4 understands every dashboard.* request type the Platform sends. When a request fails schema validation, the orchestrator still distinguishes a request type it has never heard of (unsupported_request_type, “upgrade the orchestrator”) from a known type with a malformed body (invalid_payload, a genuine client error). The error response frame carries the structured code and the orchestrator version.