Reference
WebSocket Protocol

WebSocket Protocol

Hankweave exposes a WebSocket API for building custom tools, UIs, and integrations. The built-in TUI uses this same API—anything it can do, you can do.

Who needs this page? Developers building tools that integrate with Hankweave: custom dashboards, CI/CD integrations, monitoring tools, alternative interfaces, or programmatic control. If you're just running or writing hanks, you don't need this—use the CLI instead.

Connection Overview

The Hankweave server runs a WebSocket endpoint (default port 7777) that accepts multiple simultaneous clients. Each client completes a handshake to establish its access level, then sends commands and receives events.

The protocol defines 13 client commands and 27 server events organized into four categories.

WebSocket handshake sequence

Handshake Protocol

Before sending any commands, a client must complete a handshake. This establishes its access level and optionally retrieves event history from the current run.

Handshake Request

Text
interface HandshakeRequest {
  type: "handshake";
  data: {
    mode: "readonly" | "readandwrite";
    sendPreviousEvents?: boolean;  // Request event history (default: false)
  };
}

Handshake Response

Text
interface HandshakeResponse {
  type: "handshake.response";
  data: {
    clientId: string;              // Server-assigned unique ID
    mode: "readonly" | "readandwrite";  // Granted mode
    eventHistory: ServerEvent[];   // Recent events (if requested)
    totalEvents: number;           // Total events in journal
  };
}

Access Modes and Permissions

The mode you request in the handshake determines which commands the server will accept from your client. The server confirms the granted mode in the HandshakeResponse.

ModeCan Send CommandsCan Receive EventsUse Case
readonlyQuery/System commands onlyYesMonitoring, dashboards
readandwriteAll commandsYesFull control, TUI

Read-Only Commands

Clients in readonly mode can send commands that query state but do not modify it. The following commands are always permitted:

  • checkpoint.list
  • history.sync
  • ping
  • ping.broadcast
  • server.shutdown

State-Modifying Commands

All other commands, such as codon.start or rollback.toCheckpoint, modify the execution state and require readandwrite mode. Sending a state-modifying command from a readonly client will result in an error event with the code INSUFFICIENT_PERMISSIONS.

Connecting in TypeScript

Text
const ws = new WebSocket("ws://localhost:7777");
 
ws.onopen = () => {
  // Send handshake
  ws.send(JSON.stringify({
    type: "handshake",
    data: {
      mode: "readandwrite",
      sendPreviousEvents: true
    }
  }));
};
 
ws.onmessage = (event) => {
  const data = JSON.parse(event.data);
 
  if (data.type === "handshake.response") {
    console.log(`Connected as ${data.data.clientId}`);
    console.log(`Received ${data.data.eventHistory.length} historical events`);
    // Ready to send commands
  } else {
    // Handle server events
    handleEvent(data);
  }
};

Client Commands

Commands are JSON messages with a type field and an id for correlation. All commands require a completed handshake—the server rejects any command sent before the handshake response arrives.

Command Structure

Text
interface ClientCommand {
  id: string;           // Unique command ID (for your tracking)
  type: string;         // Command type
  data?: object;        // Command-specific data
}

Codon Control Commands

codon.start

Start a specific codon by its ID.

Text
{
  "id": "cmd-1",
  "type": "codon.start",
  "data": {
    "codonId": "generate-schema",
    "skipPreCommands": false
  }
}
FieldTypeDescription
codonIdstringThe codon ID to start
skipPreCommandsboolean?Skip rig setup (default: false)

codon.next

Start the next codon in the execution plan.

Text
{
  "id": "cmd-2",
  "type": "codon.next"
}

codon.skip

Skip the currently running codon. Creates a skipped checkpoint and moves to the next codon.

Text
{
  "id": "cmd-3",
  "type": "codon.skip"
}

codon.redo

Retry the last completed or failed codon.

Text
{
  "id": "cmd-4",
  "type": "codon.redo"
}

codon.forceStop

Immediately terminate the current codon. Use this when an agent is stuck in a loop or burning tokens on a dead end.

Text
{
  "id": "cmd-5",
  "type": "codon.forceStop",
  "data": {
    "reason": "Agent stuck in loop"
  }
}

Rollback Commands

Rollback commands let you restore the execution directory to a previous checkpoint. This is central to Hankweave's debugging workflow—try something, roll back, try something else.

checkpoint.list

Query available checkpoints for rollback. The server responds with a checkpoint.list event containing the requested information.

Text
{
  "id": "cmd-6",
  "type": "checkpoint.list",
  "data": {
    "runId": "run-abc123"
  }
}

rollback.toCheckpoint

Rollback to a specific git checkpoint SHA.

Text
{
  "id": "cmd-7",
  "type": "rollback.toCheckpoint",
  "data": {
    "checkpointSha": "a1b2c3d4e5f6",
    "autoRestart": true
  }
}
FieldTypeDescription
checkpointShastringGit commit SHA to restore
autoRestartboolean?Auto-start next codon after rollback (default: false)

rollback.toCodon

Rollback to a specific codon's checkpoint.

Text
{
  "id": "cmd-8",
  "type": "rollback.toCodon",
  "data": {
    "codonId": "validate-schema",
    "checkpointType": "rig-setup",
    "autoRestart": true
  }
}
FieldTypeDescription
codonIdstringThe codon to rollback to
checkpointTypestringOne of: "rig-setup", "completed", "error", "skipped", or convenience aliases "start" / "end"
autoRestartboolean?Auto-start after rollback (default: false)

Convenience aliases: "start" resolves to "rig-setup" if available, otherwise "completed" from the previous codon. "end" resolves to "completed", "error", or "skipped" depending on the codon's final state. Use aliases when you don't know the exact checkpoint type—Hankweave will pick the right one.

rollback.toLastSuccess

Rollback to the last successfully completed codon.

Text
{
  "id": "cmd-9",
  "type": "rollback.toLastSuccess",
  "data": {
    "autoRestart": false
  }
}

System Commands

server.shutdown

Gracefully shut down the server.

Text
{
  "id": "cmd-10",
  "type": "server.shutdown",
  "data": {
    "reason": "User requested"
  }
}

ping

Test connection. The server sends a pong event only to the sender.

Text
{
  "id": "cmd-11",
  "type": "ping"
}

ping.broadcast

Test connection with broadcast. All connected clients receive the pong event.

Text
{
  "id": "cmd-12",
  "type": "ping.broadcast"
}

history.sync

Stream the full event history from the journal. Events arrive as history.batch events.

Text
{
  "id": "cmd-13",
  "type": "history.sync"
}

Server Events

Events are JSON messages the server pushes to clients. They form the real-time stream you'll use to build UIs and monitoring tools. Every event has this base structure:

Text
interface ServerEvent {
  id: string;           // Unique event ID
  timestamp: string;    // ISO 8601 timestamp
  type: string;         // Event type
  data: object;         // Event-specific payload
}

Event Categories

Hankweave classifies events into four categories, which determines how they're persisted and routed:

Event categories

CategoryJournaled?RoutingEvents
Server StateYesBroadcast to allExecution lifecycle, state changes
Agentic BackboneYesBroadcast to allAgent actions, tool results, file changes
SentinelYesBroadcast to allSentinel lifecycle and outputs
Connection StateNoUnicast to senderClient-specific responses

Server State Events

These track the execution lifecycle and are persisted to the event journal.

server.idle

Server is waiting for commands.

Text
{
  type: "server.idle",
  data: {
    reason: "startup" | "codon-completed" | "all-codons-completed",
    message: string
  }
}

codon.started

A codon has begun execution.

Text
{
  type: "codon.started",
  data: {
    codonId: string,
    codonName: string,
    codonDescription?: string,
    sessionId: string,
    previousSessionId?: string,
    startTime: string,  // ISO 8601
    promptMetadata?: {
      name?: string,
      description?: string,
      tags?: string[],
      version?: string,
      author?: string
    }
  }
}

codon.completed

A codon has finished (success or failure).

Text
{
  type: "codon.completed",
  data: {
    codonId: string,
    success: boolean,
    cost: number,         // USD
    duration: number,     // milliseconds
    exitStatus: ProcessExit,
    failureReason?: FailureReason
  }
}

ProcessExit types:

Text
type ProcessExit =
  | { type: "success" }
  | { type: "error", code: number }
  | { type: "killed", signal: string };

FailureReason structure:

Text
interface FailureReason {
  type: "timeout" | "rate-limit" | "api-error" | "sentinel-load-failure" | "unknown";
  retriable: boolean;
  message?: string;
  sentinelRefs?: string[];  // For sentinel-load-failure
}

state.snapshot

Current execution state (sent periodically during execution).

Text
{
  type: "state.snapshot",
  data: {
    currentCodon?: CodonExecution,
    completedCodons: CodonExecution[],
    fileTree: FileNode[],
    totalCost: number,
    totalTime: number,
    isRollingBack: boolean
  }
}

token.usage

Token consumption update for a codon.

Text
{
  type: "token.usage",
  data: {
    codonId: string,
    inputTokens: number,
    outputTokens: number,
    cacheCreationTokens: number,
    cacheReadTokens: number,
    totalCost: number,
    modelId?: string,
    modelUsage?: Record<string, {
      inputTokens: number,
      outputTokens: number,
      cacheReadInputTokens?: number,
      cacheCreationInputTokens?: number,
      costUSD: number
    }>
  }
}

info

Informational message.

Text
{
  type: "info",
  data: {
    message: string
  }
}

error

Error occurred during execution.

Text
{
  type: "error",
  data: {
    message: string,
    codon?: string,
    fatal: boolean,
    severity?: "fatal" | "codon" | "operation" | "warning",
    context?: string,
    code?: string
  }
}

checkpoint.list

Response to checkpoint.list command.

Text
{
  type: "checkpoint.list",
  data: {
    runId: string,
    checkpoints: CheckpointInfo[],
    currentBranch: string
  }
}

CheckpointInfo structure:

Text
interface CheckpointInfo {
  codonId: string,
  codonName: string,
  checkpointType: "rig-setup" | "completed" | "error" | "skipped",
  sha: string,
  status: CodonStatus,
  timestamp: string
}

Rollback Events

These events track the progress of a rollback operation.

rollback.started Fired when a rollback process begins.

Text
{
  type: "rollback.started",
  data: {
    fromRun: string,
    fromCodon: string,
    toCodon: string,
    toCheckpoint: string,
    checkpointType: string,
    codonsToProcess: string[]
  }
}

rollback.progress Provides a progress update during a long rollback.

Text
{
  type: "rollback.progress",
  data: {
    currentStep: number,
    totalSteps: number,
    message: string
  }
}

rollback.codonCheckpoint Fired when a specific codon's checkpoint has been successfully applied.

Text
{
  type: "rollback.codonCheckpoint",
  data: {
    codonId: string,
    codonName: string,
    checkpoint: string,
    checkpointType: string,
    message: string
  }
}

rollback.rigCleanup Reports on the cleanup of rig directories associated with rolled-back codons.

Text
{
  type: "rollback.rigCleanup",
  data: {
    codonId: string,
    codonName: string,
    directories: string[],
    status: "started" | "completed" | "failed" | "partial",
    successfulCleanups?: string[],
    failedCleanups?: { directory: string, error: string }[]
  }
}

rollback.completed Fired when the entire rollback operation is complete.

Text
{
  type: "rollback.completed",
  data: {
    fromRun: string,
    toRun: string,
    checkpoint: string,
    codonId: string,
    codonName: string,
    checkpointType: string,
    autoRestart: boolean
  }
}

state.transition

Internal state machine transition (useful for debugging).

Text
{
  type: "state.transition",
  data: {
    transitionType: StateTransitionType,
    runId?: string,
    codonId?: string,
    transition: {
      type: string,
      data: Record<string, unknown>
    },
    resultingState: {
      currentRunId: string | null,
      runCount: number,
      totalCost: number,
      currentRunCost: number
    }
  }
}

StateTransitionType values:

  • RunStarted, RunCompleted, RunFailed, RunCrashed
  • CodonStarted, CodonTransitioned
  • CostsUpdated, CostsIncremented, CodonFinalCostSet
  • AssistantMessageCountUpdated
  • CheckpointCreated, InitialCheckpointSet
  • SentinelStatesUpdated

Agentic Backbone Events

These capture what the agent is actually doing—tool calls, file changes, and outputs. If you're building a live view of agent activity, these are the events you'll consume most.

assistant.action

Agent is thinking, sending a message, or using a tool.

Text
{
  type: "assistant.action",
  data: {
    codonId: string,
    action: "thinking" | "message" | "tool_use",
    content: string,
    toolName?: string,
    toolInput?: Record<string, unknown>
  }
}

tool.result

Tool execution completed.

Text
{
  type: "tool.result",
  data: {
    codonId: string,
    toolUseId: string,
    toolName: string,
    result: string,
    truncated: boolean,
    originalLength: number,
    executionTimeMs: number,
    isError: boolean
  }
}

file.updated

File was created, modified, or deleted.

Text
{
  type: "file.updated",
  data: {
    path: string,
    filename: string,
    content: string,
    action: "created" | "modified" | "deleted"
  }
}

filetree.updated

File tree structure changed.

Text
{
  type: "filetree.updated",
  data: {
    tree: FileNode[]
  }
}

FileNode structure:

Text
interface FileNode {
  name: string;
  path: string;
  isDirectory: boolean;
  children: FileNode[];
  lastModified?: string;
}

Sentinel Events

These track sentinel lifecycle and outputs. Sentinels run in parallel to the main agent, so their events interleave with backbone events.

sentinel.loaded

Sentinel started monitoring.

Text
{
  type: "sentinel.loaded",
  data: {
    sentinelId: string,
    codonId: string,
    model: string,
    triggerType: "event" | "sequence",
    executionStrategy: "immediate" | "debounce" | "count" | "timeWindow",
    conversational: boolean,
    source: "file" | "inline",
    sourcePath?: string
  }
}

sentinel.unloaded

Sentinel stopped.

Text
{
  type: "sentinel.unloaded",
  data: {
    sentinelId: string,
    codonId: string,
    reason: "codon-complete" | "fatal-error" | "consecutive-failures" | "shutdown",
    errorType?: "template" | "configuration" | "corruption" | "resource",
    finalCost: number,
    llmCallCount: number
  }
}

sentinel.output

Sentinel produced output from an LLM call.

Text
{
  type: "sentinel.output",
  data: {
    sentinelId: string,
    codonId: string,
    triggerNumber: number,
    outputType: "text" | "structured",
    content: string | Record<string, unknown>,
    cost: number,
    tokens: {
      input: number,
      output: number
    },
    eventCount: number
  }
}

sentinel.error

Sentinel encountered an error.

Text
{
  type: "sentinel.error",
  data: {
    sentinelId: string,
    codonId: string,
    errorType: "llm-call-failed" | "template-render-failed" | "file-write-failed",
    message: string,
    retriable: boolean,
    consecutiveFailureCount: number
  }
}

sentinel.triggered

Sentinel trigger fired (verbose, usually disabled).

Text
{
  type: "sentinel.triggered",
  data: {
    sentinelId: string,
    codonId: string,
    triggerNumber: number,
    strategy: "immediate" | "debounce" | "count" | "timeWindow",
    eventCount: number,
    queueSize: number
  }
}

Connection State Events

These are not journaled and are sent only to specific clients. They're client-specific responses that don't represent shared execution state.

server.ready

Sent immediately after successful handshake.

Text
{
  type: "server.ready",
  data: {
    serverVersion: string,
    executionPath: string,
    dataPath: string
  }
}

pong

Response to ping or ping.broadcast.

Text
{
  type: "pong",
  data: {
    message: "pong",
    timestamp: string,
    clientId?: string  // Only in ping.broadcast responses
  }
}

history.batch

Batch of historical events (response to history.sync).

Text
{
  type: "history.batch",
  data: {
    events: ServerEvent[],
    hasMore: boolean
  }
}

incomplete.codon

A codon didn't finish cleanly (deprecated).

Text
{
  type: "incomplete.codon",
  data: {
    codonId: string,
    codonName: string,
    message: string
  }
}

History Synchronization

Clients that need complete event history—for instance, a UI that connects mid-run—can request a full history sync.

  1. Connect with sendPreviousEvents: true in your handshake to get recent events from the current session.
  2. Send a history.sync command to stream the complete journal from the beginning of the run.
  3. Events arrive in history.batch messages. The hasMore field will be true until the final batch.
Text
// Request full history after connecting
ws.send(JSON.stringify({
  id: "sync-1",
  type: "history.sync"
}));
 
// Handle batches
ws.onmessage = (event) => {
  const data = JSON.parse(event.data);
  if (data.type === "history.batch") {
    processEvents(data.data.events);
    if (!data.data.hasMore) {
      console.log("History sync complete");
    }
  }
};

Error Handling

Errors are reported via the error event, which includes a severity level.

SeverityMeaningAction
fatalServer must stopReconnect after fix
codonCodon failedCheck failureReason, consider codon.redo
operationA specific command failedCheck message, retry if appropriate
warningNon-blocking issueLog and continue

Common error codes sent in the error event's data payload:

CodeMeaning
HANDSHAKE_REQUIREDCommand sent before handshake completed.
INSUFFICIENT_PERMISSIONSreadonly client sent state-modifying command.
ROLLBACK_IN_PROGRESSState-modifying command sent during rollback.

Building a Client

Here's a complete example of a monitoring client that connects in readonly mode and tracks codon progress:

Text
import { WebSocket } from 'ws'; // Requires `ws` package
 
// Assuming ServerEvent is defined based on the documentation
interface ServerEvent {
  type: string;
  data: any;
}
 
class HankweaveMonitor {
  private ws: WebSocket | null = null;
  private events: ServerEvent[] = [];
 
  connect(port: number = 7777): Promise<void> {
    return new Promise((resolve, reject) => {
      this.ws = new WebSocket(`ws://localhost:${port}`);
 
      this.ws.on('open', () => {
        // Handshake as readonly (we're just watching)
        this.ws!.send(JSON.stringify({
          type: "handshake",
          data: {
            mode: "readonly",
            sendPreviousEvents: true
          }
        }));
      });
 
      this.ws.on('message', (eventData) => {
        const data = JSON.parse(eventData.toString());
 
        if (data.type === "handshake.response") {
          console.log(`Connected: ${data.data.clientId}`);
          if (data.data.eventHistory) {
              this.events = data.data.eventHistory;
          }
          resolve();
          return;
        }
 
        this.handleEvent(data);
      });
 
      this.ws.on('error', reject);
    });
  }
 
  private handleEvent(event: ServerEvent): void {
    this.events.push(event);
 
    switch (event.type) {
      case "codon.started":
        console.log(`▶ Started: ${event.data.codonName}`);
        break;
 
      case "codon.completed":
        const status = event.data.success ? "✓" : "✗";
        console.log(`${status} Completed: ${event.data.codonId} ($${event.data.cost.toFixed(4)})`);
        break;
 
      case "token.usage":
        console.log(`  Tokens: ${event.data.inputTokens} in / ${event.data.outputTokens} out`);
        break;
 
      case "error":
        if (event.data.fatal) {
          console.error(`FATAL: ${event.data.message}`);
        } else {
          console.warn(`Warning: ${event.data.message}`);
        }
        break;
 
      case "sentinel.output":
        console.log(`  [${event.data.sentinelId}] ${JSON.stringify(event.data.content)}`);
        break;
    }
  }
 
  getEvents(): ServerEvent[] {
    return this.events;
  }
 
  getTotalCost(): number {
    return this.events
      .filter((e) => e.type === "codon.completed" && e.data.cost)
      .reduce((sum, e) => sum + e.data.cost, 0);
  }
}

Related Pages

Next Steps

With this protocol, you can build anything from a simple cost-monitoring script to a full custom UI. Start with the complete TypeScript example above, then adapt it for your specific needs. Common patterns include monitoring dashboards (readonly mode), custom TUIs (readandwrite), CI/CD integrations (handling events and using rollback commands), and real-time agent viewers (consuming agentic backbone and sentinel events).