Skip to content
Crystal CodeDocs
ENEnglish

Session and turn

One user message is one turn:

  1. Snapshot the transcript and current tool definitions.
  2. Stream one chat request. Publish each delta as an event as it arrives. If that round fails with a retryable provider error (HTTP 429, 404, 408, 5xx, timeout, network, or an incomplete stream), wait with exponential backoff and repeat the same round. The retry policy in config.json sets the limit and timing (defaults: five retries, 2s base, factor 2, 25% jitter, 30s cap when Retry-After is absent); Retry-After or retry-after-ms is honored when present. The engine publishes RetryScheduled; the terminal progress row shows Retrying In Ns (Attempt K) and counts the remaining wait down, and the transcript prints retrying model request plus the operator message. Live stream text from the failed attempt is discarded. HTTP 401, 403, quota, invalid_prompt, and context overflow are not retried. A connection that closes while the request body is still being written is retried once, so a stale connection can be replaced, and then reported with the transport cause instead of using the rest of the budget. User cancel still interrupts immediately.
  3. Select candidate zero.
  4. Stop before tools when the finish reason is not a normal stop or a tool request. length keeps the assistant text, drops tool calls, and stops as output_truncated. A content filter keeps the text and stops as content_filtered. Any other reason stops as failed and publishes Model stopped with finish reason '<value>'. These stops do not compact.
  5. If the candidate has tool calls, execute the full batch through ToolExecutor, or the multimodal bridge for an image-capable turn (approval runs first in either case). A batch that does not fit the remaining tool-call budget stops as tool_call_limit_reached, keeps the assistant text and reasoning, and leaves those calls out of the transcript.
  6. Append exact ToolResult values.
  7. Repeat until the candidate has no tool calls, a configured limit stops the turn, or the user cancels. Before each model round, compact if the estimated transcript or the last model-round usage is over budget. One failed compact while still over budget stops the turn (context_overflow). A provider failure that survives retry stops the turn as failed: the error is published, the progress row clears, and the transcript, including that user message, is saved.

Text and image-capable turns share executionBudget from config.json. The default per-turn limits are 1024 model calls, 8192 tool calls, and 7 days. The setting may be "unlimited" for all three, or an object with optional maximumModelCalls, maximumToolCalls, and maximumDurationSeconds fields. Omitted fields keep their defaults; null removes that field’s limit. Model calls and duration must be positive when finite. Tool calls may be zero to prevent tool execution. Duration must fit the runtime timer. A turn with unlimited limits still ends on model completion, user cancellation, or context compaction exhaustion. This host owns the live turn budget; it follows the nullable-limit semantics of Crystal’s AgentRunLimits without using Agent for streaming UI turns.

Built-in bash has its own per-command timer, bashTimeoutSeconds in config.json. Omitting it keeps 600 seconds. null or "unlimited" disables that timer. A positive integer up to the runtime timer limit replaces it. User cancellation and the turn budget still stop the command. External tool sets keep their own timeoutSeconds, which defaults to 120 seconds and does not follow bashTimeoutSeconds. 7. After a completed turn, consider compaction from that last round’s reported usage and the estimated model context. /compact (alias /summarize) summarizes earlier turns immediately, without waiting for that threshold. Compaction runs inside an engine call the front end awaits; the front end keeps its own surface live meanwhile (the terminal host pumps the frame so the spinner, resize, and composer stay live).

The session keeps two transcripts. The archive appends user text, assistant text, reasoning, tool calls, and tool results as they are committed, and compaction does not change it. The model context is the list sent to the model. Compaction replaces that list with the live system prompt, a summary, and a recent tail. Usage, approval review, and later model calls read the model context. Resume, fork replay, and export read the archive. /stats tool share reads the archive, and a session file with no archive counts tools from its saved items. A session file written before the archive existed loads its saved items as the archive. Images stay when either transcript still mentions them. /clear drops both.

The composer stays open while a turn runs. Enter with text enqueues a follow-up (FIFO). Queued items stay in a panel above the composer until they are sent. The queue is sent when the current tool batch finishes or when the turn (thinking or conversation) ends. Empty Enter while a follow-up is queued interrupts the turn, or compaction if that is still running, so the queue is sent now. Empty Enter with nothing queued does not stop the turn. /quit, /clear, /resume, and /fork stop a busy turn and then run, including a command submitted while other session work is still on the stack. /clear, /resume, and /fork stay in the queue and run in the order typed. /clear, /resume, and /fork drop queued follow-up text; the panel updates and does not keep it. During compaction those three except /quit are refused, as are other commands that change the session, workspace, or model. /status, /help, /todos, and other commands that only report state or adjust the display run immediately. /quit cancels compaction and exits. Ctrl+C interrupts and does not drop queued text. At an idle prompt, Ctrl+C clears the composer. Two Ctrl+C presses on an empty composer exit.

/btw (alias /side) asks one side question from the archive and the newest 20 side exchanges in this process. The request uses the work model, the current system message, an empty tool list, and its own cancellation. When that request does not fit the window, the host compacts a copy and does not write the summary back. If the copy still does not fit, the side question fails. Text still streaming in the current round is not included. A tool call is not executed. Text that arrived with it is kept; a tool call with no text fails, and the question stays in the panel so it can be asked again. The question, the answer, and the token usage stay out of the queue, the transcript, the usage ledger, compaction, and the saved session. The terminal draws the answer in a panel above the composer and leaves the status bar, transcript, and progress row in place. Until reasoning starts, that panel shows Waiting for the model with the same one-cell spinner as the progress row. Once reasoning starts and before answer text, it shows Thinking with that spinner, and the frame keeps advancing while the panel is open. Esc, Enter, or Space closes the panel. Left and Right step through earlier answers. x clears the in-memory thread. While that panel is open, Ctrl+C cancels an in-flight side question, closes the panel, and leaves the main turn running. /clear, /resume, and /fork drop the thread. crystal run rejects the command with every other slash command.