Skip to main content

Agent Runtime

This document defines the stable end-to-end agent runtime shape. It ties together the agent object ABI, the human CLI, socket sessions, the persistent terminal, the tsh tool shell, prompt construction, and sandbox execution.

It does not add a root namespace. Everything here is derived from existing stable objects and files.

The durable terminal resource used by this runtime is specified in terminal-abi.md. In this first slice an Agent start materializes one terminal resource below its session; the resource id and event history remain after the live PTY socket exits.

Definition, Instance, Session, And Run​

The word agent does not name one daemon. The runtime distinguishes four layers:

definition /ctx/agent/<name> + /ctx/agent/<name>.d/
instance supervisor unit and receipt-bound invocation
session /ctx/home/<uid>/agent/<name>/session/<session>/
run one run id recorded in that session's event stream

An Agent definition is durable configuration and policy. Starting it may create more than one process role: the system Agent socket service and an optional per-session terminal unit. Those processes are runtime instances; their unit, invocation, pid, identity, socket, and session facts are bound by launch receipts. The latest cleanup receipt may be projected in agent/<name>.d/meta.json, while durable agent.start and run events preserve correlation facts in ordinary logs and session history.

A session is not a process and survives instance exit. A run is not a session and cannot silently acquire authority from previous runs. Restarting a runtime may resume a private or shared session without preserving a process identity.

There is deliberately no parallel instances/ control plane. systemd owns live process truth, receipt metadata proves which generation CortexFS may stop, and session files own durable history. Copying those facts into another mutable directory would introduce conflicting lifecycle authorities.

The Agent socket and terminal socket are transports for an active instance. Their stable /ctx paths may be aliases to endpoints under /run, and their absence never deletes the Agent definition or a private/shared session.

The packaged cortexfs-agent@.socket is ordered after and restart-coupled to cortexfs.service. ctx update restores cortexfs.service itself when that unit was active before the transaction. Package upgrades also start only the agent socket instances explicitly enabled through sockets.target.wants; an upgrade therefore does not leave an enabled chat endpoint inactive after the FUSE mount restarts.

Runtime Surfaces​

The equivalent resource commands are ctx terminal status, ctx terminal watch, and ctx terminal attach. ctx terminal create AGENT is currently an agent-backed compatibility create: it starts the Agent session and records the terminal resource. A detached command supervisor is reserved for a later terminal ABI revision.

There are three separate surfaces:

human chat ctx agent chat/send/resume/cancel
human terminal ctx agent watch/attach
agent tool use tsh inside ctxterm

They must not collapse into one interface.

ctx agent chat is the human chat UI. It owns line editing, Ctrl+C, socket requests, assistant response rendering, and prompt re-display. Interactive chat responses are buffered before printing so assistant output cannot corrupt the user's input buffer. Ctrl+C exits an idle chat; while a run is active it first requests cancellation for that run and returns to the prompt.

ctx agent send is a non-interactive human command. It may stream assistant deltas as they arrive.

ctx agent attach and ctx agent watch join the persistent agent terminal. That terminal is not the human chat UI. It exists so humans can observe or join the same PTY used by the agent-facing shell.

tsh is the agent-facing tool shell. It is a tool and a standalone binary, but it is not a host shell. It resolves commands through CTX_PATH, never through host PATH.

Every tool execution also intersects agent/<name>.d/perm: r admits the built-in read/list/stat family, w admits write/replace, and x admits shell and host-like terminal tools. tsh remains the routing shell and does not by itself consume x; the selected capability does.

Default Human Entry​

agent.sh is a small defaults frontend over the Rust-owned ctx agent commands:

agent.sh AGENT -> ctx agent chat AGENT --session default
agent.sh AGENT INPUT... -> ctx agent send AGENT --session default INPUT...
agent.sh --watch AGENT -> ctx agent watch AGENT --session default
agent.sh --attach AGENT -> ctx agent attach AGENT --session default

agent.sh must remain a tiny defaults wrapper. Socket protocols, terminal emulation, model streaming, policy checks, tool discovery, and provider behavior belong in CortexFS, primarily under ctx.

Socket Chat Flow​

The stable request flow is:

human
-> ctx agent chat/send
-> agent/<name>.sock
-> socket runtime
-> durable session files
-> selected model/agent executable
-> event JSONL
-> ctx renderer

Socket requests are JSONL. They name a session and operation:

{"op":"send","id":"msg-1","session":"default","scope":"private","cwd":"/workspace","input":"hello"}
{"op":"tsh","id":"tool-1","session":"default","args":["load","bash"]}
{"op":"resume","session":"default"}
{"op":"status","session":"default"}
{"op":"cancel","id":"run-1"}

status is a read-only control-plane query. It returns one bounded JSONL projection such as:

{"type":"status","session":"default","status":"active","phase":"active","run":"run-1","step":1,"model":"main","updated_at":"1710000000"}

The projection is non-secret. It may contain lifecycle, run, step, action, tool, model reference, context revision, timestamp, and stable errno fields, but never credentials, prompt contents, or message contents. A missing session is reported as idle; malformed state is an EIO-class runtime failure. When a hosted step has a compiled working set, context_revision is a length-delimited SHA-256 digest of its bounded inputs; it is not the Context contents and cannot be used to reconstruct them. Existing clients can continue to use resume for replay and send for a live run stream.

The socket surfaces map to Unix-style operations without adding a root namespace: chat is send, command execution is tsh or cancel, event attachment is resume with an optional cursor, and inspection is status. Each request remains bounded and one-shot at the transport layer; a caller may reconnect with the last event id after a disconnect. Durable files, not socket presence, remain lifecycle truth.

tsh executes through the authenticated agent runtime without a model call. It emits canonical start, tool_call, tool_result, and done frames. Repeating an identical request id replays the durable result without executing the command twice.

The socket runtime records user messages before invoking the agent/model path. Assistant text is derived from stable event frames and recorded back to the durable session. Raw messages and events remain ordinary files; context packs are rebuildable views.

Executable agents use the required agent/<name>.d/abi control. Its accepted value is sdk-envelope-v1: the host writes a bounded typed invocation envelope to stdin and may restart the executable with the authoritative result of a yielded tool call.

The sdk-envelope-v1 stdin body is exactly one UTF-8 JSON object followed by one newline, with no other bytes, and is at most 1 MiB including that newline:

{
"schema": "cortexfs.agent-invocation/v1",
"run": "run-1",
"step": 1,
"input": "original user input",
"origin": {
"transport": "channel",
"endpoint": "discord",
"conversation": "room-1",
"thread": "thread-1"
},
"history_messages": "[]",
"tool_context": "",
"observation": {
"tool_call_id": "call-1",
"name": "example.echo",
"status": "ok",
"content": "authoritative normalized result",
"truncated": false
}
}

Unknown or missing fields are invalid. run and step must equal the host-owned launch environment. Step 0 requires null observation; later steps require exactly the immediately preceding host result. Context strings are at most 64 KiB each and observation content at most 16 KiB. The host reads the canonical positive CTX_AGENT_STEPS value from agent/<name>.d/env as the per-run tool continuation budget; bootstrap writes 64. The derived value is informational to the executable Agent, while the host enforces the budget. The host rejects replayed call ids before authorization, rechecks policy for every call, and checks cancellation before, during, and after each SDK/tool process. Only the host writes tool results and the logical run's lifecycle frames. It records the original user message once, each normalized result once, and one final assistant/error outcome; a process crash cannot resume from agent-provided state.

An executable Agent SDK step may terminate by yielding exactly one typed tool_call. The process emits no done frame and exits. The socket host validates and executes the request through the existing agent tool authority, policy, and sandbox path, emits the matching tool_result, and may start the next bounded step with that result in the typed envelope. The host alone emits the logical run's final done. Agent-originated results, malformed or multiple calls, and frames after a yielded call are invalid output.

For a channel-backed run, Runtime additionally injects these child-only values:

CTX_CHANNEL_ID
CTX_CHANNEL_SESSION
CTX_CHANNEL_CAPS
CTX_CHANNEL_SOCKET

CTX_PATH is rebuilt for the request in this order: the user's channel tool directory, the global channel tool directory, the Agent's original path, then the normal /ctx/tool tiers. User tools override global channel tools; a collision with an existing Agent tool is rejected. The channel token and other credentials never enter these values. Identity, attachments, and thread data stay in the bounded structured origin/event envelope.

The optional agent/<name>.d/approval control is auto when absent and accepts auto or ask. In ask mode, after the host has completed direct-native declaration, path, agent/tool policy, Linux/mount, and nofollow executable checks—but before spawn—it emits a bounded approval_request. It reads exactly one bounded response on the same socket:

{"op":"approve","run":"run-1","id":"call-1","decision":"allow_once"}

Only allow_once executes that prepared call. deny, EOF, timeout, malformed, or mismatched responses fail closed and become host-owned approval and tool result facts. Agent executables cannot emit approval frames.

The root-authoritative system socket accepts the agent owner UID or UID 0 for internal child dispatch and stop. This UID 0 exception does not apply to the receipt-bound per-run capability socket, which remains owner-UID only.

Before invoking an executable agent, the socket runtime writes exactly one sdk-envelope-v1 frame to stdin. Its history_messages and tool_context fields carry bounded prompt context; the agent boundary does not expose legacy CTX_AGENT_HISTORY_MESSAGES or CTX_AGENT_TOOL_CONTEXT environment inputs. When a run makes several tool calls, tool_context retains earlier authoritative observations in order while observation carries the immediately preceding result. The host keeps this transcript bounded by the tool-context limit. The built-in hosted agent converts the latest matched call and observation into canonical assistant-tool-call and tool-result messages for OpenAI Chat and Responses continuation. The call IDs must match; missing or malformed metadata falls back to bounded text context and never invents a tool result.

If a human sends SIGINT while a run is active, ctx agent chat sends a cancel request for the active run id and returns to the prompt. In an idle interactive chat, Ctrl+C exits the chat UI.

The socket-activated executable agent runtime observes the durable session state for the active run. When the matching done/cancelled event appears, it stops the executable agent process group with SIGTERM, escalates to SIGKILL after a short grace period, and does not record later assistant output for that run.

Persistent Terminal Flow​

The terminal flow is:

ctx agent start
-> systemd-run --user
-> bwrap sandbox
-> ctxterm --broker AGENT SESSION UNIT -- /ctx/bin/tsh
-> register and activate with the root broker
-> tsh

ctx agent start NAME -- COMMAND... replaces the default tsh child with the exact hosted argv. That launch drops Restart=, so the hosted child is a one-shot Unix process. Canonical /ctx becomes one writable FUSE bind only after process-start findmnt confirms a read-write fuse/fuse.cortexfs CortexFS mount. Network stays --unshare-net unless policy allows network:default connect and the host can run pasta; see tool-policy-abi.md.

ctxterm owns the pseudo-terminal. The root broker authenticates watch and attach clients and passes accepted descriptors directly to ctxterm; it does not relay PTY bytes.

Session terminal sockets are visible through the ABI path:

/ctx/home/<uid>/agent/<agent>/session/<session>/terminal/main.sock

The visible entry aliases the immutable root-owned endpoint /run/cortexfs/terminal/broker.sock. ctx agent attach sends a bounded v1 broker request containing the agent, session, mode, and fresh nonce. It MUST NOT fall back to a per-user socket or the legacy line protocol. See terminal-broker.md.

Sandbox Contract​

ctx agent start creates the default interactive terminal sandbox. Unless overridden, it binds the caller's current working directory at /workspace with read-write access and starts the terminal there. A read-write workspace keeps ordinary Git metadata writable. CortexFS does not implicitly over-mount .git read-only; an explicit /workspace/.git policy mount may still narrow it, and out-of-tree linked-worktree metadata still needs separate authority.

Host-native terminals are rejected before session or launch state changes. A same-UID native agent cannot be distinguished reliably from its operator at the terminal boundary. Native mode may return only after such agents receive a distinct Unix identity and an equivalent broker authorization path.

The sandbox home is:

HOME=/home/agent

It is backed by:

/ctx/home/<uid>/agent/<agent>

Shell state such as .config, .cache, and .bash_history must land in the agent home, not in the project workspace.

Unless an explicit mount replaces it, /tmp is a private sandbox tmpfs capped at 512 MiB. It is never the host /tmp; exceeding the limit fails the write inside the sandbox rather than consuming unbounded host storage.

Every sandbox also applies host isolation flags before process mounts:

--as-pid-1
--new-session
--cap-drop ALL
--unshare-uts

The private UTS namespace inherits its initial hostname; the launcher does not call sethostname. The packaged service denies sethostname, setdomainname, and syslog with SystemCallFilter, including inside child namespaces. A fixed cosmetic hostname is not an isolation requirement.

The privileged launcher needs an unmasked parent procfs: ProcSubset=pid, ProtectHostname, ProtectKernelTunables, and ProtectKernelLogs mount masks prevent a non-root Bubblewrap child from mounting its private /proc. The service therefore uses ProcSubset=all, disables those three mount-mask options, and removes CAP_SYSLOG. ProtectProc=invisible, NoNewPrivileges, read-only system paths, cgroup bounds, and the child's private PID/UTS/proc namespaces and dropped capabilities remain in force.

This is not equivalent parent-process hardening: the trusted root launcher has more procfs visibility, and its compromise remains a host-level risk. Do not load arbitrary harness extensions into that process. Further separation of privileged launch from untrusted execution needs its own authority and threat review, not another inherited procfs mask that prevents sandbox startup.

Interactive terminals launched by ctx agent start are transient user systemd units with hard cgroup ceilings (see support::quota):

MemoryMax=1G
MemoryHigh=768M
CPUQuota=200%
TasksMax=256
LimitNOFILE=1024
OOMPolicy=stop

The host refuses to start another terminal when eight agent terminal units are already running for that user. Socket-activated agent runtimes use a stricter MemoryMax=512M / CPUQuota=100% / TasksMax=128 profile, matching the packaged cortexfs-agent@.service unit.

Packaged unit files set MemoryAccounting=yes and TasksAccounting=yes next to those ceilings. They do not set CPUAccounting=yes because systemd turns CPU accounting on when CPUQuota= is present. Transient systemd-run --user terminals still pass --property=CPUAccounting=yes from support::quota.

The sandbox launcher starts from --clearenv and injects a curated allowlist. ctxterm then env_clear()s and forwards that received environment to the child; it is not a second filter and does not inherit the operator shell.

CTX_ROOT
CTX_PROVIDER_CONFIG_DIR
CTX_HOME
CTX_AGENT
CTX_AGENT_ROLE
CTX_AGENT_MODEL
CTX_AGENT_LIFE
CTX_AGENT_ROOT_PATH
CTX_AGENT_CWD
CTX_AGENT_SUBJECT
CTX_AGENT_UID
CTX_AGENT_GID
CTX_AGENT_GROUPS
HOME=/home/agent
USER
LOGNAME
SHELL=/usr/bin/bash
TERM=xterm-256color
LANG=C.UTF-8
GIT_OPTIONAL_LOCKS=0
PATH=/usr/bin:/bin

Non-colliding agent/<name>.d/env pairs are appended. A default workspace also sets CTX_WORKSPACE. Host session variables and provider secrets must not be inherited by default. Executable agents launched from the socket runtime also start with env_clear() and receive only the derived agent environment plus runtime-owned CTX_* values.

Tool Workspace Overlay​

When a Tool execution receives a valid CTX_WORKSPACE, CortexFS mounts the project through the session's writable overlay. Its upper and work data remain below the durable session; the backing project is the lower view. The overlay includes .git, so a declared writable /workspace/.git mount must not bypass that session view. Without CTX_WORKSPACE, CortexFS preserves the declared mount table and creates no workspace overlay.

Agent View And Authority​

An agent runtime view is derived from:

agent/<name>.d/root
agent/<name>.d/cwd
agent/<name>.d/env
agent/<name>.d/path
agent/<name>.d/mount
agent/<name>.d/model
agent/<name>.d/window
agent/<name>.d/compact
agent/<name>.d/policy
agent/<name>.d/uid
agent/<name>.d/gid
agent/<name>.d/groups
agent/<name>.d/perm
agent/<name>.d/label

Prompt text, AGENTS.md, skill metadata, .mcp.json, and tool descriptions may influence model behavior, but they do not grant authority.

The reference tree reserves the Agent alias main:

agent/main -> agent/executor
agent/main.sock -> agent/executor.sock

An Agent alias is an entry-point link, not a second Agent or a second control directory. The canonical control tree, Linux identity, permissions, runtime receipt, and durable sessions remain owned by the target Agent. Alias targets are fixed and validated by bootstrap; arbitrary Agent directory links are not accepted as an authority boundary.

Actual authority is the intersection of:

mount/chroot visibility
Linux uid/gid/groups and mode bits
agent `perm` capability ceiling
CortexFS label and policy
CTX_PATH tool visibility
tool executable metadata and noexec placement

Both the filesystem layer and the CortexFS policy layer must allow an action.

Context Window Control​

Every Agent, including a dynamically created child, has one durable setting:

agent/<name>.d/window

It has a second durable context policy setting:

agent/<name>.d/compact

The file contains exactly one canonical LF-terminated line:

auto

or a positive base-10 u32 token count. Numeric text has no sign, surrounding whitespace, or leading zeroes. Zero, overflow, missing values, and extra lines are invalid. Writing auto\n is the reset operation: it clears the explicit override and returns the Agent to model-derived behavior. Reset does not alter session history or context files.

window stores the setting, not a stale copied maximum. Its effective value is:

auto selected model's metadata recommended window, bounded by limit
number that exact number

compact uses the same syntax. Its auto value follows the selected model's metadata compaction threshold and is bounded by the effective window; an explicit number is a deliberate smaller threshold. The model's read-only limit, recommended, and compact files remain the source of defaults, so switching an Agent model does not leave stale copied values in agent.d.

An explicit number is valid only when the selected model maximum is known and the number is not greater than that maximum. Changing model must atomically reject a state in which the existing explicit window is greater than the new model maximum or the new maximum is unknown. It must not silently clamp or reset the setting.

Fallback selection re-evaluates the same invariant for each candidate. With auto, the effective value follows the actual candidate's recommendation. With an explicit number, a fallback whose maximum is unknown or smaller is ineligible and produces an auditable candidate error rather than silently changing the Agent setting.

When the effective value is known, the host supplies its decimal token count as the runtime-owned CTX_CONTEXT_WINDOW_TOKENS environment value. Existing character-based prompt budgeting receives CTX_CONTEXT_WINDOW_CHARS, derived with the conservative estimate of four UTF-8 characters per token. This conversion is only a bounded prompt-budget estimate; it does not change the token unit of window or limit. Arithmetic saturates at the receiving budget's bound.

When known, the effective compaction threshold is supplied as CTX_CONTEXT_COMPACTION_TOKENS; the selected durable setting is available as CTX_AGENT_COMPACT_SETTING. These values tell a context compiler when to rebuild its working set; they never delete messages.jsonl or the raw view.

The host reserves min(4096, max(1, effective_tokens / 4)) output tokens. Prompt input admission therefore uses effective_tokens * 4 total characters minus four characters for every reserved output token. The exact rendered Agent message array is serialized as JSON and its UTF-8 byte length is charged conservatively against that character budget before every model dispatch.

The effective window bounds the complete prompt working set assembled for the model call. Skill metadata keeps its existing 2% share of that derived character budget. History, tool context, rules, system text, current input, and output reservation must be accounted for before dispatch; durable raw history is never deleted to meet this bound. When the effective window is unknown, CTX_CONTEXT_WINDOW_TOKENS and CTX_CONTEXT_WINDOW_CHARS are absent and the documented conservative legacy sub-budgets apply without claiming a known model maximum.

Provider output controls such as Anthropic max_tokens remain separate. They must not be derived by treating the combined context window as an output-token limit.

Tool Shell Contract​

Agents see one native callable tool by default:

tsh

An agent's optional .d/tools control may statically declare additional direct-native tool names. Those names remain subject to fresh path, agent policy, tool policy, mount, Linux permission, schema, and nofollow checks on every call. Other tools are dynamically discovered, loaded, pinned, and invoked through tsh; dynamic tsh cache state never expands the direct-native set.

The permission control is re-read into the runtime view together with identity, mount, path, and policy controls. Changing it affects subsequent executions; it never expands policy, mount, or Linux authority.

tsh resolves tools by CTX_PATH. For standalone human sessions, it reads the data-only startup file before inherited process CTX_PATH when the file exists:

CTX_HOME/.tshrc

The file supports only:

CTX_PATH=/ctx/tool:/ctx/home/<uid>/tool

Inside an agent terminal, the runtime-provided process CTX_PATH remains authoritative because it is part of the agent view.

It is not shell syntax and must not execute code.

load means tool metadata is added to the agent's current tool context. pin means the tool is loaded and protected from automatic context eviction. Eviction policy may unload unpinned tool metadata, never authority controls.

Interactive host-like behavior is provided by ordinary tool objects named bash, tmux, or zellij when visible and allowed. tsh itself must not fallback to arbitrary host commands.

These passthrough tools use terminal stdout/stderr rather than the Tool SDK JSONL envelope. The runtime captures their bounded output and wraps it as the provider-neutral tool observation; SDK-backed tools continue to require the start/message/done envelope.

Self Iteration​

An agent iterates itself through the agent.update tool. The tool sends one agent.update frame over the receipt-bound run capability socket:

{"op":"agent.update","request_id":"tool-1","agent":"executor","session":"default","run":"run-1","control":"system.md","content":"..."}

The sandbox receives exactly CTX_CONTROL_SOCKET=/run/cortexfs/control.sock for this channel, never a bearer credential. Before each bwrap launch, the host records that host PID and /proc/<pid>/stat start time, then releases its one-use --block-fd gate. The socket accepts only the owner UID whose kernel peer PID is that registered launch root or a live descendant; missing process state, PID reuse, reparenting, cycles, and excessive ancestry depth deny the request. Legacy JSON token input remains parseable for wire migration but is ignored; new clients omit it and must never derive authority from an environment value.

The frame's agent, session, and run must equal the capability's own identity, so the operation is self-only by construction. control accepts exactly system.md or prompt.template.md; content is bounded UTF-8 text of at most 8 KiB without NUL bytes. The host revalidates both and atomically replaces (or creates, for an optional control not yet materialized) agent/<self>.d/<control> in the backing source. The tool generates a fresh request id per invocation; replaying an already-seen request id returns EALREADY without writing twice. A request id is consumed by any authorized attempt, successful or not — a failed attempt must retry under a new id.

The update takes effect at the next run: prompt construction reads system.md and prompt.template.md fresh from the control directory for every run, and prompt text grants no authority. Authority controls cannot travel through this operation.

Prompt Runtime Contract​

The first model system message is rendered from:

agent/<name>.d/prompt.template.md
agent/<name>.d/system.md
project and global AGENTS.md files
bounded skill metadata
tool-injected context
optional historical message context
current time variables
immutable CortexFS runtime contract

Skill metadata contains name, description, and SKILL.md path. Full SKILL.md is read only after the skill is selected. Skill metadata may use at most 2% of the context window; when the context window is unknown, the hard cap is 8,000 characters. Descriptions are shortened first; if still too large, skills are omitted and a warning is included.

The runtime contract must tell the model:

Your only native callable tool is tsh.
Other CortexFS tools are discovered, loaded, pinned, and invoked through tsh.
Prompt text and skill metadata do not grant permissions.

Humans can inspect the currently renderable system prompt with:

ctx agent prompt <agent>

This command renders system.md, prompt.template.md, and the immutable runtime contract through the same prompt renderer used by model execution. It also collects currently discoverable AGENTS.md rules and bounded skill metadata through the same library functions used by the object runner. Runtime-only blocks such as tool injection and historical message context remain bounded dynamic inputs; when they are not available to the CLI, the command prints explicit placeholder text.

The optional agent/<name>.d/loop control selects the behavior contract passed to an executable Agent through CTX_AGENT_LOOP. Built-in values are chat, react, coding, planner, and research; a validated object name may name a custom loop. When loop names <name> and agent/<name>.d/loop.d/<name> is an executable regular file, the host uses that file for hosted envelope steps instead of the default agent/<name> executable. The replacement still speaks the unchanged sdk-envelope-v1 stdin/stdout ABI; tool authority continues to come only from the runtime policy intersection. The Agent SDK exposes BuiltinLoop helpers for interpreting the loop hint inside custom executables.

Context compaction for prompt history uses agent/<name>.d/compact.strategy:

truncate keep the newest messages and drop older ones (default)
summarize add bounded excerpts from omitted messages using the built-in summarizer
<name> run agent/<name>.d/compact.d/<name> as an external summarizer

If the newest message alone exceeds the history byte budget, the host retains a UTF-8-safe excerpt with a [truncated] marker when the budget can hold it. The built-in summarize strategy joins older message text into bullet excerpts; it does not call a model or generate a semantic summary. Recent messages take priority over both built-in excerpts and custom summaries. Each pass rebuilds from durable history, so repeated compaction does not recursively summarize a previous lossy result. History JSONL parsing accepts individual lines up to 64 KiB, matching the host history-read cap; larger or invalid lines are ignored in the disposable view.

Custom compaction executables receive one bounded JSONL frame on stdin:

{"abi":"cortexfs.compact/v1","agent":"executor","session":"default","max_chars":8000,"omitted":2,"messages":[{"role":"user","content":"..."}]}

Exit zero with bounded stdout text continues the run; the host inserts that text as Summary of earlier context: above the retained recent messages. Empty output, non-zero exit, timeout, or oversize output falls back to the truncate strategy for that run. The five-second deadline also bounds a compactor that does not read its input; stdin and output are serviced concurrently. Compaction never rewrites durable messages.jsonl.

Agent-local hooks use the existing agent/<name>.d/hooks/pre.d/ and post.d/ directories. The runtime runs them in lexical order immediately before and after each model action. A hook is an executable regular file and receives one JSONL frame such as:

{"abi":"cortexfs.hook/v1","phase":"pre","action":"model","agent":"executor","run":"run-1","step":0}

The frame is deliberately metadata-only. Hooks do not receive the current prompt, user message, model response, provider secret, or socket capability. Exit zero continues the run; a non-zero exit becomes a host-owned error frame. The host applies the same agent Linux identity, rejects symlinks, bounds hook count/output/time, and discards hook stdout/stderr after recording only the stable error code. Hook directories are ordinary object-local files and are reloaded at the next runtime/process boundary; no watcher or hot reload is involved.

When an agent run builds its prompt, the object runner also writes session load snapshots (best-effort) under the private agent session directory:

/ctx/home/<uid>/agent/<agent>/session/<session>/AGENTS.md
/ctx/home/<uid>/agent/<agent>/session/<session>/SKILLS.md
AGENTS.md effective merged rules snapshot (same text as {{rules}})
SKILLS.md skill metadata snapshot only (name, description, path)

These files are ordinary observability snapshots, not control or authority files. Full skill content is not inlined; the agent opens listed SKILL.md paths when needed. Snapshot writes must not fail the model run.

Design Tests​

The runtime design is healthy when all of these are true:

agent.sh contains no protocol implementation beyond resolving ctx and execing ctx agent
ctx agent chat is the default human chat UI
ctx agent watch is the read-only human path into ctxterm -> tsh
ctx agent attach is the writable human path into ctxterm -> tsh
tsh never falls back to host PATH
default terminal cwd is /workspace
default terminal HOME is /home/agent
authorized rw /workspace keeps ordinary Git metadata unless path policy narrows it
service/provider secrets are not inherited by executable agents
prompt text cannot grant tool, model, filesystem, network, or session authority