Skip to content

Transcript streaming

octomate_protocol.stream

The transcript stream's wire protocol — what a client-side tail exchanges with the server's stream endpoint when a native session's transcript lives on another machine.

The payload is raw transcript lines plus the byte offsets they occupy in the client's own file. The client never parses what it ships — every translation stays server-side, so a transcript format change is a server-only fix — which also keeps this protocol agent-neutral: the same frames carry a Claude session file or a Codex rollout, and which tree a stream feeds is the mounting endpoint's business.

The server is the only cursor authority. A connect opens with hello/welcome: the client claims a session, the server answers where each of its files resumes (recomputed from the committed runs), and the client holds no durable state — a crash anywhere costs a re-stream, never a loss. Lines then flow contiguously per file; finalize (server → client, relaying SessionEnd from the hook pipe) and eof (client → server, "drained, commit the trailing turns") close a session out.

STREAM_PROTOCOL module-attribute

STREAM_PROTOCOL = 2

SESSION_FILE module-attribute

SESSION_FILE = ''

StreamClientMessage module-attribute

StreamClientMessage = Annotated[
    StreamHello
    | StreamLine
    | StreamSnapshotStart
    | StreamEof,
    Field(discriminator="type"),
]

StreamServerMessage module-attribute

StreamServerMessage = Annotated[
    StreamWelcome | StreamFinalize | StreamSnapshotStored,
    Field(discriminator="type"),
]

client_message_adapter module-attribute

client_message_adapter: TypeAdapter[StreamClientMessage] = (
    TypeAdapter(StreamClientMessage)
)

server_message_adapter module-attribute

server_message_adapter: TypeAdapter[StreamServerMessage] = (
    TypeAdapter(StreamServerMessage)
)

StreamHello pydantic-model

Bases: BaseModel

The client's opening claim: which session it tails, where the transcript lives on the client's machine (a label in the client's namespace — the server never opens it), and the directory the session runs in, which files the session's thread under its project exactly as the http hooks' cwd does.

Fields:

Source code in protocol/octomate_protocol/stream.py
class StreamHello(BaseModel):
    """The client's opening claim: which session it tails, where the transcript lives
    on the client's machine (a label in the *client's* namespace — the server never
    opens it), and the directory the session runs in, which files the session's thread
    under its project exactly as the http hooks' `cwd` does."""

    type: Literal["hello"] = "hello"
    protocol: int
    session_id: str
    transcript_path: str
    cwd: str = ""
    # The shipping client's own version (the octomate-cli package), logged at attach for
    # skew diagnosis. The Claude Code version is not carried here: it rides every
    # transcript line, which the server parses anyway.
    client_version: str = ""

type pydantic-field

type: Literal['hello'] = 'hello'

protocol pydantic-field

protocol: int

session_id pydantic-field

session_id: str

transcript_path pydantic-field

transcript_path: str

cwd pydantic-field

cwd: str = ''

client_version pydantic-field

client_version: str = ''

StreamLine pydantic-model

Bases: BaseModel

One complete transcript line and the byte range it occupies in its file.

start/end are the client file's own offsets, end past the newline the line was framed on — the same range the assembled run's start_offset/end_offset records. Lines arrive contiguously per file, blank ones included (skipping one would read as a gap); the server closes on a gap and the reconnect re-asks where to resume.

Fields:

Source code in protocol/octomate_protocol/stream.py
class StreamLine(BaseModel):
    """One complete transcript line and the byte range it occupies in its file.

    `start`/`end` are the client file's own offsets, `end` past the newline the line
    was framed on — the same range the assembled run's `start_offset`/`end_offset`
    records. Lines arrive contiguously per file, blank ones included (skipping one
    would read as a gap); the server closes on a gap and the reconnect re-asks where
    to resume.
    """

    type: Literal["line"] = "line"
    # None: the session transcript; else the client's key for the subagent file this
    # line is from — an agent id for Claude, a sibling rollout's basename for Codex.
    agent_id: str | None = None
    start: int
    end: int
    line: str

type pydantic-field

type: Literal['line'] = 'line'

agent_id pydantic-field

agent_id: str | None = None

start pydantic-field

start: int

end pydantic-field

end: int

line pydantic-field

line: str

StreamEof pydantic-model

Bases: BaseModel

The client drained its files to EOF and is done — answering a finalize, or on its own idle give-up. The server commits the trailing turns on this; a connection that drops without it leaves them uncommitted for the next connect to re-stream.

Fields:

  • type (Literal['eof'])
Source code in protocol/octomate_protocol/stream.py
class StreamEof(BaseModel):
    """The client drained its files to EOF and is done — answering a `finalize`, or on
    its own idle give-up. The server commits the trailing turns on this; a connection
    that drops without it leaves them uncommitted for the next connect to re-stream."""

    type: Literal["eof"] = "eof"

type pydantic-field

type: Literal['eof'] = 'eof'

StreamSnapshotCursor pydantic-model

Bases: BaseModel

The durable transcript prefix the client must verify before appending.

Fields:

Source code in protocol/octomate_protocol/stream.py
class StreamSnapshotCursor(BaseModel):
    """The durable transcript prefix the client must verify before appending."""

    offset: int = Field(ge=0, description="Number of transcript bytes already stored.")
    sha256: str = Field(
        pattern=r"^[0-9a-f]{64}$", description="SHA-256 of that prefix."
    )

offset pydantic-field

offset: int

Number of transcript bytes already stored.

sha256 pydantic-field

sha256: str

SHA-256 of that prefix.

StreamWelcome pydantic-model

Bases: BaseModel

The server's answer to hello: where each file resumes, keyed by SESSION_FILE or agent id. A file absent from the map starts at 0.

Fields:

Source code in protocol/octomate_protocol/stream.py
class StreamWelcome(BaseModel):
    """The server's answer to hello: where each file resumes, keyed by `SESSION_FILE`
    or agent id. A file absent from the map starts at 0."""

    type: Literal["welcome"] = "welcome"
    offsets: dict[str, int]
    transcript: StreamSnapshotCursor | None = Field(
        default=None,
        description="Continuously upload bytes after this verified prefix; None disables upload.",
    )

type pydantic-field

type: Literal['welcome'] = 'welcome'

offsets pydantic-field

offsets: dict[str, int]

transcript pydantic-field

transcript: StreamSnapshotCursor | None = None

Continuously upload bytes after this verified prefix; None disables upload.

StreamSnapshotStart pydantic-model

Bases: BaseModel

Client announces the next transcript range before sending its bytes.

Fields:

Source code in protocol/octomate_protocol/stream.py
class StreamSnapshotStart(BaseModel):
    """Client announces the next transcript range before sending its bytes."""

    type: Literal["snapshot_start"] = "snapshot_start"
    transfer_id: UUID
    start: int = Field(
        ge=0, description="Exclusive end of the previously stored prefix."
    )
    end: int = Field(
        gt=0, description="Exclusive byte offset at a complete-line boundary."
    )

type pydantic-field

type: Literal['snapshot_start'] = 'snapshot_start'

transfer_id pydantic-field

transfer_id: UUID

start pydantic-field

start: int

Exclusive end of the previously stored prefix.

end pydantic-field

end: int

Exclusive byte offset at a complete-line boundary.

StreamSnapshotStored pydantic-model

Bases: BaseModel

Server acknowledgment after the uploaded snapshot is durably stored.

Fields:

Source code in protocol/octomate_protocol/stream.py
class StreamSnapshotStored(BaseModel):
    """Server acknowledgment after the uploaded snapshot is durably stored."""

    type: Literal["snapshot_stored"] = "snapshot_stored"
    transfer_id: UUID

type pydantic-field

type: Literal['snapshot_stored'] = 'snapshot_stored'

transfer_id pydantic-field

transfer_id: UUID

StreamFinalize pydantic-model

Bases: BaseModel

Server → client: the session ended (SessionEnd arrived on the hook pipe) — drain the remaining bytes, answer with eof, and exit.

A snapshot starts with snapshot_start, followed by binary messages totaling end - start bytes. The client waits for snapshot_stored before sending eof. Each batch's transcript lines precede its binary messages; they never interleave. Batches may upload throughout a turn, before finalization.

Fields:

  • type (Literal['finalize'])
Source code in protocol/octomate_protocol/stream.py
class StreamFinalize(BaseModel):
    """Server → client: the session ended (`SessionEnd` arrived on the hook pipe) —
    drain the remaining bytes, answer with `eof`, and exit.

    A snapshot starts with `snapshot_start`, followed by binary messages totaling
    `end - start` bytes. The client waits for `snapshot_stored` before sending `eof`.
    Each batch's transcript lines precede its binary messages; they never interleave.
    Batches may upload throughout a turn, before finalization.
    """

    type: Literal["finalize"] = "finalize"

type pydantic-field

type: Literal['finalize'] = 'finalize'