Tools¶
Installation registers the complete built-in catalog, including schemas,
prompts, groups, authority callbacks, and renderers. Execution, patch, and web
registration live in mevedel-tool-*-registration.el; their handlers and
callbacks autoload the implementation on first use. Filesystem registration
likewise leaves Read and search implementations unloaded. Restoring a saved
tool reference uses the same registrar mapping as discovery. Shared editing
loads its host implementation when an editing entry point is called.
Model-facing descriptions and manuals¶
Native tool schemas own parameter names, types, required fields, and basic constraints. Descriptions retain suitability, ordinary usage, essential limits, and failure behavior in When to use / When NOT to use / How to use sections. Keep worked examples that clarify parameter combinations, result interpretation, recovery, or consequential mistakes. Examples may repeat a contract when the concrete case makes it easier to use. Improve or omit examples that add no useful information; neither an example nor a good/bad pair is required for every tool. Avoid repeating a workflow policy across tools; suitability should explain this tool's actual behavior.
Bash requires both command and a short human-readable description. The same
required-field schema feeds native provider tools and nested ToolCall validation;
an omitted label is refused before execution rather than left to prompt advice.
Tools with meaningful advanced behavior point to packaged Markdown and name
the cases that need it. Read, Glob, and Grep share
tools/files.md; ApplyPatch uses
tools/applypatch.md; Bash uses
tools/execution.md; ToolCall uses
ptc-dialect.md. Models retrieve these through Read at stable
mevedel:// addresses, with no mandatory full-manual load for ordinary calls.
Simple tools do not need manuals. Keep consequential prerequisites, authority
boundaries, irreversible effects, and ordinary grammar visible in descriptions.
Every built-in role exposing these tools also has Read. Manual retrieval uses
the installed docs/ tree, including docs/tools/ in Eask packages, and needs
no source checkout or session-specific address. Missing guidance is an ordinary
Read failure; use only the known contract or report the missing manual. A
manual read adds later result context without rewriting the tool description.
Update manuals with the implementation, and retrieve needed detail again after
context loss or a manual change. Static manuals do not list dynamic capabilities:
ToolSearch delivers current contracts; the dialect manual lists pure operations.
Shared editing¶
Session-owned collaborative whiteboards and documents are read with Read and
Grep at shared:// addresses: one hashed line per element or block, full
elements, board renderings, discussions, history and the host's element
libraries. Oversized overview entries point to their full element instead of
truncating its text. Full elements, long discussions and contribution lists
fit Read's physical-line bound; self-described string-chunks(...) values
reconstruct by decoding and joining their JSON string lines before editing.
SharedCreate and SharedEdit change them through the normal
permission pipeline, naming each target by the hash the model read.
Whiteboard targets are Excalidraw elements; SharedEdit also inserts library
items, adds an image from a file or readable resource, and sets a board's
canvas colour; SharedSaveImage writes a shared image to a new file. An
atomic commit and its result delivery defer cancellation
settlement until the actual durable outcome is known. See
shared editing for schemas, reversion, and host ownership.
Tool pipeline¶
mevedel-tool-registry.el owns registration, schemas, and conversion of gptel's
positional arguments to handler plists. Registration loads gptel's tool API;
the registered callbacks load the execution pipeline on their first call.
All tools share one execution pipeline. Provider calls enter through
mevedel-pipeline-run-tool; nested programmatic calls use
mevedel-pipeline-run-tool-outcome:
flowchart TD
Raw[Raw model arguments] --> Repair[Schema-directed repair]
Repair --> Validate[Final validation and PreToolUse hooks]
Validate --> Resolve[Normalize paths and prepare resources]
Resolve --> Permission{Permission}
Permission -->|Deny| Refuse[Return refusal]
Permission -->|Ask| Review[Permission hooks and user interaction]
Review -->|Denied| Refuse
Review -->|Approved| Capture
Permission -->|Allow| Capture[Capture mutation coverage and snapshots]
Capture --> Execute[Run handler and render transform]
Execute --> Post[PostToolUse or failure hooks]
Post --> Outcome[Canonical structured outcome]
Outcome --> Nested[Nested ToolCall consumer]
Outcome --> Provider[Provider projection and result persistence]
This shows the common execution path and its two output consumers. Validation, resource preparation, hooks, or the handler can terminate a call with a failure; permission denial cannot reach mutation capture or the handler. Permission hooks and user decisions are detailed in Permissions. Provider projection adds hook context and repair feedback, persists oversized output when declared, queues any Goal-budget warning, then attaches render data and media. It does not feed that presentation back into a nested ToolCall.
Bash checks session mutation authority at managed execution admission, using the analyzed command after hook rewrites. The pipeline does not apply Bash's static mutating-tool flag to initialize session storage before permission: read-only inspections and denied Plan commands must not modify repository metadata as a side effect of that initialization. Mutating Bash still requires the owning execution boundary's authority check before launch. Read-only Bash reuses already initialized artifact storage without updating Git exclusions; an unsaved session uses the execution owner's temporary spool. Oversized provider projections likewise use only existing storage or truncate the presentation; they do not initialize an unsaved session. Complete execution output remains available through the managed spool while that execution is retained.
Synchronous handlers receive (args) and asynchronous handlers receive
(callback args), where args is a keyword plist. The
pipeline sequences the standard cross-cutting steps; handlers contain no
boilerplate for validation, hooks, permissions, snapshots, or
persistence.
Interactive pipelines yield between steps for pending input, their elapsed-time budget, or after eight nested runner frames. The stack bound also applies across nested tool pipelines, leaving room for result rendering and gptel transitions. A deferred step waits for the preceding pipeline stack to unwind, including when its timer becomes due inside a process wait. Cancellation still owns the pending step. Batch callers retain synchronous chaining unless transport requires deferral.
The structured outcome boundary is not a second pipeline. It captures status, canonical result, raw result, render-data, media, tool-use identity, parent identity, and call source after common execution. Provider-only projection text never becomes input to a ToolCall guest. Text-only metadata stripping preserves non-string result values, including nil, for hooks and nested callers. Provider projection uses gptel's normal text conversion when attaching display metadata, so numeric and other non-text results retain their underlying tool renderer. The call returns a cancellation thunk that settles its current pipeline continuation exactly once; already-started tool-specific effects remain cancellable only when their owner exposes that capability.
Accepted automatic approvals enter the common outcome as render-data
:permission-approvals: a list of (:source reviewer :outcome allow-once :reason
STRING) records. Shared permission settlement alone produces these display
facts, after final validation. Canonical settlement and provider attachment
replace handler/transform-authored approval fields with those accepted records,
including when a later pipeline step fails. The ordinary render-data provider
adapter strips them from API messages; they never enter ToolCall guest values.
mevedel-pipeline-tool-event-hook publishes started, permission-approved and
settled events with the same tool-use ID, parent ID, source, name, args and data
buffer. Pending-row consumers bind identity at dispatch, not by matching a later
approval's arguments. Settlement precedes the external result callback. Each
observer receives a copied snapshot, and observer errors cannot change tool
settlement. mevedel-pipeline-active-permission-approvals exposes a copy while a
handler starts or resumes so execution admission can retain its own provenance;
it is never permission authority. Nested PTC progress and audit keep approvals
on the child, not the envelope, and retain small approval records even when
oversized optional child details are dropped.
External providers can request :projection provider on the structured outcome
entry point. This keeps typed status, tool identity and media while applying the
same persistence, feedback and display side channels as direct gptel calls.
mevedel-mcp-tools uses that projection after verifying that the captured root
request or retained-child invocation still owns its buffer. Child ownership
requires the same running invocation and root session, with no borrowed root
request. Settled, replaced and boundary-stopped owners cannot admit another
call. Child Plan restrictions still apply through the ordinary permission
pipeline. Its MCP result removes trusted display metadata and
includes captured media blocks; the separate outcome retains canonical display
data. Pipeline context carries the owning engine, so Goal tools and nested
ToolCall retain request authority with either a real gptel FSM or an external
request. The private asynchronous MCP transport owns connection cleanup and
pending call and hook-control cancellation. Every server socket lives under one
owner-only directory, $XDG_RUNTIME_DIR/mevedel-mcp-UID (else under
temporary-file-directory), fixed for the Emacs session once first created;
local Bubblewrap confinement mounts an empty tmpfs over it, so confined
children cannot call tools with a turn's authority. When that directory cannot
be created no server can exist, and confinement runs without the mask.
Internal failures answer the request's own id; blank lines and JSON-RPC
responses get no reply.
Retained invocations expose their native FSM context or their own external context through the shared engine accessor. Their ACP driver uses the same transport and projection runner, while the agent runtime owns child terminal publication, independently of root-request settlement.
mevedel-acp-turn binds that endpoint to one admitted request, publishes ACP text
and MCP results through the canonical gptel renderers, and shares final patch
capture and terminal settlement. Native Claude PreToolUse, PostToolBatch
and SessionStart(compact) hooks query the same private endpoint for context
and stop decisions; they are absent from tool discovery. Hook handlers complete
asynchronously. Event, hook and tool-admission ordering and cancellation follow
ADR 0123. A process exit warns
with the agent's last stderr output and appends it to the private
agent-stderr.log under mevedel-user-dir, which keeps recent exits after the
warning buffer is dismissed; the turn's error names that log, not the text.
Native tool calls run the data buffer's gptel-pre-tool-call-functions and
gptel-post-tool-call-functions (tool repair already ran in the pipeline), so
views, collaboration rooms and agent activity observe them as gptel calls.
Pre-hook argument replacements pass through normal validation and permissions;
synthetic results skip execution. Post-hook replacements and blocks update both
the MCP result and displayed transcript, dropping withheld media. A stop from
either hook ends the turn before another call; a post-hook stop retains the
completed call's evidence. Hook changes are passed to subsequent observers.
ACP cannot apply gptel's separate :confirm UI or rename a native tool call;
hooks requesting either stop before execution with an actionable error. Use
mevedel's permission rules or the gptel engine for those controls.
The Claude Code provider routes ordinary root submissions through
this runner. Its usage accounting is described under
Goals; duplicate terminal replies
cannot charge or publish the turn again.
Handler-owned cleanup registers before the outer pipeline canceller, so a compound async tool can cancel and audit its active children before its own provider-facing result settles.
Tool-call identity in transcripts¶
Mevedel renders completed calls from gptel's authoritative :tool-use records,
which contain the original ID, final arguments and result. For each record, the
adapter supplies gptel's existing renderer with that single call and its tool
specification. It restores the complete request call list even if insertion
fails and keeps the normal insertion markers, result-inclusion settings and
callback contract. This applies to main and agent conversation buffers carrying
a mevedel session; unrelated gptel conversations keep their own rendering path.
Results appear in the model's call order, independently of asynchronous completion order. Distinct invocations with identical arguments or results retain their own IDs. The callback's tool specification is located by name; call identity, arguments and output always come from the authoritative record, never a name/argument guess or text inside the result.
The adapter retains gptel's callback format and normal transcript storage. Its rationale and the prior ID-assignment failure are recorded in ADR 0115.
Retained provider history¶
mevedel-history.el records the provider message fragment for each fully
rendered tool response and its results. It records neither the system prompt nor
the native schemas, transport headers or keys. This is a fragment per completed
response, not a growing full-conversation snapshot. It duplicates response
content in local storage so reconstruction can preserve grouping, preambles,
result whitespace and opaque fields such as reasoning signatures.
A fragment has a provider/endpoint fingerprint, model and cache policy, paired trusted boundaries, and a digest of the covered rendered text and meaningful role boundaries. Replay requires the same provider/model/policy and complete unchanged source. Prompt preparation maps verified spans through gptel's Org conversion; a second digest includes role properties so later text edits or filtering invalidate replay. Context selection must retain both boundaries. Omitted results are never recovered from hidden metadata. Model or provider switches use the new backend's normal serialization.
The adapter applies only to mevedel conversations and their prepared prompt copies. Other gptel buffers retain their normal behavior. Decoded reminders keep separate message boundaries, and the backend's current explicit cache annotation is applied to the assembled messages. Stable reconstruction enables cache reuse; it cannot guarantee that a provider retains or serves a cache entry.
The view continues to render the normal transcript and grouped reminder rows; provider fragments stay hidden. Compaction uses displayed evidence and decoded reminder bodies, excludes encoded provider metadata, and removes records with retired history. Source edits, partial selection, compaction and deliberate provider/model/policy changes can still change the cached prefix. History without complete fragments uses normal backend serialization.
Programmatic Tool Calling¶
ToolCall runs one fresh orchestration script in the closed machine
documented by its static description and on-demand dialect manual. mevedel-tool-ptc.el owns
that description, the request roster, registration, and aggregate rendering;
mevedel-ptc-driver.el owns nested pipeline orchestration and exposes one
execution entry to the tool adapter; mevedel-ptc-interpreter.el remains the
pipeline-independent guest machine. Nested calls retain the normal validation,
hook, permission, snapshot, cancellation, and telemetry behavior. They are
shown as one live aggregate row and one settled ordered audit, not as invented
provider transcript messages. Child identities use ToolCall-ID/N and source
ptc; those facts reach hooks, permission logs, pipeline telemetry, and the
child audit. The live row reports active tools, terminal completion counts,
failures, denials, and permission waits. In composed expressions, child outputs that fit the guest value
budget stay in the user-visible settled audit but are never copied into
provider history. An oversized child value becomes a bounded guest error before
the audit or its checkpoint retains it. The cumulative retained-value budget is
also charged before parallel results enter either durable surface.
The ToolCall envelope is available to the root session and to retained agents; every built-in role declares it. An agent-run script differs in one way: it never writes the durable envelope checkpoint, because an agent buffer resolves to the parent session, whose checkpoint recovery reconciles into the root transcript. After a restart an interrupted agent script settles through the agent's own interrupted-turn handling instead.
A syntactically direct expression renders using the underlying tool's normal
renderer and status, while retaining the ToolCall envelope and child IDs in the
transcript audit. Collapsed activity summaries use the same settled specialist
name as the row: a direct Eval contributes “evaluated 1 form”, not “ran 1
script”. The cached presentation name is separate from the canonical ToolCall
child record, so expansion and redraw preserve source and envelope identity.
Live/composed programs, malformed audits and failed envelopes retain ToolCall
presentation. The redundant direct-call text result in that audit is a
bounded preview; the outer result remains the displayed result. Non-text values,
arguments, render data, IDs, and supported media retain their ordinary direct-call
semantics. Its result and supported media reach the model unchanged
apart from ordinary output limits and pipeline guidance. Ask, Skill, WaitAgent
and UpdateGoal are standalone-only; unclassified wrapped tools default to that
route. mevedel-ptc-composable-tools admits additional names for composition.
A program that transforms one child result still renders as a script.
Emacs and collaboration projection share the same direct-call selection rule.
Browser records retain the outer execution name and ID for pending/settled
reconciliation and carry a separate, bounded presentation tree. Registered
tool renderers supply its headers, bodies, statuses, children and parallel
batches. Skill display metadata retains the prepared dependency bodies from
invocation; browsers show each in a separate collapsed foldout, without
rereading files or exposing dependency reminder wrappers as the root body.
Display metadata remains excluded from provider messages.
The settled envelope's own body carries only what the script returned. On a
successful completion, a returned value longer than
mevedel-tool-ptc-result-collapse-line-threshold lines moves into a trailing,
initially collapsed Returned row; setting the threshold to zero keeps every
value inline. Each nested call becomes its own collapsible row rendered by that
tool's registered renderer, so a nested Grep row gets Grep's header and
grep-mode body rather than one flat dump fontified in a single mode. Rows
exist only while the envelope is expanded, failed rows start collapsed, and a
nested compound call expands into its own child rows one level deeper. The calls
of one parallel or parallel-map join share a batch identity and are drawn
as a bracketed group, so a concurrent fan-out is distinguishable from the same
calls made in sequence; a one-call join is not concurrency and is not marked.
Retained per-child argument strings are truncated; argument aggregates that are
too large or too deeply nested, and oversized per-child render data, are dropped
whole. The transcript therefore never grows a second full copy of a child result.
The dialect supports sequential dependencies plus bounded parallel and
parallel-map joins. The latter accept only one direct tool call per entry;
the host concurrency cap is mevedel-ptc-parallelism, and results retain input
order. A denial aborts the envelope with a bounded partial-work summary.
Ordinary failures remain values the guest can inspect. In-flight interpreter
state is ephemeral: cancellation or restart interrupts it and it is never
resumed from session storage. Before child effects, the session sidecar records
the envelope. Child audit changes are journaled in memory and become durable on
an unrelated autosave or the settlement write, avoiding one full publication
per child. The provider pipeline commits the settlement checkpoint after result
projection: an oversized result is stored once as a tool artifact, and the
checkpoint contains its bounded preview and resource reference. That marker
commits both before delivery. Post-use hooks still see the full result before
projection. If the pipeline is interrupted before that commit, recovery uses
the last running checkpoint. Structured outcome-only callers checkpoint their
unprojected result at their own completion boundary.
A restart reconstructs one interrupted ToolCall row from the last
durable checkpoint and consumes it with the repaired segment. Synchronous child callbacks are admitted
in bounded timer turns so one batch cannot monopolize Emacs. Provider
projection records whether the envelope output was inline, truncated, or
persisted, plus its original character count. A ptc-script telemetry span
records only the outcome, budget category, nested-call count, and duration;
script text, arguments, and results are never included. This span ends with
guest execution, before provider projection and final checkpoint publication;
tool-finished records the pipeline's final outcome. See
0111-run-programmatic-tool-calls-in-a-closed-machine.md.
Request teardown cancels the currently active pipeline step. The tool callback
then receives one canonical error result, tool-finished records one error,
and the open step span records one cancelled terminal outcome. A late async
continuation from the cancelled primitive is ignored.
Interactive pipeline chains yield between steps after 20 ms of elapsed work or when input is pending, using an ordinary timer so Emacs can process input before the next step. A step never runs inside a remote command: a chain that a process sentinel starts or continues while another TRAMP operation is in flight yields until that operation returns, through a transport-held timer that TRAMP's suspended timer list cannot discard. A due timer checks input again and waits while keystrokes are pending, since GC or another callback may have used up its original delay. A single step may take longer. The pending step retains its cancellation handler and native error boundary; cancelling removes its timer before side effects run. A queued successor also checks its cancellation ownership before running, so a predecessor's late error cannot leave pending side effects. Short chains without pending input and batch callers keep synchronous chaining. This does not move tool execution into a Lisp thread.
Tool-result media has one focused boundary in mevedel-tool-media.el.
It validates and sanitizes captured media records, stores their bytes behind
opaque transcript references, restores trusted records during replay, removes
validated media from hook-visible text, preserves marker-shaped ordinary text,
and converts restored records into each provider's native payload shape.
mevedel-pipeline.el supplies the session's
tool-results directory and calls that boundary from the attach, hook, render,
and gptel parse steps; it does not construct provider-specific media blocks.
The transcript reference contains only an opaque record id and its owning tool
use id. Native delivery follows that captured provenance, regardless of the
provider-facing tool name, so direct ToolCall dispatch and editing tools retain
their images. Replay never rereads the original filesystem path. Remote records are
published and replayed through the session artifact manifest; a fixed-path
cache is never an authority fallback. In-memory retention is bounded by
mevedel-tool-media-cache-max-bytes (default 25 MiB): the oldest records are
dropped first and the newest is always kept. Durable records are reread from
their published copy after eviction; media captured without durable storage is
unavailable once evicted.
mevedel-tool-render-data.el owns render-data serialization, provider
scrubbing, transcript mutation, and stale execution reconciliation. The
Pipeline owns only the render-transform and final attachment steps and their
ordering.
Important tool metadata:
- Behavior:
:read-only-p,:snapshot-p,:destructive-p,:async-p - Permissions:
:check-permission,:check-permission-async,:get-path,:get-paths,:get-pattern,:get-domain,:get-name - Loading/grouping:
:category,:groups,:wrap,:prompt-file - Input contracts:
:args - Display/output:
:summary,:max-result-size,:render-transform,:renderer
Native and wrapped registrations preserve the same permission metadata.
mevedel-define-tool rejects unrecognized keywords during macro expansion.
Every registration builds its own provider schema. Per-request updates to a
registered schema, such as the Agent tool's role and model enums, therefore
never carry into a later registration, and the authored :args stay as
declared.
:snapshot-p is an explicit declaration for file-mutating tools whose
before-state participates in the final patch. ApplyPatch declares it and
uses :get-paths so permission and snapshot steps cover every affected path.
A path-or-resource argument may declare :access read, for a tool that
mutates something else and only reads the file or resource that argument
names. Its plain path is canonicalized and authorized as a read, so a mutating
tool's source file needs the same authority as Read, not write authority. A
resource address is prepared with the read operation and reaches the handler
as the same authorized attempt Read receives. The key is not part of the
provider schema. SharedEdit's source is the one such argument.
During directive implementation, the pipeline also records conservative untracked-effect markers for non-read-only execution tools and agent dispatch. Those markers do not attempt to infer changed paths; they prevent the final attempt capture from claiming completeness and become explicit Rewind gaps.
Tool input validation and repair¶
mevedel-tool-repair.el mediates raw model calls before gptel dispatches
them into the pipeline. The temporary provider bridge lives in
mevedel-tool-repair-gptel.el; audit and telemetry live in
mevedel-tool-repair-diagnostics.el. The core first validates the call unchanged. Valid input is
never rewritten. Only invalid model-produced input gets one atomic repair
attempt; the pipeline then validates the committed arguments again before
hooks or permissions run. Direct programmatic calls and arguments rewritten
by PreToolUse remain validation-only.
While gptel decodes provider responses, mevedel preserves JSON null as a
distinct sentinel. Before pre-tool hooks it restores decoded empty objects in
the common tool-call representation. This temporary adapter covers gptel's
tool-capable backends in one place to preserve the distinctions required by schema validation.
The generic repair catalogue is deliberately small and ordered:
- omit explicit
nullfrom optional properties; - parse exact JSON strings when the parsed value satisfies the expected non-string contract;
- wrap a schema-valid singleton where an array is expected;
- replace an empty object placeholder with an empty array only for optional arrays that permit zero items;
- unwrap an exact Markdown HTTP(S) auto-link in the final component of a semantic filesystem path;
- clamp a number to the
:minimum/:maximumbounds its argument declares in the tool arg DSL (the same bounds the provider schema advertises).
Repairs never invent required values and do not coerce arbitrary strings to
numbers or booleans: the JSON parser must consume the exact input and the
result must validate, tolerating only range issues the clamp rule fixes in a
later step of the same pass. Clamping is not invention: the bound is declared
schema data, the repair is deterministic, and it is always reported in the
corrective note and telemetry. WriteStdin advertises the union of its input and poll ranges;
its chars-dependent bounds remain handler policy and requested-versus-
effective telemetry.
Required null and required empty-object placeholders
therefore remain invalid. Generic repairs run as one bounded, ordered pass.
The entire candidate is committed only when final validation succeeds;
otherwise the model gets bounded, value-free retry guidance and no tentative
arguments run.
path is an internal semantic argument type for filesystem-only contracts.
Provider schemas lower it to an ordinary JSON string and append the guidance
“Pass a raw filesystem path, not Markdown or a URL.” Tools that also accept
resource addresses use a separate path-or-resource contract, so a recognized
scheme:// address is not rejected as a web URL and ordinary paths retain
their current behavior.
Code-navigation tools keep filesystem access in Emacs buffers, so Imenu and Tree-sitter operate on remote files through the active file handler. Remote Xref is capability-scoped: Emacs Lisp reference search is the currently tested TRAMP-aware path, while definition lookup and other backends return a direct unsupported-backend diagnostic instead of invoking client-side programs. Location results are rendered as target-native paths. Emacs Lisp reference searches use the recognized project, or the file's directory when no project is recognized. Searching does not register a project or require writing Emacs's remembered-project list.
Code navigation answers the location it was asked for or reports why it
cannot. A Treesitter line or column the file does not have is an error, never
the nearest position that does exist; the buffer is widened first, and the
column is an Emacs display column, so a tab counts as the width it displays.
Read and Grep pagination follows the same policy: a negative offset,
limit, or head_limit is an error, a Read range starting past the last
line is an error naming the offset, and a Grep offset past the last output
line returns an error about the range instead of returning an empty success
indistinguishable from no matches.
Imenu descends every nesting level and prefixes each leaf with its category
path, so class- and namespace-shaped indexes are listed to their full depth,
with whole-buffer line numbers even when the visiting buffer is narrowed. Only
Imenu's own special entries are skipped, identified by the negative position
that marks them rather than by their name, so an ordinary symbol whose name
starts with * is listed. Whole-file Treesitter traversal has a construction
limit well above the tool's result limit: it stops there and says the tree was
truncated, so a wide or generated file cannot spend unbounded time and memory
on a tree nothing can consume.
Committed repairs proceed without a retry and add one corrective note to the final tool result, including error results. If a multi-step candidate still fails validation, its repair audit is marked abandoned and the handler is not called. Both audit states contain only rule IDs, schema paths, and before/after shape names.
Repairs, validation failures, and abandoned candidates record a redacted event
on their root session with the actual backend, model, tool, canonical origin
(/root or an agent path), outcome
(valid, repaired, invalid, or abandoned), rule IDs, schema paths,
execution state, and result classification. Argument values, paths, commands,
prompts, schemas, validation messages, and results are excluded. The in-memory
and persistent repair logs omit unchanged valid inputs, even during profiling.
The in-memory mevedel-session-repair-log is bounded by
mevedel-tool-repair-log-limit (default 200). When
mevedel-tool-repair-persist-log is non-nil, materialized sessions also append
events to <session>/repair-log.el in UTF-8 without rereading prior entries or
checking an already-existing parent directory; bounded events recorded before first
materialization are backfilled when the session directory is created.
Remote callback entries wait for session settlement and append only their
queued delta. A crash may tear the final line; failed appends warn, remain
queued, and retry after the next successful session save. Telemetry failures
never block tool execution.
mevedel-tool-input-repair-enabled disables mutation while retaining
validation and telemetry.
mevedel-define-tool :wrap SOURCE freezes the source argument schema, order,
and async calling convention exposed to the provider. Each call resolves the
current source with gptel-get-tool, so a reconnect can replace its function
without rewrapping when that contract is unchanged. Contract drift fails the
call and requires rewrapping. Re-registering the same wrapped (category,
name) replaces the prior mevedel wrapper, matching native tool registration.
The Emacs introspection and web tools are native mevedel tools. Introspection
uses Emacs source, documentation and Info APIs, with Orderless symbol matching.
Source lookup for C primitives and variables requires a configured
find-function-C-source-directory; missing configuration returns a tool error
instead of opening an interactive directory prompt.
There are three tools. EmacsDescribe reads one function's, variable's or
library's documentation, source, location, Info manual section or global value;
an omitted kind is inferred as a defined function, else a variable, else a
library for source or location, and an unknown name returns a not-defined
message without interning it. A library's location is its first file in local
load-path directories, preferring source, plus whether its feature is loaded.
EmacsFind only searches names: functions, commands, variables, or libraries in
local load-path directories, matching an Orderless pattern. EmacsManual lists
Info manuals, a manual's nodes, or one node's contents. Node reads use a
temporary, undisplayed Info buffer, leaving the user's Info buffer and window
layout alone even when a live chat redraws during navigation. A value read always
asks for permission; library source must resolve inside a local load-path
directory (see
Permissions).
WebSearch queries DuckDuckGo's HTML endpoint, independent of EWW's
eww-search-prefix, with two searches active at a time. It parses
DuckDuckGo's result blocks rather than every link on the page: each result
contributes its title, its destination unwrapped from DuckDuckGo's uddg
redirect parameter and re-encoded as a valid URL, and its snippet. Ads are
skipped, destinations are deduplicated, and at most ten numbered results are
returned. Optional allowed_domains or blocked_domains (not both) add
site: or -site: terms to the query and also filter results by host at a
label boundary, so filtering holds even when DuckDuckGo ignores the terms.
DuckDuckGo's bot challenge page is a tool error, distinct from an empty
result.
WebFetch asks for text/markdown first, so servers that can send markdown do,
and returns a response by its content type. HTML and XHTML are decoded with the
header charset, else a <meta> charset, else UTF-8, and rendered as readable
text with EWW/SHR. Links keep their targets as markdown links, [text](url),
resolved against the final URL through a base element (shr-insert-document
resets shr-base itself). Link text loses its line wrapping and escapes
brackets, and parentheses in targets are percent-encoded, so the view's link
renderer and the model read the same link. Non-http(s) links, links without
text and anchors into the fetched page stay plain text; a link whose text is
its URL becomes the bare URL. Other text (text/*, JSON, XML and their +json/+xml
variants, JavaScript) is decoded the same way and returned verbatim. PNG, JPEG,
GIF and WebP images are attached as media, with a one-line text summary, when
the current model accepts that type, the bytes carry the type's signature and
the image is at most 10 MiB; otherwise the call fails. An outcome-only
ToolCall run, which never attaches media, sees only the summary. PDFs become
the text pdftotext -layout extracts; the bytes go to a private temporary file
for a helper that always runs on this machine, whatever the session's execution
target, under the web timeout. With durable session storage, a PDF of at most
25 MiB is also saved as a WebFetch-*.pdf session artifact, and the result
leads with its artifact:// address, which Read opens as a document or as
rendered pages. The saved copy covers what extraction misses: scans, figures
and layout. A PDF yielding only whitespace says so; a failed extraction, or a
missing pdftotext, still succeeds with the saved address. Without a saved
copy, extraction failure fails the call. Other
content types are tool errors naming the type and size. A response without a
content type is sniffed as PDF, HTML, binary (a NUL byte early on) or text.
When redirects moved the request, the result's first line names the final URL.
WebFetch's renderer header shows the requested host, the final host when it
differs, and the size, status and content type. YouTube URLs retrieve video
descriptions and English captions.
Web retrieval accepts only http and https URLs with a host, both as the
requested URL and as every redirect target, so neither a model-supplied URL nor
a Location header reaches file: or other URL handlers. Redirects are followed
by mevedel rather than by url-http: each request binds url-max-redirections
to 0 and repeats its method, body and headers, as url-http adjusted them for
the redirect. At most ten redirects are followed. A redirect to the same host is
followed. WebFetch decides a redirect to another host, including a www.
variant, with mevedel-tool-permission-decide-now under the dispatching
buffer's policy: a target a fresh WebFetch call could fetch without a prompt is
followed, a denied target is a tool error, and any other target is returned as
a REDIRECT: result naming the URL, so the model's next WebFetch call gets the
ordinary permission check. A redirect from a public host to the local machine
or a private network address is always returned that way. WebSearch and the
YouTube stages contact fixed hosts and follow every redirect. A redirect
without a Location header never completes in url-http and ends in the
timeout.
Every HTTP retrieval has a 30-second timeout, including its redirects. Each YouTube stage gets its own timeout. The retrieval owns its response buffers and timer, releases them before delivery, and delivers success or failure once. Malformed responses, network errors and timeouts return explicit tool failures. A caption failure retains any retrieved video description alongside the transcript error.
Preset application also resolves its tool specs from the current registry.
Reloading a native tool therefore updates the next request's schema and
handler instead of leaving a stale gptel-tool captured by an older preset.
Claude root sends refresh their selected registered preset's tools, extras
and local Agent schema before freezing the native roster. Request-only presets
keep their explicitly applied scope, including raw tool specifications; a
session's broader preset must not replace them.
Resource addresses in filesystem-shaped tools¶
The closed resource resolver accepts the eight documented scheme:// families
without adding a model-facing tool. The operation matrix is deliberately
narrow: Read accepts every family; Glob and Grep accept work://,
artifact://, skill://, memory:// (including memory://journal/),
history://saved/, and mevedel://. Grep also searches the current projected
transcript at history://root and history://root/PATH, with Read-compatible
line references;
ApplyPatch accepts work://, explicit memory file descendants, and ordinary
filesystem paths. Unsupported combinations fail
explicitly. Bare addresses list only when the family defines a discovery
listing. mevedel:// is always available, including without a session, and
exposes only packaged Markdown documentation.
Skill tools accept exact skill://NAME@SOURCE-KEY locators and explicit
origin aliases for local Mevedel, local agents, global Mevedel, global agents,
bundled, managed, and plugin sources. Bare skill listings expose both forms.
An alias resolves to the exact current full-hash locator for authority while
transcripts, errors, and results keep the address the model authored; there is
no unqualified skill://NAME alias.
Resource preparation runs after repair, final validation, PreToolUse, and
hook-rewrite validation, but before permission, snapshots, helper execution,
patch review, or handlers. It returns an opaque resolved attempt and logical
authority facts without reading content. After permission, the handler
executes that attempt without reparsing the authored address. Malformed or
unsupported addresses therefore stop before permission and post-use hooks;
valid but unavailable resources follow ordinary handler failure handling.
Authored addresses remain in model-visible errors, headings, search results,
truncation notices, and persisted tool arguments. Backing paths and helper
roots stay private. Directory-backed resource searches use the existing
confined helper boundary with exact read roots; virtual resources stay
in-process. A mixed local/ordinary ApplyPatch remains one proposal and one
atomic review transaction outside standalone/sticky Plan mode. Standalone/sticky
Plan mode keeps session-only ApplyPatch available, including proposals from
retained agents, but rejects any ordinary, shared, memory, or bare endpoint before
local materialization. Mixed local/ordinary and ordinary-only proposals
therefore fail before either side is touched. Directive Planning remains
strictly read-only and does not allow ApplyPatch, including session-only
proposals, or Eval. See
address-to-resource.md for canonical grammar,
freshness, and lifecycle contracts.
Discovery and stable native tools¶
Tools carry :groups. (:discoverable GROUP) in a preset or agent tool list
adds that group to its ToolSearch/ToolCall catalog. The implementation native
list is Read, Bash, WriteStdin, Monitor, Glob, Grep, ApplyPatch, ToolSearch and
ToolCall. Other roles use the permitted core subset; named roles with Bash also
get native WriteStdin. Every named role gets native SendMessage, and a default
agent gets it natively when its inherited catalog permits it, so reporting to a
parent needs no lookup. Other capabilities, including ListAgents and configured
wrapped tools, remain discoverable.
User extras use mevedel-preset-extra-tool-specs and
mevedel-agent-extra-tool-specs; an explicitly native extra remains native.
Each conversation receives a retained specialist tool listing: every catalog
tool its current role and request may call through ToolCall, excluding native
tools, as a calling shape and summary such as
- (Imenu :file_path string) - Fast outline of .... Bracketed arguments are
optional; a [standalone only] suffix means the call must be the entire
expression. The listing lets the model call a known specialist without searching
first; ToolSearch supplies full contracts when argument meaning or usage rules
matter, and recovers after a failed call. mevedel-tools-listing-section renders
it from the receiving conversation's catalog, so an agent sees its own
specialists. Retained observations use that conversation's admitted engine and
actual native tool set, including current root Goal attribution; an unrelated
callback's engine cannot replace the recipient's context.
It is delivered like the skill catalog: only when it changes, under
a budget of mevedel-tools-listing-budget (default 2%) of the model's context
window. Tools beyond the budget are counted and left to ToolSearch.
Every registered tool has a summary. An omitted :summary defaults to the first
sentence of the tool's description, at most 160 characters. Built-in summaries
describe useful outcomes in ordinary vocabulary; for example, introspection
summaries identify Emacs, and task summaries expose plan/checklist tracking,
ownership, and dependencies. Summaries serve search matching, broad results and
the listing; full prompts are not search indexes.
ToolSearch performs case-insensitive OR matching over names, summaries, categories and groups. One to three matches return all contracts, arguments, signatures and manual references. Broader queries return at most 20 calling shapes with summaries, total/truncation information, and a request to narrow to one or two exact names for full contracts. A group keyword therefore yields callable shapes in one search. An exact name selects only that name for its term. Responses deduplicate identities; repeated searches return contracts again, including after compaction. Queries with no matches suggest at most 20 sorted categories/groups from the current callable catalog. An empty catalog produces no suggestions. This recovery guidance appears only in the failed search result.
Search does not load native schemas. Each session or retained invocation keeps
only a tool-catalog, not pending/loaded/injected lists or a TTL. Search results
are ordinary later transcript content. The retained listing is the only
availability roster; no per-turn availability reminder is prepended to task
history. Calls use the current owner's catalog and request
restrictions; search is not authorization. Wrapped tools use category/name
identities, while built-ins use their short names. Unsupported category
separators and language-name collisions require native exposure.
The native and deferred rosters share Plan, Goal and agent visibility rules;
hidden native tools cannot reappear through the catalog. Native membership
requires the registered implementation, not merely its name. An admitted
request's empty or omitted tool list never falls back to the buffer's wider
native tools; only pre-admission discovery uses the buffer configuration.
Native schema additions can invalidate a cached prefix containing much more than the changed tool. Role/configuration changes can still alter schemas; ToolSearch and ToolCall do not. Provider cache hit guarantees remain separate from payload stability.
Plan filtering retains the shared payload writer and the cache boundary chosen
by gptel's serializer. Anthropic cache_control and Bedrock cachePoint retain
their backend-specific meaning. Discovery no longer invokes this writer.
Interaction tool ownership¶
mevedel-tool-ui.el assembles the user-interaction tool surface and owns the
Agent, InterruptAgent, ListAgents, ToolSearch, SendMessage, and
WaitAgent adapters. Ask's questionnaire lives in mevedel-tool-ask-ui.el; its
handler, renderer, and schema live in mevedel-tool-ask.el. Exact external-path
authority is part of the normal permission pipeline, not a model-visible tool.
Ask displays its questions together in the interaction zone, one of them open
with its options and every other one on a single line, answered ones ending in
→ answer (ADR 0127). Point is
the focused entry: n/p, C-n/C-p, and the arrow keys move between entries
without repainting the form or opening anything; TAB/S-TAB open the next or
previous question, Submit being the last stop; 1–9 focus that option of the
open question, from its heading too.
RET or a click chooses the entry, including when point is on its description,
and opens a closed question on its current answer. c edits a custom answer
in the minibuffer, retaining the previous answer as the initial text. A question
can set multiSelect: true (default false): its options become checkboxes,
RET/click toggles them without leaving the question, and custom text is kept
alongside the selected labels. Editing custom text replaces only that text;
clearing it removes only the custom answer. TAB/S-TAB still navigate questions.
Single-select answers open the next unanswered question, then move to the final
Submit row. In the narrowest-capacity window, the form keeps one height while pending,
the tallest any open question needs, with blank lines above Submit making up
the difference. Rows are counted using each window's own continuation-column
policy, reserving that column when it has no right fringe. A wider window can
still show different heights across questions because its text wraps less;
the shared blank-line budget is not a per-window layout. RET on Submit
or C-c C-c submits the whole questionnaire; unanswered questions report no
preference explicitly. q/C-g cancel only the questionnaire, while C-c C-k
also aborts
the run. The form keeps one progress count beside Submit and one keyboard hint
line, with hanging indentation for wrapped questions and descriptions.
Multi-select answers are listed in offered-option order, followed by at most one
custom answer, and include the samples of every selected sample-bearing option.
An empty selection means no preference, never authorization.
Option labels are trimmed consistently for display, answers and sample matching;
duplicate labels after trimming are rejected before the questionnaire is shown.
A question whose options carry samples ends in a sample panel, rendered as a
source block titled with the option it shows. The panel shows the
sample-bearing option point visited last. Samples are wrapped to the window's
width into rows, continuation rows marked ↪, and the panel's height is the
question's longest sample in rows, capped by mevedel-ask-sample-height, so
comparing options never moves the rest of the form. When the narrowest window
showing the view changes width, the form lays itself out again at once. With
point in an open question, C-v/M-v
page an overflowing sample; otherwise they scroll the window. See
ADR 0113.
Agent's required inputs are task_name and message. Its optional role,
context, model, and effort inputs are validated before reservation:
context accepts all, none, summary, or positive decimal strings; model
selectors use the shared tier/provider parser; and effort support is delegated
to the resolved gptel model. summary freezes the realized parent evidence,
omits the triggering Agent tool segment, and makes one handoff-summary request
focused on the hook-accepted task. A valid request reserves path and capacity
while its ordinary task hooks and optional summary prepare asynchronously. The
path is not published until the labelled background, authoritative task,
durable transcript, and provider dispatch succeed; cancellation or failure
releases the reservation.
The same shared resource-grant interface authorizes native filesystem tools and
additive Bash/batch-Eval mounts; command authorization remains independent.
When one Bash or batch-Eval invocation is missing both operation authority and
additive network or exact-path authority, the pipeline presents one combined
card. Checked capabilities are already granted; unchecked capabilities are the
complete upgrade. Approval grants that complete request to the current
invocation, while denial rejects it without replay or reduced execution. Full
execution escalation uses a separate card because it disables confinement
rather than adding a named capability.
All direct user interactions share the settlement and cancellation primitive in
mevedel-interaction-prompt.el. Domain owners supply their own text, keymaps,
outcome translation, and persistence effects; the shared primitive owns only
overlay identity, exactly-once settlement, request-local cancellation, and the
standard frame. Ask and other child-originated interactions are attributed by
canonical path and rendered only in the root session's interactive view; child
transcript views remain inspection-only. Interrupting one agent request invokes
that request's canceller and leaves sibling interactions queued.
Native Tools Surface¶
The session cockpit t Tools row opens the native *mevedel tools* surface
for the current main session. /tools and /tools list open the same
surface. The buffer is read-only UI chrome, not transcript content.
The tools surface lists native and discoverable tools. It supports contract
search (s), details, and opening the gptel configuration menu.
Tool renderers¶
Individual tools may ship a :renderer FN-OR-ALIST for rich collapsible
views in the view buffer. Function contract:
(lambda (NAME ARGS RESULT RENDER-DATA) -> rendering-plist-or-nil)
Pure function — no I/O, no mutation. Nil falls back to the generic renderer.
The view binds mevedel-tool-render-summary-only while requesting an initially
collapsed row. Renderers may then omit body and child-row construction, while
retaining the same header, status, visibility, grouping and disclosure decisions.
An initially expanded rendering must still supply its body. Expansion and browser
projection request the full rendering. Bash uses this context to defer output
cleanup and command-prefix construction; ToolCall defers returned-value formatting,
line counting and child-row construction. Metadata decoding and status
classification still happen before the collapsed row is shown.
Alist form dispatches on the visible result status:
((success . FN) (error . FN) (default . FN))
The view first uses structured :status from render-data, then falls back to
the visible result: error when mevedel-view--tool-result-error-p matches,
otherwise success. Lookup tries the exact status first, then default, then
the generic renderer. Built-in count and success-summary renderers register
only for success, so a failed search, fetch, or mutation uses the generic
error summary and retains its diagnostic body. Bash and ToolCall retain
their structured failure details. This dispatch also applies to nested calls
and wrapped tools; unregistered tools use the generic renderer.
Explicit pipeline errors override custom visual status. Explicit success
overrides a renderer's inferred error but preserves visual warnings and
running/cancelled lifecycle states. Visual status does not change the
pipeline's success/error outcome or participate in renderer dispatch.
Errors use a red × (error face); warnings use ! with warning highlighting.
Only the marker is emphasized. Expected empty results are successful.
Completed ToolCall programs with handled child failures or warning disclosures
carry a visual warning; failed programs carry an error. Children retain their
own outcomes and start collapsed, including in the browser projection.
Rendering plist: (:header STRING :body STRING :preview-body STRING
:body-mode SYMBOL :status SYMBOL :expandable-p BOOL :hidden-p BOOL
:coalesce-key STRING :child-calls LIST
:initially-collapsed-p BOOL).
:preview-body, :status, :expandable-p, :hidden-p, :coalesce-key,
and :child-calls are optional. :preview-body replaces :body only when
the row renders expanded by default (:initially-collapsed-p nil); an
explicit user expansion re-invokes the renderer and shows the complete
:body. ApplyPatch uses it to open a reviewed patch on its first changes. :child-calls belongs to a compound tool that runs other tools:
each entry is (:id ID :tool NAME :args PLIST :result STRING :status SYMBOL
:batch ID :render-data DATA) and becomes its own collapsible row rendered by
that tool's own renderer, inserted only while the owning block is expanded.
Entries sharing a :batch value ran concurrently and are bracketed by a glyph
gutter; a block whose calls all ran in sequence gets no gutter. When
:expandable-p is nil, the view inserts a compact non-toggleable event line
and ignores :body and :initially-collapsed-p. When :hidden-p is
non-nil, the view inserts nothing. Consecutive visible renderings with equal
coalescing keys retain only the final row and append their call count; any
other visible rendering ends the run. Validated by
mevedel-view--rendering-plist-p.
Well-formed tool segments always render through a registered renderer or the generic fallback. Malformed or unparseable tool segments keep the safe raw-text fallback.
Renderers that remove appended system reminders from their display body must strip only an explicit trailing appended block. Tool output may legitimately contain marker-shaped text, especially Read output with line prefixes, so renderer cleanup should first check for the marker and never treat arbitrary file content as hidden guidance.
Render transforms¶
Wrapped tools may ship a :render-transform FN to synthesize bounded
render metadata from string output:
(lambda (NAME ARGS RESULT) -> render-data-or-nil)
RESULT is the normalized string result before oversized-result
persistence and before render/media side-channel attachment. The
transform runs only when the handler did not already provide
:render-data, only for string results whose pipeline status is not error,
and never changes :result or :raw-result. Transform errors emit a warning
and leave the tool result unchanged.
Transforms must return small metadata, not copies of large result bodies. The pipeline rejects oversized transform metadata so a transform cannot bypass tool-result persistence by hiding the full output in render-data.
Render-data side channel¶
Every handler returns a plist containing :result and may set :status to
success or error. The handler boundary normalizes that optional status
into canonical lifecycle state before post-use hooks run; legacy Error:
results are classified only at that boundary. Invalid returns and handler
signals become canonical errors there as well. Final lifecycle and repair
telemetry retain that same classification regardless of displayed result text.
When a handler
includes :render-data DATA or
explicit status, the pipeline writes :result to the data buffer and appends a
hidden block wrapped in <!-- mevedel-render-data --> delimiters, propertized
'gptel 'mevedel-render-data and 'invisible t. Parser:
mevedel-tool-render-data-extract.
Tool-result blocks carry the owning tool-use ID. Provider scrubbing, view
extraction, and live metadata updates accept only the block whose owner matches
the surrounding tool call; other valid marker-shaped blocks remain literal
result text. Non-tool render records are unbound and are parsed only by their
dedicated non-tool paths when the complete source span carries live producer
provenance or the persisted gptel=mevedel-render-data property;
delimiter-shaped user, assistant, and reasoning text remains ordinary
transcript content.
The codec reads one complete Lisp object before checking whitespace and the
closing delimiter. Delimiter-shaped strings and quoted transcript/tool marker
lines inside that object remain payload data, even when they contain a complete
nested marker-shaped block. Buffer consumers share this decoder and advance
over the full block without copying the transcript. Reader evaluation is disabled.
The payload is exactly one proper keyword plist. Marker-looking text with a
non-plist payload, trailing Lisp data, or unreadable data is ordinary visible
tool output and is preserved verbatim. Handler envelopes are validated at the
pipeline boundary as well: a non-nil :render-data value must already be a
proper, even keyword plist. Malformed renderer metadata becomes a canonical
tool error instead of reaching transcript serialization.
Child-process settlement may add a model-hidden :sandbox-summary to this
same payload. It contains only logical attempt/start/refusal counts, boundary
symbols, and aggregate additional read/write mount counts. Default confined
execution and additional read-only mounts are omitted. Paths, commands, and
raw launcher reasons are never copied into the summary.
Execution terminal updates that arrive before their tool rows are retained without eviction and reconciled immediately after gptel inserts each result batch, independently of view rendering or turn completion. Compaction also reconciles pending updates before freezing removed rows. Numbered ToolCall children update their exact nested metadata inside the enclosing tool row, not a nonexistent standalone segment. If a retained agent's sandbox summary changes after its parent row was compacted, reconciliation publishes that row's receiving-transcript archive; it does not publish the live draft or search a different agent's history. Unchanged archived updates use a source-identity-checked cache to avoid repeated reads and writes. Failed publication remains pending and reports the failure rather than discarding it.
Tool renderer input is derived from the data buffer on each rerender; it
must not depend on durable state stored only in view overlays or text
properties. View-local fragment metadata, collapse state, and renderer caches
are disposable UI state.
mevedel-view--invoke-renderer condition-cases the call; malformed
output emits a warning and falls through to the one-liner.
Wrapped tools (gptel/MCP) have render-data = nil unless they declare a
:render-transform; their renderer can use transform metadata when
present or parse the result string directly.
Agent tool calls and direct asynchronous workflows use :kind
collaboration-event render-data. A started event renders the retained
transcript handle; the registry-backed aggregate status uses distinct
Running, Waiting, and Blocked rows. Canonical tool and lifecycle events
are the only sources for Started PATH,
SendMessage: PATH, InterruptAgent: PATH, and Waiting for agents.
Settled WaitAgent calls render WaitAgent: agents (OUTCOME); consecutive
waits coalesce into the final row with a count. SendMessage: PATH starts
collapsed and expands to its exact message. Its :delivery render-data field
selects message queued, started, or waiting for a slot in the header.
Render-data lookup/patching searches opening delimiters and decodes the complete
payload rather than stopping at the first closing marker or matching the whole
hidden block with one regexp; live agent metadata
and multiline payloads can be large enough to overflow Emacs regexp
limits. ApplyPatch uses :kind patch render-data for one persisted aggregate
whose body contains structured per-file diff blocks.
Tool result persistence¶
Message normalization checks valid Unicode with a single string scan and returns valid text unchanged, including its properties. Raw byte characters, surrogate code points and values above the Unicode range still use the repair path. This avoids allocating one Lisp object per character on each pass through large tool results; normalization still happens before durable output and previews are made.
When :max-result-size is set and result exceeds the effective limit
(min of tool value and 50,000-char global cap), the full result is saved
to the owning session's tool-results/ directory and replaced with a preview wrapped in
<persisted-output> XML. The LLM can Read the file to see the full
output, and the notice provides a followable artifact:// address plus exact
bounded Read continuation and Grep recovery calls. Grep accepts an
explicit artifact address; absolute session-storage paths remain internal.
When persistence is unavailable, the notice says the omitted text is
unavailable and asks for a narrower rerun. Oversized error results are
truncated but not persisted according to
the canonical status produced at the handler boundary. Every
oversized preview keeps equal head and tail budgets, prefers nearby newline
boundaries, and reports the exact omitted character count. The persisted file
remains complete. Bash and Eval do not apply an earlier prefix-only cap. Ephemeral requests and calls without durable session storage cannot persist
output.
Per-tool character limits are: Grep 20k, Bash/Eval 30k,
Glob 30k, Ask 30k, Xref*/Imenu 20k, Treesitter 30k,
WebFetch 50k. Read/ApplyPatch: nil (self-bounded or short). Agent
RESULT mailbox records inline at most a 32,768-character preview of the final response;
the retained agent resource keeps the complete latest settled payload and
terminal outcome.
External helper confinement¶
Native tool implementations launch short-lived external helpers through the
mevedel-execution.el facade, backed by the same opaque process owner used by
Bash and batch Eval.
The external-helper execution boundary returns an idempotent cancellation
function. Sessionless request owners can stop their own helper children while
preserving normal terminal delivery and cleanup. Glob/Grep return this handle
when they launch a helper; synchronous no-match paths settle directly.
Callers continue from the helper's callback instead of waiting for it. A synchronous wait re-entered the event loop wherever it was called. That includes gptel's process callbacks, which run with quitting inhibited, so a helper that never settled wedged the turn. The affected callers are:
- Read: PDF page counts, page rendering, image transforms, and directory
listings. It delivers its result once the helpers settle. The pipeline's
handler context and Read's own state are restored for each continuation.
A failure after a helper becomes the ordinary
Error:tool result. Temporary local copies of remote media live until the Read settles. - Final patch: a turn's final patch runs one
diffper touched file while the turn's settlement hold stays in place. - Edited-file reminder: its diffs are prepared before a request is dispatched. A cancelled request ends a dispatch still waiting on them. Reminder evaluation outside that dispatch reports the change without a diff.
Mention expansion still waits synchronously for its directory listing and PDF page count.
The caller supplies a structured argv, authorized read paths, and explicit
writable artifact directories. The facade adds a private scratch working
directory, applies mevedel-sandbox-mode, and removes the scratch directory
after the callback. The process owner handles timeout/process-group cleanup
and streams output into a bounded temporary disk spool rather than an Emacs
process buffer. The one-shot terminal result contains the captured output and
structured exit, timeout, output-limit, byte, and wall-time facts. In
best-effort, an unavailable initial confinement probe permits disclosed direct
execution. Once confined preparation begins, preparation and launcher failures
return without retrying the helper. required refuses an unavailable backend
and off runs directly.
Local Linux protected-directory scans yield through owned native find children
before managed Bash or a one-shot helper launches. The execution scheduler keeps
its admission slot during preparation. Cancellation and owner teardown stop the
discovery child; changed authority, a dead source buffer or replaced discovery
root refuses launch. Results belong only to that attempt. Path resolution and
mount planning still run in the editor; custom glob walks and remote discovery
retain their synchronous path. Discovery has a 30-second child timeout and the
ordinary output bound; a failed or incomplete scan never permits direct execution.
Bubblewrap capability probes are cached independently per execution target.
Local probes use the short mevedel-sandbox-probe-timeout; remote probes use
mevedel-sandbox-remote-probe-timeout (10 seconds by default) so transport
latency does not silently disable best-effort confinement.
All operating-system children receive deterministic defaults for UTF-8 locale,
no color, terminal mode, and pagers, plus MEVEDEL_EXECUTION=1. An invocation
can still override these variables inside its own command. Ordinary one-shot
stdin is closed immediately. For local Unix execution, a normal main-process
exit drains any remaining process-group descendants through the same bounded
TERM/KILL path before the one-shot callback runs. Local Windows execution
remains limited to the direct child; remote execution retains the target-side
wrapper behavior described below. Remote direct-async channel overrides are
scoped to the individual spawn and never change the user's global TRAMP
connection properties.
mevedel-tool-fs.el owns registration and the shared path/resource result
primitives. mevedel-tool-fs-read.el owns text/media decoding and bounded Read
output; mevedel-tool-fs-search.el owns Glob/Grep execution and private
resource-output rewriting. Diff generation belongs to the shared Utilities
owner, while pre-turn file snapshots belong to Pipeline.
The current external-helper inventory is diff; rg for Read directory
listings, Glob, and Grep; pdfinfo and pdftoppm; and ImageMagick's magick
or convert. Their sandbox facts stay out of successful model-visible results.
Directory Read does not follow descendant symbolic links beyond its authorized
root.
Materially non-default facts are aggregated per owning tool invocation and
persisted in its hidden render-data as a durable warning; additional read-only
mounts stay silent.
Helpers that consume target files stage scratch on that target. Unified diff
presentation is deliberately local because it compares two already-local
content snapshots; an ambient remote session never sends its local temporary
files to the target's diff process.
Native filesystem permission checks remain the authorization boundary; helper
confinement limits effects after that authorization.
Glob and Grep keep the helper's private scratch working directory and pass
absolute authorized search roots to ripgrep. Both narrow a
directory-qualified pattern by its leading literal components.
Absolute patterns, parent traversal, and existing symlink escapes are
rejected. Missing qualified directories settle as ordinary no-match results.
Both tools search hidden
files and exclude .git, .svn, .hg, .bzr, .jj, and .sl metadata at
any depth. Glob deliberately ignores ignore files. Grep respects ignore rules
during ordinary traversal, while an explicit path or positive glob may
select ignored content; explicit scope takes precedence. Neither sorts results
or follows symlinks. Both share
mevedel-tool-fs-search-timeout (20 seconds by default). Error, timeout, and
output-limit facts are settled before exit codes, with captured timeout or
output-limit text labeled partial and passed through the existing result
bounds. Result ordering is unspecified. Incomplete and failed searches tell
the model which path or expression fields to narrow before retrying.
Managed Bash execution¶
mevedel-tool-exec.el owns Bash/Eval tool registration, execution lifecycle,
and rendering. Bash classification and reusable rules live in
mevedel-bash-policy.el; execution-specific permission normalization and
prompt adaptation live in mevedel-tool-exec-permission.el.
Bash source normally runs through bash -lc, so login-shell initialization
contributes to the requested command's output. Analyzer-proven read-only calls
instead use bash --noprofile --norc -p -c to avoid startup-file and environment
side effects. Managed Bash has no automatic timeout; use
the native timeout command when the command itself needs a deadline. On Unix,
Emacs places each child in a dedicated process group, and mevedel sends TERM
followed by KILL to the whole group. On Windows it terminates the direct child.
The result includes partial combined stdout/stderr and structured termination
facts.
A command that ends on its own -- exit, an external signal, or its timeout -- settles from a timer and delivers its result in a later event-loop turn, so input and redisplay run between process settlement and the tool's continuation (result handling and, on gptel backends, the next request). Together they would make each tool completion one uninterruptible pause. Stops and aborts still deliver to waiting foreground callers before they return. If owner teardown arrives after natural settlement but before delivery, it completes that foreground callback before releasing the spool. Session teardown and discarded background work cancel pending delivery instead; no late callback or mailbox publication can revive a retired owner. Delivery from inside another remote command still waits for an idle transport. Queued or running result delivery still blocks session transfer and history mutation. An unclaimed result after a failed mailbox delivery remains pollable by its original owner, but does not block those operations once delivery has ended; neither do retained results already delivered.
Bash waits up to yield_time_ms (10 seconds by default; the declared
250-30000ms schema range is enforced by the input-repair clamp with a
corrective note, and a non-numeric value is rejected by validation). A command
that finishes first returns normally and discards its temporary spool when all
output fits inline. A command still running at the boundary returns its unread
output and an opaque owner-scoped execution ID. Local sessions retain a session
artifact when output does not fit inline. Remote execution spools client-locally
while live and never exposes that client path to the model. When an observation
omits output, mevedel stages a complete session artifact at that tool-call
boundary and returns its target-native logical path; terminal settlement
updates it with the final bytes. If another session publication is already
active, the session artifact resolver serves the queued local staging bytes
until its manifest commit. The local remote spool is removed at terminal-record
retirement or session teardown.
The 64 MiB output cap continues running after yield. Pipe-mode stdin is closed
from spawn. Explicit tty=true instead allocates a PTY and retains writable
stdin without changing the captured owner, workdir, confinement, or resource
grants. Native Windows Emacs exposes only pipe subprocesses, so mevedel rejects
PTY and Interrupt requests there; Stop remains available for the direct child.
If the managed spool cannot be written, mevedel records the file error,
settles with output-write-failed, and starts the same bounded TERM/KILL path;
unwritten chunks never advance output counters or previews.
WriteStdin advertises a static 250-300000ms union range. Empty polls default
to 5000ms and use 5000-300000ms; input writes default to 250ms and use
250-30000ms. These mode-dependent bounds remain handler policy because they
depend on chars. WriteStdin sends ordinary input only to
PTYs. Unconfined Ctrl-C is
written through PTYs or signals pipe-mode process groups; confined Ctrl-C
instead signals the foreground process group once across Bubblewrap's session
boundary.
Every observation returns only the newly unread output. ListExecutions
exposes only the caller's yielded handles, and StopExecution terminates only
a handle owned by that caller. Input and stop inherit that execution authority
without another prompt, while explicit deny rules and permission hooks still
apply. Successful empty-input WriteStdin polls remain model-visible but do
not create separate view rows, whether they return no output, new output, or
terminal facts: the initiating Bash row owns that output and status. Actual input,
Ctrl-C, and stop actions remain compact linked interactions with their
submitted input available on disclosure; failed control operations, including
denials and invalid handles, remain visible. A successfully collected nonzero
command exit is not a failed polling operation. The model still receives each
observation, and retained polling and delivery records remain inspectable in
execution history. Direct and ToolCall-nested execution tools follow the same
presentation rules.
Each WriteStdin attempt records
its requested yield_time_ms and the
effective wait, making omitted or stale tool arguments visible without storing
stdin or process output. Terminal facts record PTY mode and preserve the raw
process exit or signal status. Canonical lifecycle state distinguishes
queued, running, stopping, and completed; Interrupt rejects queued work
that has not started, while Stop cancels it. There is no chunk ID: each
observation advances one private unread cursor and returns canonical execution
facts separately from the raw output. Unread ranges beyond 2000 characters use
the shared newline-aware,
equal head-and-tail preview while the retained artifact remains complete.
The initiating Bash row shows the first command line (truncated to 60 columns),
explicit running, failure, interruption or lost state, and elapsed time. Its
output disclosure starts collapsed while live and stays collapsed on settlement
unless the reader explicitly opened it; expansion shows the exact full command
above the bounded output. Execution ID, working directory, output counts, and
routine exit facts belong in expanded details, while consequential sandbox
disclosures and truncation remain visible.
Canonical live and terminal output is displayed verbatim, including lines that
resemble a model-facing <bash-execution .../> envelope. Only a rendered tool
result whose hidden data marks an appended model-facing envelope has that
envelope removed from its expanded body.
Ordinary Bash executions publish transient progress after two seconds: at most four
times per second while output arrives, and once a second while the command is
silent, which keeps its whole-second elapsed time current. Quiet updates share
whole-second clock ticks with housekeeping and elapsed metadata. The transition
from an output-driven update to that clock waits 250 ms to 1.25 seconds, preserving
the four-updates-per-second bound. A longer
mevedel-execution-progress-interval slows both: quiet updates then come at
least that long apart, still on whole-second ticks. Output read while a remote operation binds the editor's timer
list away moves the pending event on the top-level list rather than arming a
second chain of events. Each event refreshes the row; callbacks due
together share a host wakeup. The existing Bash row receives the
bounded live output tail and updates its status and elapsed time without
creating another output owner. A missed exit sentinel is noticed within a
second.
These progress updates live only in bounded view state and never create
transcript turns. Events carry the originating data buffer and durable tool-use
ID, so the matching main or agent view is selected directly. A progress or
terminal event replaces only that source-backed Bash row. A missing view row
schedules an incremental recovery only when the transcript already contains
its source (or a compound call's source-owning parent). A call that has not yet
entered the transcript keeps its bounded progress cache; normal stream and
tool-boundary projection handle source arrival. Repeated progress does not
rebuild an unchanged transcript in search of an absent source.
Terminal settlement replaces the original row's hidden render-data side channel
in the authoritative transcript with the bounded whole-artifact head-and-tail
preview plus exit, outcome, duration, omitted-output facts, and any noteworthy
sandbox summary. Polling, input, and stop tools never duplicate that disclosure
on their own rows. The provider
scrubber keeps that durable UI state model-hidden, while transcript persistence
keeps it stable across cache turnover and resume. Parallel completion may beat
gptel's insertion of the original row; a bounded data-buffer queue retains that
terminal projection and retries it at tool and final-render boundaries.
Agent data buffers run the final-boundary retry even when no transcript view is
open.
Terminal delivery has one publisher. The yield boundary first reconciles an
already-exited child, so its initiating Bash call receives completion instead
of a stale live handle. A yielded terminal result remains owner-pollable for 60
seconds; repeated polls return the same observation without publishing another
terminal event or mailbox message. If a yielded process exits independently,
or the user stops it outside the model tool, root-owned output is queued
synchronously in the root mailbox. A root result from a process that ended on
its own also wakes an idle model: the record carries a runtime-only wake mark,
and after it is saved the harness offers idle work. The wake starts one turn
whose model-facing prompt is fixed ("Background work has reported; its messages
follow. This is not a message from the user."); the view hides that prompt and
the turn's heading and begins with the background notice lines. The result
itself still arrives through the ordinary mailbox delivery (the native
inbox or the Claude Code turn-start batch). A stop by the user or the model
does not wake. A result the model already read, at its next step, through
WriteStdin, or through StopExecution, does not wake again, and several
completions while idle start one turn. The wake is a new request, so it runs
under the session's current permission settings and sticky Plan restrictions,
never a request-scoped override from the turn that started the command. It
waits like a queued follow-up (see view for the idle-work order and
holds); a failed save means no wake, and a wake that cannot start leaves the
mail unread for the next turn. After a restart the result is still delivered
but no longer wakes. Agent-owned
completion is captured by the retained invocation instead: it does not wake
WaitAgent, and once the provider has produced its terminal response the
runtime settles the turn directly in either arrival order. Each captured completion
is published to the spawn parent as a separate EXECUTION record alongside the
unchanged agent RESULT. This starts no model request. Passive progress/view
subscribers cannot acknowledge delivery, and finished records never appear in
live execution listings.
Only commands that actually yielded produce a completion breadcrumb, which sits
at the original command row. The one line names the command and terminal
outcome (completed, failed, stopped, or timed out) and links to the original
Bash result; it has no expandable output copy or second metadata summary.
Foreground completions produce none. The durable
presentation identity is the session, owning agent, and execution ID, not
command text, so repeated polls, event retries, and later mailbox delivery do
not create a second breadcrumb, while concurrent identical commands remain
distinct. Recording checks older segments through their breadcrumb identities,
cached by each archive's content identity (published hash or file stat), so a
completion does not reread and parse every archive. The mailbox payload and model-facing
delivery remain unchanged; the
owner's own delivery renders nothing in its transcript, and other receiving
transcripts show a one-line background notice built from
the EXECUTION, MONITOR, and MONITOR_TERMINAL opening-tag attributes (see
view), never from payload text.
Show result reveals the source row in the current, archived, or agent
transcript as appropriate, expanding enclosing groups; when that row is
unavailable, a read-only retained-evidence view reports what is available or
explicitly identifies missing evidence. A parent receiving a child's forwarded
EXECUTION record shows it as a child-attributed notice alongside the agent
answer.
The view's background work panel lists active agents, yielded commands and
Monitors, with an execution's owner when it is not /root. A Bash row shows
the call's description when the model gave one, else the command. Click or
RET expands one item locally, without navigating away from the composer; an
execution shows its bounded output preview. Explicit actions open full output
read-only, show the original conversation entry, or stop the live item.
Inspection never consumes model output. An expanded item remains after
completion until collapsed or replaced; other finished rows leave the panel.
[Show all N] expands an overflowing panel in place and [Manage] opens the
/ps cockpit. The original tool row remains the durable owner of execution
output and status.
Users have a separate session-wide control surface. /ps, [Manage] and the
session cockpit's Background work row open a tabulated list of active agents
and of foreground and yielded executions from every model owner. Execution
rows show kind, state, description or command, canonical owner (/root or a
retained agent path), elapsed time, and details: the opaque execution ID, PTY
mode, output bytes, and sandbox state. Details include the bounded live tail
and current spool path. The user may send a PTY line, signal Ctrl-C, stop the
process group, or open the spool. /stop EXECUTION_ID stops directly; bare
/stop stops every live execution in the session. These user controls do not
widen model tool authority: WriteStdin, ListExecutions, and
StopExecution remain scoped to the calling owner and yielded handles.
Progress and completion refresh the table in place, and terminal rows
disappear instead of becoming tombstones.
Terminal facts preserve the raw exit code and derive a separate outcome.
For a final outer pipeline, Bash also reports pipeline_exit_codes and
one-based pipeline_failed_stages. A genuine stage failure makes outcome
failure even when the last stage and raw exit_code are zero; a short line
identifies the failed stages. Nonfinal SIGPIPE (141) is benign when a downstream
stage succeeded. Each identifiable stage uses the same expected-status rules
as a simple command, so grep no-match and diff differences remain ordinary
observations in pipelines.
The snapshot runs after the source returns, without enabling pipefail or
installing traps. This preserves yes | head && echo continued and caller
shell options. Source that visibly installs traps or runs dynamic shell code
is left untouched, preserving the pipeline context available to user EXIT
traps. Plain parameter arguments ($x, ${x}, "$@") and plain for loops
are eligible, including for f in *.el; do ... "$f" ...; done | tail.
Variable and loop names must match [a-z_][a-z0-9_]*, except _; uppercase
and special shell variables are declined because some evaluate arithmetic on
assignment or read. The only special expansions admitted are "$@", $#, $?.
Nested loops and pipelines inside loop bodies remain ineligible. A loop is one
conservative compound stage, not its last body command.
This does not make those commands read-only or grant execution authority.
Dynamic executable names, command/process substitution, arithmetic, array or
indirect expansions, parameter operators, function definitions, and builtins
that evaluate shell code or variable subscripts remain ineligible. Complex
expansion syntax is declined even when quoted or escaped; printf requires a
literal non-option format without %n.
EXIT traps installed by startup files or inherited shell functions
are not detected; the appended snapshot can change the PIPESTATUS they see.
The proof also declines arithmetic, subscript syntax, variable-targeting or
code-evaluating builtins (including test, [, let, and declare), and
printf -v or %n formats: these can evaluate a subscript taken from the
environment even without visible substitution syntax in the source.
Startup-file namerefs are likewise outside the source proof.
More generally, startup-file variable attributes (including integer attributes)
and shell functions are not inspected. This is conservative source observation,
not a security boundary or a proof about the inherited shell environment.
Explicit exit/exec, early shell exits, and pipelines hidden inside compound
commands can bypass or collapse the snapshot. Classification requires a sole
syntactic pipeline at the end of the source. Multiple pipelines, including
sequential or conditional lists, make stage identity ambiguous: captured exit
codes remain visible, but
pipeline_classification="unavailable" leaves the shell outcome unchanged.
This is final-pipeline observation, not an audit of every earlier command.
Both local and TRAMP execution explicitly launch Bash, independent of the
target's default login shell; a missing Bash executable is a launch failure.
Without a classified pipeline, zero is success. Exit one is no-match for
one proven simple grep or rg command, different for diff, and false
for test or [. These outcomes
are successful tool observations rather than execution errors. Exit codes two
and above, non-exit termination, path-qualified executables, and compound,
dangerous, complex, or unsupported analysis fall back to failure. Command
output is never prefixed or rewritten to encode the outcome. Model-visible XML
and UI render data consume the same canonical fact snapshot; the XML also
repeats the exact command so parallel same-name calls remain self-identifying.
Analyzer-proven read-only Bash calls may overlap within one session. Unknown, unparsable, and mutating calls use the exclusive lane. Admission is FIFO: once an exclusive call is waiting, later readers wait behind it, preventing writer starvation. Main and sub-agent owners share their session's scheduler, while separate sessions remain independent. A command releases its scheduler lease when it finishes, aborts, or yields; a yielded operating-system process keeps running under its original owner and resource boundary without blocking later admission. Before starting queued work, admission rechecks that a retained agent owner is still active and settles rejected work without spawning a process.
A remote mutating Bash command first acquires or verifies the session's portable
mutation lease and durably arms its unsettled-mutation latch before process
launch is attempted. Yield releases the scheduler lane, so more than one
mutating process can remain armed; one clean settlement cannot clear the latch
while another armed record remains. A post-attempt launch error, transport loss,
or failed lease compare-and-set remains unknown. A later target proof that the recorded process group is dead can clear the
block. A durable latch restored without process identity requires
mevedel-retry-target-readiness and explicit acknowledgement before mutation
admission reopens.
Non-read-only tools are rejected while the latch is armed without a live
provable writer. Lifecycle teardown gives a final KILL one bounded proof
interval before it decides whether the latch can clear. Process records,
timers, and spools remain transient.
At most 64 managed Bash processes may be live in one session. The sixty-fifth is refused before spawn without evicting existing work. Foreground work remains owned by its initiating request; yielding detaches it from later request aborts without changing its session, model owner, sandbox boundary, working directory, or resource grants. Shell-native background operators are rejected because they would bypass this lifecycle. Remaining descendants are terminated when the managed shell exits: the captured process group is signalled once and then force-killed after a single bounded cleanup interval, and the record settles only after that. Run a service that must outlive its command outside managed Bash.
Execution lifetime follows ownership rather than transcript visibility. Agent termination synchronously discards only that canonical agent's Bash and native helper children; data-buffer teardown, package uninstall, and Emacs exit do the same for every child in the session, including queued scheduler work and process-group descendants. Record-owned teardown also releases helper scratch directories when normal callbacks are suppressed. Ordinary yielded completion still uses the captured owner context and never launches an unsolicited model request. Bash, Eval, and filesystem helpers all resolve that owner through the same request-first execution-context resolver.
Real transport acceptance¶
test/test-mevedel-execution-remote.el has opt-in cases for each supported
transport. Set any combination of MEVEDEL_TEST_SSH_ROOT,
MEVEDEL_TEST_DOCKER_ROOT, and MEVEDEL_TEST_PODMAN_ROOT to existing writable
TRAMP directories; authentication and target/container startup remain external
to the suite. Unset transports skip independently. Once configured, each
transport must also pass required Bubblewrap readiness and its exact-grant
case; unavailable confinement fails that release gate.
MEVEDEL_TEST_SSH_ROOT=/ssh:user@host:/srv/project/ \
MEVEDEL_TEST_DOCKER_ROOT=/docker:container:/workspace/ \
MEVEDEL_TEST_PODMAN_ROOT=/podman:container:/workspace/ \
npx @emacs-eask/cli test ert test/test-mevedel-execution-remote.el
The loss cases discard this client's transport and refuse reconnection while the execution settles, so the target keeps running and the outcome is genuinely unprovable. They require an unknown outcome plus the mutating-execution block, then reconnect, identity-check the bounded descendant that survived, and clean its process group.
For a disposable local matrix, the repository provides one OCI target image
used by Docker, Podman, and SSH. The launcher builds it, starts one container
per runtime, routes SSH through the Docker instance, temporarily installs the
pinned Bash tree-sitter grammar when absent, runs the acceptance file, and
removes its containers, grammar, and temporary credentials. It deliberately
supplies no --privileged, capability, or security-profile override; the same
default-run core transport matrix is a CI gate:
test/run-remote-acceptance.sh
Monitor¶
Monitor runs a watcher whose repeated stdout lines should reach the root model
as events. For a single eventual result, use Bash (for example, an until loop)
and let its completion wake the model instead. Monitor is native in the
implementation preset, hidden from retained agents, and rejected by the handler
for any owner other than /root.
The required arguments are command and a non-empty description, shown on
every event. timeout_ms defaults to 300000 (five minutes) and must be an integer
from 1000 to 3600000. sandbox_permissions, additional_permissions, and
justification have the same meanings as Bash. There is no tty argument:
stdin is closed at launch. Monitor uses Bash's permission rules, request-scoped
authority, approval/review path, and Plan restrictions; the tool name does not
grant additional execution authority. It also preserves Bash's read-only
startup hardening described above rather than unconditionally using a login
shell. Any stdout emitted by the selected shell's startup is event output.
After scheduler admission and launch, Monitor yields immediately, releasing its scheduler slot. The initial result contains execution metadata, not a second copy of stdout. The process continues across model turns under its captured owner and execution boundary, with its timeout measured from launch. Monitor's deadline does not add a deadline to ordinary Bash.
Only stdout becomes events. Native separated stderr joins the retained output
file but never triggers a line event. Local and direct-async launches separate
it through a native stderr pipe. A remote monitor that runs through TRAMP's
shell adapter (no direct-async channel, or a command over its length limit)
cannot use one: that adapter serves :stderr through a FIFO on a second
connection, and the cleanup it installs on the process sentinel issues remote
commands from inside whatever command is already running on the host, which
corrupted a concurrent process-file result in testing. There a target-side
wrapper appends stderr to a temporary file on the target instead. Result
delivery, which already waits for an idle transport, moves the file into the
output file within the same 64 MiB cap and deletes it; registry cleanup
deletes an uncollected file. On that path stderr joins the output file only
when the monitor ends, after its stdout. Each pipe stage in a watcher must flush
its output; buffering inside the command delays events. Filters should match
failure states as well as success. The harness frames split chunks into lines,
normalizes CRLF, and retains a final partial line on settlement. Each line keeps
up to 2000 characters plus a truncation count. A single 200 ms timer batches
pending lines; a batch holds at most 8 KiB and reports extra lines omitted from
the event body. The output file remains subject to the managed 64 MiB spool cap.
Each framed line of a batch is its own durable MONITOR mailbox record bearing
the execution ID and description, with a runtime-only wake mark; omitted lines
are summarized on the last record (omitted attribute plus the model-visible
note). Events carry the framed lines (:monitor-lines, :monitor-omitted) so
consumers never split a payload. It follows the same idle-wake holds
and delivery path as background Bash completion above. Event text is tool
output, never a user instruction. More than 100 lines in a rolling 60-second
window, or more than 64 KiB of that monitor's unread mailbox events, stops the
process with termination flood.
Settlement cancels pending timers and publishes pending lines as MONITOR
records followed by exactly one MONITOR_TERMINAL record whose opening tag
carries event_count, termination, outcome and exit_code and whose body
is the terminal notice. Natural exit
reports the exit code; expiry says to re-arm explicitly; flooding says to
restart with a tighter filter. These notices wake the idle model. A deliberate
stop through StopExecution or the user controls folds pending lines into a
non-waking terminal notice. Teardown discards pending publication instead of
reviving a closed owner.
ListExecutions includes live root-owned yielded monitors. WriteStdin with
empty chars polls their combined stdout/stderr spool, not just event lines;
ordinary input is rejected. Ctrl-C can signal the process, but does not promise
termination or a non-waking completion. Use StopExecution to stop the watcher
with the non-waking stop semantics. Observing a terminal result clears its
pending wake.
The initiating tool row, status zone, and /ps identify a monitor as
"Watching" with its description. Details show the event count and fixed expiry
timestamp (unknown until launch), not an elapsed-time ticker. Quiet monitors
skip periodic progress redraws while retaining lost-process detection. A
waiting monitor alone does not inhibit system sleep; an event-triggered request
or permission interaction still does.
Monitors exist only in the current Emacs runtime. Session teardown, clear, or close stops them; resume and fork never restart them. Transcript facts remain, with prior live executions shown as lost. Saved event mail survives reload but loses its wake mark, so it is read on the next turn rather than starting a new request after restart.
Eval execution scope¶
Eval has two execution modes. live is the default and runs inside the
current Emacs process so it can inspect live session state. Live mode
restores the selected frame's window configuration by default, preventing
accidental calls like delete-other-windows from surprising the user;
preserve_ui: false opts out for deliberate UI manipulation. batch
runs a child emacs --batch -Q process with the current load-path and
the session working directory. Bash and batch Eval share child-process output,
cleanup, process-group handling, and optional Bubblewrap confinement; live Eval
does not use that child seam. Batch mode isolates interactive Emacs state and,
when the platform sandbox is active, applies the same filesystem, protected
path, process, and network boundaries as Bash.
Bash and batch-Eval results record the boundary that applied to their invocation. The settled owning tool row retains a compact durable warning for materially non-default boundaries, additional writes, refusals, and children that never started. Additional read-only mounts stay silent. Agent rows aggregate warnings from their direct child executions, while the agent transcript identifies the affected tools.