trail-boss/docs/notes/normalized-event-contract.md
jedarden 4ff41bc74b docs(normalized-event-contract): add comprehensive harness-agnostic adapter contract specification
Document the POST /event/normalized endpoint contract with:
- Complete endpoint specification (URL, method, request/response)
- All 4 event types with field descriptions and examples
- Derivation guide for sessionId, paneId, cwd, transcriptPath from harness context
- Implementation example for adapter authors
- Error handling and versioning guidance

This enables future adapter authors to integrate any coding harness
without reading the daemon source code.
2026-07-02 09:38:57 -04:00

9.5 KiB

Normalized Event Contract

Overview

The Trail Boss daemon accepts normalized events via the POST /event/normalized endpoint. This contract is harness-agnostic — any adapter can emit these events to integrate a coding harness with Trail Boss, regardless of the underlying AI provider or session model.

Endpoint

POST /event/normalized

URL: http://127.0.0.1:4000/event/normalized

Content-Type: application/json

Authentication: None (localhost-only daemon)

Request Body: A JSON object representing one of the event types below. All events include a type discriminator field.

Response (Success):

HTTP/1.1 200 OK
Content-Type: application/json

{
  "ok": true
}

Error Responses:

Status Body Description
400 {"error": "Invalid JSON"} Request body is not valid JSON
400 {"error": "Invalid or missing event type"} Missing or invalid type field
500 {"error": "Internal server error"} Server-side processing error

Event Types

All events share a common structure with a type discriminator. The daemon dispatches based on this field.

StuckEvent

Emitted when a session becomes stuck (blocked on user input).

Type Discriminator: "stuck"

Fields:

Field Type Required Description
type string Yes Must be "stuck"
sessionId string Yes Unique session identifier (harness-specific)
paneId string Yes tmux pane identifier (e.g., %3)
cwd string Yes Current working directory of the session
transcriptPath string Yes Path to the session transcript file
reason string Yes Either "stopped" (hook event) or "permission" (tool blocked)
message string Yes Human-readable context: last assistant message (stopped) or tool operation (permission)
timestamp number Yes Unix timestamp in milliseconds

Example (stopped):

{
  "type": "stuck",
  "sessionId": "sess_01a2b3c4d5e6f7g8",
  "paneId": "%12",
  "cwd": "/home/coding/project",
  "transcriptPath": "/home/coding/.claude/sessions/sess_01a2b3c4d5e6f7g8/transcript.jsonl",
  "reason": "stopped",
  "message": "I'm waiting for you to review these changes before proceeding.",
  "timestamp": 1719987600000
}

Example (permission):

{
  "type": "stuck",
  "sessionId": "sess_01a2b3c4d5e6f7g8",
  "paneId": "%12",
  "cwd": "/home/coding/project",
  "transcriptPath": "/home/coding/.claude/sessions/sess_01a2b3c4d5e6f7g8/transcript.jsonl",
  "reason": "permission",
  "message": "[Bash] {\"command\":\"rm -rf /tmp/cache\"}",
  "timestamp": 1719987600000
}

UnstuckEvent

Emitted when a session becomes unstuck (user provided input or the session resumed).

Type Discriminator: "unstuck"

Fields:

Field Type Required Description
type string Yes Must be "unstuck"
sessionId string Yes Unique session identifier (must match a previously stuck session)
timestamp number Yes Unix timestamp in milliseconds

Example:

{
  "type": "unstuck",
  "sessionId": "sess_01a2b3c4d5e6f7g8",
  "timestamp": 1719987660000
}

Note: The daemon automatically looks up the paneId from the stored session state for cleanup.

SessionRegistered

Emitted when a new session starts. Registers the session metadata with the daemon.

Type Discriminator: "registered"

Fields:

Field Type Required Description
type string Yes Must be "registered"
sessionId string Yes Unique session identifier (harness-specific)
paneId string Yes tmux pane identifier (e.g., %3)
cwd string Yes Current working directory of the session
transcriptPath string Yes Path to the session transcript file
timestamp number Yes Unix timestamp in milliseconds

Example:

{
  "type": "registered",
  "sessionId": "sess_01a2b3c4d5e6f7g8",
  "paneId": "%12",
  "cwd": "/home/coding/project",
  "transcriptPath": "/home/coding/.claude/sessions/sess_01a2b3c4d5e6f7g8/transcript.jsonl",
  "timestamp": 1719987600000
}

SessionEnded

Emitted when a session terminates. Cleans up session state from the daemon.

Type Discriminator: "ended"

Fields:

Field Type Required Description
type string Yes Must be "ended"
sessionId string Yes Unique session identifier
timestamp number Yes Unix timestamp in milliseconds

Example:

{
  "type": "ended",
  "sessionId": "sess_01a2b3c4d5e6f7g8",
  "timestamp": 1719988000000
}

Deriving Fields from Harness Context

This section explains how to derive the normalized event fields from common harness contexts.

sessionId

The session identifier is harness-specific. Common sources:

Harness Session ID Source
Claude Code Transcript filename or internal session UUID
Aider Thread ID or workspace session key
Cursor VS Code workspace URI + session UUID
Custom Generate a UUID or use thread/conversation ID

Key properties:

  • Must be unique per session (across restarts of the same session)
  • Should be stable for the lifetime of the session
  • Can be a UUID, hash, or harness-provided ID

paneId

The tmux pane identifier in the form %<number>. Derive from:

Method 1: Environment variable (recommended)

# The TMUX_PANE env var is set automatically in tmux sessions
echo $TMUX_PANE  # Outputs: %12

Method 2: tmux command

# Get the pane ID of the current tmux session
tmux display -p '#{pane_id}'  # Outputs: %12

Method 3: Parsing tmux socket path

# Extract from TMUX env var
tmux show-env | grep TMUX_PANE

cwd

The current working directory of the session.

Method 1: Direct API (if harness provides it)

// Claude Code example
const cwd = session.cwd;

Method 2: Environment variable

# In the session's shell context
pwd

Method 3: Infer from transcript path

# If transcript path is /home/user/.claude/sessions/.../transcript.jsonl
# The session may have been invoked from the project root

transcriptPath

The path to the session transcript file. This is used by the reconcile loop for recovery.

Common patterns:

Harness Transcript Path Pattern
Claude Code $HOME/.claude/sessions/<sessionId>/transcript.jsonl
Aider Project directory .aider.transcript.md or .aider.cache/<thread>/transcript
Cursor $HOME/.cursor/sessions/<sessionId>/transcript.jsonl
Custom Adapter-defined location

Claude Code example:

// From hook event payload
const transcriptPath = hookEvent.transcript_path;
// "/home/coding/.claude/sessions/sess_01a2b3c4d5e6f7g8/transcript.jsonl"

timestamp

Unix timestamp in milliseconds. Use the current time when emitting the event.

const timestamp = Date.now();

Adapter Implementation Example

Here's a minimal adapter skeleton for a hypothetical harness:

interface NormalizedEvent {
  type: "stuck" | "unstuck" | "registered" | "ended";
}

async function emitEvent(event: NormalizedEvent): Promise<void> {
  const response = await fetch("http://127.0.0.1:4000/event/normalized", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(event),
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(`Trail Boss error: ${error.error}`);
  }
}

// When session starts
await emitEvent({
  type: "registered",
  sessionId: getSessionId(),
  paneId: process.env.TMUX_PANE,
  cwd: process.cwd(),
  transcriptPath: getTranscriptPath(),
  timestamp: Date.now(),
});

// When session blocks on permission
await emitEvent({
  type: "stuck",
  sessionId: getSessionId(),
  paneId: process.env.TMUX_PANE,
  cwd: process.cwd(),
  transcriptPath: getTranscriptPath(),
  reason: "permission",
  message: `[${toolName}] ${JSON.stringify(toolInput)}`,
  timestamp: Date.now(),
});

// When user provides input
await emitEvent({
  type: "unstuck",
  sessionId: getSessionId(),
  timestamp: Date.now(),
});

// When session ends
await emitEvent({
  type: "ended",
  sessionId: getSessionId(),
  timestamp: Date.now(),
});

Validation

The daemon validates events at runtime:

  1. JSON validity: Request body must be parseable JSON
  2. Type discriminator: The type field must be one of: stuck, unstuck, registered, ended
  3. Required fields: Each event type must include its required fields (see table above)

Additional validation (field formats, sessionId existence) may be added in future versions.

Error Handling

Adapters should handle errors gracefully:

Scenario Recommended Handling
Daemon not running Log warning, queue event locally, or retry with backoff
Invalid event type Log error, fix adapter code
Missing required field Log error, fix adapter code
Network error Retry with exponential backoff, batch pending events

Versioning

This contract corresponds to Trail Boss daemon v1.0. Future versions may:

  • Add optional fields to existing events (backward compatible)
  • Add new event types (backward compatible)
  • Introduce a version header for breaking changes

For breaking changes, the daemon will version the endpoint (e.g., /event/normalized/v2).