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.
The five “session” meanings
Section titled “The five “session” meanings”Mono-agent uses “session” for five related but different boundaries:
| Meaning | What owns it | What it controls | What resets it |
|---|---|---|---|
runtime.session config block | Agent config / env | Whether turns try to reuse a warm provider session and how long idle warmth lasts | Changing config, setting mode: "per-message", or disabling resume support |
| Provider session | Runtime backend / provider bridge | Warm runtime continuity: provider-side context, provider session id, busy state, and idle eviction | Idle 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 history | Durable message-history files plus the separate tool-history/tool-lifecycles.sqlite sidecar | Cold 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 account | A 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 transcript | Pi-native JSONL store plus the canonical history record’s random provider epoch and transcript revision | Crash-safe cross-restart and cross-process resume for Pi-native provider sessions | mono-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 thread | mono-agent web / @mono-agent/web | Persistent source-bound browser conversation, its messages/attachments/live follow-ups, durable submission receipts, and at most one active turn; different threads can run concurrently | Archive 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:
| Boundary | What ends | What survives | What 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 thread | session_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 session | Existing interactive warm session, durable history, memory, and run artifacts | session_boundary with kind: "isolated" and reason: "proactive" |
| Model change within a continuous conversation | The previous model-bound provider epoch; the new model starts from canonical history | Durable message and tool history, memory, and run artifacts | session_boundary with kind: "resume_replay" and reason: "model_change" |
| First bound turn for a legacy unbound provider record | The pre-model-binding provider epoch; the requested model starts from canonical history without guessing the previous owner | Durable message and tool history, memory, and run artifacts | One cold session event plus session_boundary with kind: "resume_replay", both with reason: "legacy_unbound_model" |
| Resume replay after stale/missing provider session | The stale provider session id | Durable history, memory, run artifacts, and the run itself, which retries once | runtime_warning session_resume_retry plus session_boundary with kind: "resume_replay" |
| Host-only history append / unsynchronized provider result | The prior durable provider epoch | Canonical history, memory, and run artifacts | The next provider turn receives a fresh epoch id and replays canonical history |
| Cancelled admitted interactive turn | The unfinished run; an unsafe or unsettled provider epoch | A bounded, redacted continuity account in canonical history, retained tool-history records, and run artifacts | Eligible 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 commit | The failed run; an ineligible or unproven provider epoch | A bounded, redacted continuity account in canonical history, retained tool-history records, and run artifacts; memory capture remains excluded | One 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 /new | Current chat’s warm provider session plus message and tool history for its logical session across daily rollover | All unrelated conversations, durable memory, run artifacts, and the chat’s model/effort override | Telegram confirmation; the next message rebuilds startup context and reloads skills |
| Idle eviction / replaced / disposed provider session | Warm runtime continuity for that conversation id | Durable Pi transcripts, durable history, memory, and run artifacts | App log line and status metadata event (evicted) with reason |
| Detached status read | Nothing | All runtime/session state | No runtime event; status reads the latest published config + store snapshot |
mono-agent restart --clear-sessions / explicit purge | Durable Pi transcripts under piSessionsRoot, message-history files, the tool-history sidecar, and ACP session authorizations beside artifacts.dir | Durable memory under memory.path, recorded run artifacts, and process-job records/output; nonterminal jobs are interrupted by any restart | Restart/status output reports message-history and tool-history counts/bytes plus ACP authorization counts separately |
| Browser disconnect or reload | Only that SSE/browser connection | Web service turn, source-bound thread, messages, committed attachments, provider/harness work | Reconnect receives current state and subsequent events |
| Web service restart | Any web-owned active upstream connection | Terminal messages, archived/active threads, committed attachments, queued live follow-ups, submission receipts, agent memory/history, recorded runs | Active 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 --yes | Entire stopped web-console SQLite/settings/upload state | Agent configs, provider/harness history, memory, and recorded-run artifacts | CLI confirmation/result only |
mono-agent web-control reset | Validated idle host admission, cooldown and quota metadata under ~/.mono-agent/web-control; active requests prevent reset | Conversations, artifacts, documents and account quota; ordinary session resets and restarts preserve web-control state | CLI operational metadata only |
Provider sessions
Section titled “Provider sessions”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.
| Key | Type | Default | Meaning |
|---|---|---|---|
runtime.session.mode | "continuous" | "per-message" | continuous | continuous keeps a warm provider session per conversation; per-message rebuilds context each turn |
runtime.session.idleTimeoutMs | number (ms) | 1800000 (30 min) | How long a warm session lingers before idle eviction |
runtime.session.rollover | "none" | "daily" | none | Whether the responder buckets conversation ids by local day |
runtime.session.rolloverTimezone | IANA timezone string | system local timezone | Timezone used to compute the daily rollover bucket |
runtime.session.rolloverNotice | boolean | unset / off | When 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:
| Key | Default-bearing | Caps | Applied |
|---|---|---|---|
concurrency.maxConcurrentRuns | yes | How many runs execute against the provider at once (execution width) | At the provider step |
concurrency.maxPendingRuns | yes | How many runs may be admitted and wait before the provider step | Before 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.
Per-channel scope gotcha
Section titled “Per-channel scope gotcha”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.
Pi-native tuning
Section titled “Pi-native tuning”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.
| Key | Range / Default | Meaning |
|---|---|---|
providers.piNative.transport | auto (default), sse, websocket, websocket-cached | Preferred provider transport; providers without multiple transports ignore it |
providers.piNative.promptCacheDiagnostics | boolean; default false | Metadata-only request fingerprints in run artifacts |
providers.piNative.piMaxRetries | 0–8, default 2 | Transient provider-transport retries |
providers.piNative.maxRetryDelayMs | default 60000 | Backoff cap between retries (ms) |
providers.piNative.piSessionsRoot | path; unset = in-memory | Durable 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.
Durable sessions and restart
Section titled “Durable sessions and restart”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.
Related
Section titled “Related”- Pi runtime & model references — choosing
runtime.model - Local providers —
<provider>:<model>for Ollama / LM Studio / OpenAI-compatible - Fallback & failover — ordered backups on retryable provider failure
- Tool scheduling — safe parallel or forced-sequential tool calls within a model step (code-only)
Prompt-cache diagnostics
Section titled “Prompt-cache diagnostics”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.