Skip to content

Sessions, concurrency & Pi-native tuning

This page covers how the runtime keeps provider sessions warm per conversation, how it bounds in-flight work with admission and execution limits, and the Pi-native transport knobs for transport selection, retries, and durable on-disk sessions. Every option here is config coverage with a matching MONO_AGENT_* env var unless noted.

Mono-agent uses “session” for five related but different boundaries:

MeaningWhat owns itWhat it controlsWhat resets it
runtime.session config blockAgent config / envWhether turns try to reuse a warm provider session and how long idle warmth lastsChanging config, setting mode: "per-message", or disabling resume support
Provider sessionRuntime backend / provider bridgeWarm runtime continuity: provider-side context, provider session id, busy state, and idle evictionIdle eviction, stale/busy resume retry, provider session rotation, unsafe or unsettled cancelled/failed turns, harness disposal, or process restart when only in-memory
Canonical logical-session historyDurable message-history files plus the separate tool-history/tool-lifecycles.sqlite sidecarCold context replay and retained, searchable managed-tool invocation/result evidence; settled failed/cancelled non-isolated turns have bounded message accounts, while tool records may still outlive isolated, never-started, or hard-crashed runs with no accountA conversation reset clears every message and tool-history bucket visible in that logical session; mono-agent restart --clear-sessions clears all persisted conversation state
Durable Pi transcriptPi-native JSONL store plus the canonical history record’s random provider epoch and transcript revisionCrash-safe cross-restart and cross-process resume for Pi-native provider sessionsmono-agent restart --clear-sessions, deleting either store, a dirty fence or legacy/missing history record, host-only history append, failed provider sync, or leaving piSessionsRoot unset
Web console threadmono-agent web / @mono-agent/webPersistent source-bound browser conversation, its messages/attachments/live follow-ups, durable submission receipts, and at most one active turn; different threads can run concurrentlyArchive only hides it; mono-agent web reset --all --yes removes the entire stopped console store. Browser disconnect does not end its active turn; service restart marks that turn interrupted, requeues only unmarked offers, renders dispatch-marked live input uncertain without retry, and retains submission receipts for read-only recovery

Boundary rules:

BoundaryWhat endsWhat survivesWhat is emitted
Daily rollover (runtime.session.rollover: "daily")The current day-bucket conversation id and its warm provider-session lineage, on every channel except the console (TUI + web)Durable memory, old run artifacts, durable Pi transcripts for other ids, app process state, and every console threadsession_boundary with kind: "rollover" on the first turn of the new bucket
Isolated proactive turn (runtime.session.isolateProactive: true)Nothing shared; the proactive turn intentionally skips the conversation’s warm provider sessionExisting interactive warm session, durable history, memory, and run artifactssession_boundary with kind: "isolated" and reason: "proactive"
Model change within a continuous conversationThe previous model-bound provider epoch; the new model starts from canonical historyDurable message and tool history, memory, and run artifactssession_boundary with kind: "resume_replay" and reason: "model_change"
First bound turn for a legacy unbound provider recordThe pre-model-binding provider epoch; the requested model starts from canonical history without guessing the previous ownerDurable message and tool history, memory, and run artifactsOne cold session event plus session_boundary with kind: "resume_replay", both with reason: "legacy_unbound_model"
Resume replay after stale/missing provider sessionThe stale provider session idDurable history, memory, run artifacts, and the run itself, which retries onceruntime_warning session_resume_retry plus session_boundary with kind: "resume_replay"
Host-only history append / unsynchronized provider resultThe prior durable provider epochCanonical history, memory, and run artifactsThe next provider turn receives a fresh epoch id and replays canonical history
Cancelled admitted interactive turnThe unfinished run; an unsafe or unsettled provider epochA bounded, redacted continuity account in canonical history, retained tool-history records, and run artifactsEligible durable Pi turns retain their native context and emit resume_replay with cancelled_turn_resume on the next turn; unsafe tails reseed from the continuity account
Failed admitted non-isolated turn before success commitThe failed run; an ineligible or unproven provider epochA bounded, redacted continuity account in canonical history, retained tool-history records, and run artifacts; memory capture remains excludedOne eligible provider failure per epoch can retain native context and emit failed_turn_resume; context/auth/usage failures, invalid results and exhausted retries reseed
Telegram /newCurrent chat’s warm provider session plus message and tool history for its logical session across daily rolloverAll unrelated conversations, durable memory, run artifacts, and the chat’s model/effort overrideTelegram confirmation; the next message rebuilds startup context and reloads skills
Idle eviction / replaced / disposed provider sessionWarm runtime continuity for that conversation idDurable Pi transcripts, durable history, memory, and run artifactsApp log line and status metadata event (evicted) with reason
Detached status readNothingAll runtime/session stateNo runtime event; status reads the latest published config + store snapshot
mono-agent restart --clear-sessions / explicit purgeDurable Pi transcripts under piSessionsRoot, message-history files, the tool-history sidecar, and ACP session authorizations beside artifacts.dirDurable memory under memory.path, recorded run artifacts, and process-job records/output; nonterminal jobs are interrupted by any restartRestart/status output reports message-history and tool-history counts/bytes plus ACP authorization counts separately
Browser disconnect or reloadOnly that SSE/browser connectionWeb service turn, source-bound thread, messages, committed attachments, provider/harness workReconnect receives current state and subsequent events
Web service restartAny web-owned active upstream connectionTerminal messages, archived/active threads, committed attachments, queued live follow-ups, submission receipts, agent memory/history, recorded runsActive web turn is projected as interrupted; unmarked live offers become queued normal turns, dispatch-marked offers become uncertain, and browsers recover a known submission with GET instead of repeating POST
mono-agent web reset --all --yesEntire stopped web-console SQLite/settings/upload stateAgent configs, provider/harness history, memory, and recorded-run artifactsCLI confirmation/result only
mono-agent web-control resetValidated idle host admission, cooldown and quota metadata under ~/.mono-agent/web-control; active requests prevent resetConversations, artifacts, documents and account quota; ordinary session resets and restarts preserve web-control stateCLI operational metadata only

runtime.session decides whether the runtime keeps a warm provider session per conversation or starts fresh on every message.

The primary’s first attempt owns the provider session. Retries and failovers run stateless with bounded transcript-tail replay. With coordinated durable Pi history, any answer from a retry or backup retires the primary epoch. The next turn cold-reseeds from canonical history; after a primary first-attempt success, subsequent turns resume the new session and are eligible for provider caching.

On a warm turn whose primary attempt fails, the retry or backup attempt runs stateless with the current message and a bounded snapshot of the failed attempt, without the earlier conversation; the next turn reseeds from canonical history.

A continuous conversation binds its provider session to the requested primary model, including a thread or channel model override. Repeating that model stays warm; changing it (including returning to the default) retires the old session on its owning runtime and starts a fresh epoch. The cold turn is seeded from canonical user/assistant text and the existing bounded tool-history projection; subsequent warm turns retain the native transcript, including tool results and signed reasoning. Effort-only and same-model overrides do not rotate the session. Continuations and opt-in proactive isolation keep their existing one-shot behavior. Configured retry/fallback behavior follows the fallback session policy.

With providers.piNative.piSessionsRoot, the durable history record and its recovery fence persist the model binding alongside the epoch. A restarted process resolves the session’s owning runtime from that binding. Existing histories without a binding load normally but take one cold reseed before becoming bound. Older binaries reject the new bound history shape; downgrade does not automatically rotate or migrate those records.

KeyTypeDefaultMeaning
runtime.session.mode"continuous" | "per-message"continuouscontinuous keeps a warm provider session per conversation; per-message rebuilds context each turn
runtime.session.idleTimeoutMsnumber (ms)1800000 (30 min)How long a warm session lingers before idle eviction
runtime.session.rollover"none" | "daily"noneWhether the responder buckets conversation ids by local day
runtime.session.rolloverTimezoneIANA timezone stringsystem local timezoneTimezone used to compute the daily rollover bucket
runtime.session.rolloverNoticebooleanunset / offWhen true, the first turn of a new daily bucket gets a one-line adapter-visible notice before the model answer

In continuous mode the runtime holds one warm provider session per conversation. Same-conversation follow-ups queue and resume warm rather than rebuilding the provider session from scratch. A queued warm-session follow-up holds no concurrency slot while it waits (see below). After idleTimeoutMs with no activity, the session is evicted and the next message starts cold.

{
"runtime": {
"model": "anthropic:claude-sonnet-4-6",
"session": { "mode": "continuous", "idleTimeoutMs": 1800000, "rollover": "daily", "rolloverTimezone": "UTC", "rolloverNotice": false }
}
}

Env vars: MONO_AGENT_SESSION_MODE, MONO_AGENT_SESSION_IDLE_TIMEOUT_MS, MONO_AGENT_SESSION_ROLLOVER, MONO_AGENT_SESSION_ROLLOVER_TIMEZONE, MONO_AGENT_SESSION_ROLLOVER_NOTICE.

Warm in-memory sessions are lost on restart. To resume across restarts, use the default durable history store together with providers.piNative.piSessionsRoot (see Pi-native tuning below). The history store, not a conversation-id hash, owns the resumable provider epoch.

rolloverNotice is adapter-local and default-off. It does not enable rollover by itself and does not add a new IPC channel or change provider resume behavior. When daily rollover is already enabled and a base conversation crosses into a new day bucket, the responder streams New session bucket started: <bucket>. before the model answer and includes the same prelude in the returned final text for final-only transports.

Daily rollover partitions active conversation/provider history, but it does not partition retained exploration: the app-owned RunHistory and SessionHistory tools strip the daily bucket only for their request-scoped authorization match, so completed runs and tool records from earlier buckets of the same logical conversation remain searchable. Reset clears both message history and tool records across every daily bucket exposed as that logical session. Custom history stores must implement logical-session reset for daily rollover or the operation fails closed before either store is cleared. Other conversations and threads remain inaccessible.

The default durable history store makes that reset atomic across processes: a logical-session owner row covers bucket discovery and every matching append or reset, while a date-shaped exact bucket has a separate exact-id row. The rows contain namespaced digests rather than conversation ids, and the shared SQLite transactions are short, so unrelated logical sessions remain concurrent even when they map to the same one of 16 fixed registry files. Normal settlement deletes each row; crash recovery replaces it only after its owner PID is no longer live. A bounded capacity pass applies the same proof to distinct crashed owners; the store never age-deletes a claim. Full-synchronous DELETE journals make interrupted row changes recoverable, while fixed file, row-count, and byte ceilings prevent per-conversation lock-file growth.

Rollover never applies to the console channel (the gui operator channel behind both mono-agent tui and the web console). A console thread already carries an explicit, reader-owned session boundary: it has a permanent conversation id and a visible “new thread” action. Bucketing it by day on top of that severed a live conversation at midnight, so the next morning’s follow-up in the same visible thread woke with no transcript and had to reconstruct it through RunHistory. Every other channel — Telegram, Slack, cron, webhook, OpenAI-API — takes the configured policy unchanged. Both history tools still use the configured policy for their scope match, so runs and tool records a console thread already recorded under a dated id stay searchable from the same thread’s undated id.

Concurrency: admission and execution bounds

Section titled “Concurrency: admission and execution bounds”

concurrency bounds how much work is in flight. There are two separate limits, applied at different points in a run:

KeyDefault-bearingCapsApplied
concurrency.maxConcurrentRunsyesHow many runs execute against the provider at once (execution width)At the provider step
concurrency.maxPendingRunsyesHow many runs may be admitted and wait before the provider stepBefore the expensive provider step

maxConcurrentRuns is the execution width — the number of runs that may be calling the provider simultaneously. maxPendingRuns is the admission bound — it caps how many runs can be queued waiting for an execution slot before new work is rejected, protecting you from unbounded backlog ahead of the expensive provider call. Queued follow-ups on a warm session hold no slot against either limit.

{
"concurrency": {
"maxConcurrentRuns": 4,
"maxPendingRuns": 16
}
}

Env vars: MONO_AGENT_CONCURRENCY_MAX_CONCURRENT_RUNS, MONO_AGENT_CONCURRENCY_MAX_PENDING_RUNS.

These bounds cover the harness run path (which begins at responder.respond). Channel adapters (Slack/Telegram) do per-conversation admission and attachment downloads before that boundary, so cross-conversation transport download IO is not covered here — per-file byte caps and timeouts apply to that instead. A plain-text same-conversation follow-up can be applied inside the active provider run; its reserved adapter queue slot is released after acknowledgement or becomes the next normal turn on an unsupported/failed/end-of-turn race. Adapter queues are drained and aborted on /cancel and stop.

The web console separately admits only one active turn per thread. Its one Send path carries a client-generated submission UUID, while the service chooses a normal turn or targets the exact active Web operation through harness-owned live-input ownership. A targeting-capable operator waits only for that operation’s run id; a closed, disconnected, timed-out, or mismatched wait cannot drift into its successor. An older operator produces the visible, durable unsupported_targeting next-turn queue instead of guessing. Replaying the same UUID and immutable payload returns the durable receipt without another dispatch, and browser reload recovery reads that receipt without automatically posting authored content again. These submissions never create parallel responses in the same thread. Because each thread has its own permanent conversation id, distinct web threads and distinct agents can execute concurrently subject to the selected agent’s ordinary harness limits. Closing the browser does not free a harness slot or cancel that turn; use the visible cancel action when cancellation is intended.

Once an admitted, non-isolated run settles as cancelled or failed before success commit, the harness seals the accepted partial assistant/tool prefix, releases the conversation lane, and runs one bounded continuity finalizer. A per-conversation barrier prevents the next turn from assembling context until that finalizer publishes the 48 KiB account and either recovers or retires the provider epoch. Cancellation closes the mailbox and rejects the live caller immediately; the publication barrier allows up to 1,000 ms by default for the provider to settle before choosing retirement. Recovery itself completes its persistence transaction before the barrier opens. Late text and tool events from that call are quarantined. Cancellation retains its typed host abort reason. Failure records trusted host settlement fields and keeps raw runtime/provider code and detail only as bounded, redacted untrusted evidence. Isolated proactive/continuation runs remain outside shared history, and a queued request cancelled before admission publishes no account. If publication fails, later turns fail closed with the outcome-specific continuity error. Hosts may override the window with AgentHarnessOptions.session.terminalRecoverySettlementMs, a positive safe integer, or the top-level terminalRecoverySettlementMs option of createConfiguredAgentHarness. Tests may use a longer window; this is not a configuration-file setting.

The ordinary successful-turn boundary remains atomic. Once success claims that boundary, a later abort or exception does not replace it with a continuity account. A process signal that unwinds through the harness is covered by the ordinary cancellation/failure paths. A hard process death that never unwinds is not: startup reconciliation can mark the run artifact and web projection interrupted, but cannot synthesize canonical history from lost process state.

These values are not a single global cap. The app builds one harness — and therefore one limiter — per enabled channel. Each channel’s limiter bounds that channel independently. With N enabled channels, the effective ceiling is N × the configured value.

Size the value as a per-channel budget. If you need a hard app-wide ceiling, divide your target by the number of enabled channels. See Channels for which channels are active.

providers.piNative tunes the Pi-native provider path: transport selection, retry behavior on transient provider failures, and optional durable session storage. These apply to every provider route. All fields are optional.

KeyRange / DefaultMeaning
providers.piNative.transportauto (default), sse, websocket, websocket-cachedPreferred provider transport; providers without multiple transports ignore it
providers.piNative.promptCacheDiagnosticsboolean; default falseMetadata-only request fingerprints in run artifacts
providers.piNative.piMaxRetries08, default 2Transient provider-transport retries
providers.piNative.maxRetryDelayMsdefault 60000Backoff cap between retries (ms)
providers.piNative.piSessionsRootpath; unset = in-memoryDurable JSONL session store enabling resume across restarts
{
"providers": {
"piNative": {
"transport": "sse",
"piMaxRetries": 2,
"maxRetryDelayMs": 60000,
"piSessionsRoot": ".mono-agent/sessions"
}
}
}

Env vars: MONO_AGENT_PI_PROMPT_CACHE_DIAGNOSTICS, MONO_AGENT_PI_TRANSPORT, MONO_AGENT_PI_MAX_RETRIES, MONO_AGENT_MAX_RETRY_DELAY_MS, MONO_AGENT_PI_SESSIONS_ROOT.

auto preserves Pi’s provider-specific default and fallback behavior. An explicit mode is host-authoritative for configured agents: request-scoped runtime extensions cannot replace it. Every Pi result records the normalized choice as diagnostics.pi_transport_requested; this is the requested mode, not a claim that a provider with only one transport changed its wire protocol.

With the configured app’s default history store, setting piSessionsRoot persists Pi sessions to JSONL and enables history-coordinated resume after restart. Before provider execution, the store publishes and fsyncs a separate owner-only dirty fence while holding a cross-process conversation lock from a fixed 16-shard table. The fixed table bounds lock files while safely serializing shard collisions; legacy per-conversation lock files are honored in place during migration. The fence does not replace, count as, or prune canonical history. A successful provider result is eligible for reuse only when it returns the exact epoch-derived id and the runtime affirmatively fsyncs both its JSONL file and parent directory. The history messages, clean provider epoch, and incremented transcript revision then publish in one atomic replacement before the fence is cleared.

If the process dies after provider mutation but before that clean commit, the fence remains. The next same-conversation run retires the exact fenced JSONL, rotates to a new random epoch, and replays canonical history. An unrelated mutation also reclaims inactive fences as retirement journals: provider deletion and directory fsync complete before the fence is removed. If canonical epoch/revision proves that history commit succeeded and only fence cleanup crashed, maintenance preserves the valid transcript and removes only the stale fence. Beginning and aborting a fresh conversation cannot evict an older successful conversation because fences are bounded separately. Missing/v1 records, failed sync, retention that removes a record, and appendVerbatimTurn host-only deliveries retire and rotate provider state for the same reason.

Cancelled and failed turns can retain the same durable Pi epoch when the primary first attempt provides a receipt for a closed operation. Recovery reserves and reopens the exact record, checks model, revision, tip, ancestry, settled operation and applied input identities, validates the effective provider projection, and fsyncs the transcript and directory before canonical history advances one revision. It appends nothing to Pi. Pi filters interrupted assistant prose and reasoning; completed native tool turns and the cancelled user input remain. Pi’s serializers supply error results for surviving orphaned tool calls. Canonical history keeps the existing continuity account, including bounded partial prose and error detail; SessionHistory retains the same tool evidence. No cancelled/failed memory capture occurs.

User cancellation does not spend the failure budget. One provider_unavailable failure, including single-primary exhaustion with matching proof, may recover per epoch in the current process. Success does not reset that budget. A second failure, context termination, auth/usage limits, invalid/empty results, session errors, ambiguous throws, extra attempts, late contradictory evidence, failed tool-history finalization or uncertain persistence selects cold reseed. The budget and one-shot boundary marker live only in the in-memory session record and clear on rotation; reconstructing the harness can allow one additional failed-turn recovery. The durable record and v4 fence shapes are unchanged by terminal recovery, so this feature adds no older-binary incompatibility beyond model binding.

Recovery is opt-in through the built-in coordinator’s providerSessionRecovery: "v1" capability and the owning runtime’s recoverSession method. Custom stores without the capability retain retirement. Clear-sessions, retention removal, host-only appends, model changes and unreconciled dirty fences still reseed. If retirement races an abort-ignoring provider, the late result cleans only its captured old id, including any recreated headerless JSONL. Retirement uncertainty fails the publication barrier closed.

Each clean record also carries the durable provider transcript revision. A process saves that revision with its warm handle. If another process commits the same epoch first, the revision mismatch forces the stale process-local handle to close and reopen the current JSONL (or rebuild from canonical history) before it can omit history. The same strict refresh runs for an unconfirmed durable resume when a newly constructed harness has no local mapping, preventing a module-global provider registry from reviving older process memory. Cross-process serialization therefore protects both disk writes and in-memory provider state.

On every cold durable Pi reopen, the harness loads canonical history and passes it as structured leading runtime messages, with the current user message last; it does not duplicate those turns inside the system prompt. Pi appends the leading messages only when the requested durable epoch has no JSONL and must be created on miss. When the JSONL exists, Pi resumes it and skips the supplied leading history, so a true resume also sees each prior turn exactly once. Confirmed warm turns send only the current user message. Stateless/non-resumable turns and the one explicit resume-retry continue to replay history through the ordinary prompt path.

When piSessionsRoot is unset, sessions are in-memory only. A programmatic custom historyStore also stays process-local unless it both implements beginProviderSessionTurn and advertises providerSessionRetirement: "fail-closed"; the harness withholds the durable path because fencing alone cannot reclaim cold JSONL after rotation or retention. Advertise that capability only when the store can durably fence before the provider, serialize the conversation across processes, expose a monotonic provider transcript revision, atomically publish the next revision or rotate the epoch with history commit, and prove exact-id provider transcript retirement before making an epoch unreachable.

Canonical context import is a separate optional v1 contract; append or deliverVerbatim does not imply it. A store may advertise import only when a two-message provenance/assistant batch fits every retention and staging quota, and when durable provider state is explicitly absent or exact retirement is fail-closed. The default store serializes import with Send in continuous, per-message, and sessions-disabled modes. The new non-provider path holds only logical/exact claims during provider execution, then briefly acquires the physical shard to verify an opaque history version and publish. The existing durable-provider transaction still holds that shard for the full turn; this known same-shard blocking behavior is unchanged.

An exact retained provenance/assistant pair is the bounded retry receipt. A same-key/same-text retry returns duplicate, including after a later Send while the pair remains retained; a changed payload conflicts. Retention never keeps half the pair. Explicit reset, corruption, or deletion of the whole canonical record also removes the receipt, so idempotency is not permanent across those boundaries. An empty replacement conversation can be seeded again; a nonempty conversation whose pair was evicted fails closed as conversation_not_empty. Warm process-local handles carry the canonical history version and are retired before the next Send when a reset/import advanced it.

For retry behavior across different models (provider failover, not transport retries), see Fallback & failover. Transport retries here are within a single model; fallback moves to the next model in the chain.

providers.piNative.promptCacheDiagnostics (default false; env MONO_AGENT_PI_PROMPT_CACHE_DIAGNOSTICS) enables metadata-only request fingerprints in existing run artifacts. It never emits prompt text, tool arguments, raw cache keys, endpoints or credentials. See Prompt-cache measurement for the artifact reader.