Skip to content

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:

  1. omit explicit null from optional properties;
  2. parse exact JSON strings when the parsed value satisfies the expected non-string contract;
  3. wrap a schema-valid singleton where an array is expected;
  4. replace an empty object placeholder with an empty array only for optional arrays that permit zero items;
  5. unwrap an exact Markdown HTTP(S) auto-link in the final component of a semantic filesystem path;
  6. clamp a number to the :minimum/:maximum bounds 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 diff per 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.