Skip to main content
Claw Code tracks conversation state in a QueryEnginePort instance. Each engine holds a unique session_id, a list of mutable_messages, and a TranscriptStore. Sessions can be persisted to disk and reloaded later.

Session anatomy

FieldTypeDescription
session_idstr (UUID hex)Auto-generated unique identifier
mutable_messageslist[str]Ordered list of prompts submitted this session
transcript_storeTranscriptStoreAppend-only log with a flushed flag
total_usageUsageSummaryCumulative input_tokens / output_tokens
permission_denialslist[PermissionDenial]Accumulated tool-denial records

Flushing a transcript

flush-transcript submits a prompt, persists the session to .port_sessions/<session_id>.json, and reports the file path plus the flush state.
python3 -m src.main flush-transcript "<prompt>"
Example output:
.port_sessions/a3f9d2e1b0c4...json
flushed=True
1

Submit the message

engine.submit_message(prompt) appends the prompt to mutable_messages and transcript_store.entries, then computes projected token usage.
2

Persist the session

engine.persist_session() calls flush_transcript() internally — setting transcript_store.flushed = True — then writes StoredSession JSON under .port_sessions/.
3

Print path and flush state

The CLI prints the file path, followed by flushed=True.

TranscriptStore.flush()

flush() simply marks the store as flushed — it does not clear the entries. Entries are retained for replay or compaction.
store = TranscriptStore()
store.append("hello")
store.flush()
assert store.flushed is True
assert store.entries == ["hello"]  # entries preserved
Any subsequent append() call resets flushed to False.

Loading a saved session

python3 -m src.main load-session <session_id>
Output format:
<session_id>
<N> messages
in=<input_tokens> out=<output_tokens>
Example:
python3 -m src.main load-session a3f9d2e1b0c4
a3f9d2e1b0c4...
3 messages
in=142 out=88
Sessions are stored under .port_sessions/ relative to the working directory. The file is named <session_id>.json.

Python API

Creating an engine

from src.query_engine import QueryEnginePort

# Fresh engine tied to the current workspace manifest — run from the repo root
engine = QueryEnginePort.from_workspace()

Restoring a saved session

engine = QueryEnginePort.from_saved_session("a3f9d2e1b0c4")
from_saved_session() reconstructs the engine with:
  • the stored session_id
  • mutable_messages populated from saved messages
  • total_usage restored from saved token counts
  • transcript_store.flushed = True (marking the restored transcript as already flushed)

Persisting a session

path = engine.persist_session()
# Returns the file path as a string, e.g. ".port_sessions/a3f9....json"
persist_session() always calls flush_transcript() before writing, ensuring flushed=True on return.

Turn limits and budget

Max turns

Default 8. When len(mutable_messages) >= max_turns, submit_message() returns immediately with stop_reason='max_turns_reached' without appending the new prompt.

Max budget tokens

Default 2000. If the projected cumulative token count exceeds this after a turn, stop_reason is set to 'max_budget_reached' (the message is still recorded).
These defaults live in QueryEngineConfig:
@dataclass(frozen=True)
class QueryEngineConfig:
    max_turns: int = 8
    max_budget_tokens: int = 2000
    compact_after_turns: int = 12
    structured_output: bool = False
    structured_retry_limit: int = 2

Message compaction

After each turn, compact_messages_if_needed() checks whether mutable_messages exceeds compact_after_turns (default 12). If so, only the most recent compact_after_turns messages are kept. The same window is applied to transcript_store.entries.
def compact_messages_if_needed(self) -> None:
    if len(self.mutable_messages) > self.config.compact_after_turns:
        self.mutable_messages[:] = self.mutable_messages[-self.config.compact_after_turns:]
    self.transcript_store.compact(self.config.compact_after_turns)
Compaction happens in-place on the live mutable_messages list. The persisted session file always reflects the state at the time persist_session() was called.

Stop reasons

stop_reasonMeaning
completedTurn processed successfully within all limits
max_turns_reachedlen(mutable_messages) >= max_turns at time of call; prompt not recorded
max_budget_reachedProjected token count exceeded max_budget_tokens; turn was still recorded

Streaming events

stream_submit_message() yields a sequence of typed event dicts before and after the underlying submit_message() call:
Event typeWhen emitted
message_startAlways first; carries session_id and prompt
command_matchWhen matched commands are non-empty
tool_matchWhen matched tools are non-empty
permission_denialWhen denied tools are non-empty
message_deltaCarries the formatted output text
message_stopAlways last; carries usage, stop_reason, and transcript_size

Build docs developers (and LLMs) love