@-mention system¶
Supports @ref, @file, @agent, @mcp.
Expansion runs as a gptel prompt transform (priority -90) via
mevedel--transform-expand-mentions, dispatching through
mevedel-mention-handlers. Each mention becomes a compact
[kind:KEY -- STATUS] placeholder in the prompt text; the full content
is a staged reminder entry (mevedel-reminders-stage-entry) that the
WAIT injector delivers in the synthetic reminder message after the
current input and records in the hidden injection row. Deduplication
commits ride the deferred reminder commit channel, so a request that
dies before injection never marks content as shown.
Root WaitAgent steering has no new gptel request on which that transform can
run. At its delivery boundary, mevedel-mentions-expand-user-input therefore
applies the same expansion to the prepared steering text before the
suspended request re-enters WAIT; the returned reminder items are staged on
the same fsm and delivered by the reminder injector later in that WAIT. The
original bound text remains in the composer, history, or pending-steering
entry; only the model-ready copy contains placeholders. Expansion returns
media contexts and deduplication updates explicitly: ordinary request
transforms apply both, while steering attaches the media to its own injected
user message with mevedel-mentions-wrap-prompt-media and commits
deduplication only when the steering entry is delivered.
Inline $skill attachment scanning lives next to this transform and reuses
the same placeholder plus staged-reminder output path, but keeps a
separate parser because $skill quote, escape, and Markdown-code rules differ
from @ mentions.
Authored !$skill required-attachment declarations are different again. They
are recognized only from literal author-origin text in a SKILL.md during skill
preparation, not by the user-prompt mention scanner. They declare instruction
attachments rather than user invocations, tool calls, or delegation. Escaped
or Markdown-code forms and markers introduced by substitution, injections,
hooks, prepared bodies, or model output remain inert.
Resource addresses are a separate tool-target syntax. A mention performs an attachment, invocation, or delegation operation; a resource address names content for an existing filesystem-shaped tool. When both identify a file, skill source, or MCP server/URI, they share the same canonical resource locator, but they do not share side effects.
Session-owned work:// descendants, including work://plans/..., are
shared by the parent and retained agents for durable plans, notes, findings,
contracts, and handoffs. work://shared/... instead belongs to the workspace
across sessions. These remain tool targets; @file and @mcp attach,
user $skill invokes or attaches, authored !$skill requires instruction
context, and @agent delegates. Standalone/sticky Plan mode permits
session-only ApplyPatch there, while an ordinary, shared, memory, or bare endpoint is
denied before the namespace can be materialized. Directive Planning does not
allow ApplyPatch there, including session-only proposals; Eval is
unavailable in both.
mevedel-mention-bindings.el owns the shared atomic binding lifecycle for
skills, direct references, files, and MCP resources. Kind-specific discovery,
resolution, content loading, permission checks, and request annotations remain
in the skill and mention modules. File token recognition and braced-path
decoding also live in the binding module and are shared with Markdown view
links; request ranges and rendered line-reference lists keep their respective
syntax. Decoding a token neither resolves nor authorizes its pathname.
Atomic binding lifecycle¶
A binding is a hidden text property on the exact visible mention token. It
stores :kind, the unchanged :token, and one canonical locator:
- skill: canonical
SKILL.mdsource pathname - direct reference: reference UUID
- file: absolute expanded pathname
- MCP resource: configured server name plus resource URI
The binding preserves identity, not contents or authorization. Completion and programmatic insertion bind immediately when the exact target is known. Remaining manually typed mentions bind at send before async preparation, queueing, or input-history insertion. A missing direct reference or unknown skill remains unbound because no exact target is known; a file pathname and an MCP server/URI pair can bind even while unavailable. Reference queries intentionally remain unbound so every dispatch evaluates the current matching set. Agent mentions also remain unbound because registered agent names already provide their live identity.
The canonical locator can be serialized as a resource address for tool input, but the hidden binding itself is not an address string and does not grant authority. Dynamic listings and queries remain unbound so dispatch resolves their current state.
Composer drafts, pending input, retries, transcript prompts, history recall, and persisted input history carry the propertized prompt string. Copying or yanking a bound token inside mevedel preserves its locator; plain external text has no property and follows normal send-time binding. Editing a token or lexically extending it removes only that occurrence's binding. Edits elsewhere and untouched copied occurrences retain theirs. Bindings add no face, badge, lock state, indivisible token behavior, or permanent transcript warning.
At actual dispatch, each kind resolves only through its stored locator and reads current target state and contents. It never falls back to a same-named skill, reused reference number, different pathname, or different MCP server name. Current file and MCP access checks still apply; binding is not approval. Unavailable, deleted, disconnected, unreadable, or denied targets become explicit annotations only in the temporary model request, emit a nonblocking message, omit unavailable contents, and do not block the rest of the turn. The composer, queue, transcript, and history retain the user's original text.
A malformed binding is corruption rather than ordinary unavailability. An invalid plist, unsupported kind, mismatched token, partial property run, or invalid lexical boundary blocks live submission so visible text cannot silently retarget. Persisted history containing an incompatible or malformed binding is quarantined by the corrupt-history path. Bindings live in the prompt string rather than a registry or sidecar store. Supporting another binding kind requires explicit schema, send-time binding, and dispatch branches.
Mention kinds¶
- @ref:N / @ref:{tag query} — direct references display their numeric ID but bind the selected reference UUID. Completion binds immediately; manually typed direct references bind at send when the reference exists. Queueing and input history preserve that UUID, while dispatch reads the reference's current contents. A deleted bound reference is annotated as unavailable with a nonblocking warning and never falls back to a reused numeric ID. Tag queries remain unbound and re-evaluate current matches at every dispatch.
- @file:path / @file:{path with spaces} — hierarchical file
completion inserts the bare form; drag/drop and clipboard image paste
use the braced form when quoting is needed. Completion and programmatic
insertion bind an absolute pathname immediately; manually typed forms bind
their expanded absolute pathname at send, even if it is currently missing.
Dispatch reads current contents at that pathname. Replacing a file at the
same path retains the target; moving or deleting it makes the bound target
unavailable. Optional
#L<start>[-<end>]pins a line range for text files (not recorded in touched-files, since LLM may still need other parts) and remains request syntax rather than part of the pathname locator. The start must be at least one and the end no earlier than the start; a token violating either is rejected with an explanatory placeholder rather than attached, since the read would otherwise be repaired silently and labelled with the range that was asked for. The binding schema still accepts such a token, because a binding carries the target's identity rather than the range's validity. Directories return a gitignore-filtered recursive listing (rg --files --hidden --sort path, without following descendant symlinks) capped atmevedel-file-mention-directory-max-entries(default 1000). Text contents read throughmevedel-tool-fs-read-slurp-file-contents(512 KiB cap, line numbers). Text delivery explicitly labels the attachment as task data, not host-loaded workspace instructions. An instruction-like filename such asAGENTS.local.mddoes not activate the attached contents as policy. The user can explicitly ask the model to follow an attachment; independently applicable workspace instructions retain their authority. This label belongs to the file mention envelope, not to the file's contents or a filename-specific heuristic. An exact path inside an active remote session store resolves only through that session's artifact resolver: fixed-path cache files do not establish presence or content. Resolved media bytes are staged in a local temporary file for gptel and removed when the request ends. Supported media file types from the Read tool (png,jpg,jpeg,gif,webp,pdf) are attached through gptel context when the active model advertises compatible media support. A model that takes images but not PDF documents receives the PDF's leading pages as PNG images, rendered synchronously withpdftoppmup to the Read page cap and attached in document order; a reminder names thepagesvalue that continues a longer document. Otherwise the mention is rejected with an explanatory placeholder. Runsmevedel-check-permission "Read"first — any non-allow yields "permission denied". Missing and unreadable files are rejected. - @agent:name — asks main agent to delegate via
Agent(task_name="NAME", message=...)(the name is validated againstmevedel-agent--registrybefore the reminder is created). This delegation mention is not anagent://resource address, which is read-only inspection. - @mcp:server:uri — attaches an MCP resource via mcp.el
(
mcp-hub-get-servers,mcp-server-connections,mcp-read-resource). Resource completion binds the selected server-name/URI pair immediately; manually typed complete locators bind at send even while disconnected. Dispatch uses the current connection and current resource contents, so a reconnected or replacement configured server with the same name remains the same locator. Current MCP availability and server access behavior still apply, and read or access failure exposes no resource contents. URI capture is greedy past internal colons sofile:///...works. mcp.el is optional (declared viadeclare-function). The same canonical server/URI locator is used bymcp://resource addresses; both resolve the current connection and fresh resource contents.
Every rejection branch emits a follow-up <system-reminder> telling the
LLM the bracketed placeholder is a system annotation, not user text.
Expansion flow¶
flowchart TD
A[User prompt with mentions] --> B[Prompt transform]
B --> C[Parse mention kind and key]
C --> G[Resolve target, check access and load content]
G --> D{Handler accepts?}
D -- No --> E[Insert rejected placeholder]
E --> F[Stage explanatory reminder]
D -- Yes --> H{Already shown unchanged?}
H -- Yes --> I[Keep compact placeholder only]
H -- No --> J[Stage content reminder entry]
J --> L[Model-visible prompt]
I --> L
F --> L
The diagram shows preparation of text attachments. Media contexts are prepared separately and added to the request even when their content hash is unchanged. The reminder transaction then delivers staged entries after the current input or completed tool results. If preparation or injection fails, it does not acknowledge those entries; unchanged contents are suppressed only after accepted delivery.
Dedup¶
- Per-session:
mevedel-session-mentions-shownkeyed on(KIND . KEY)stores(turn . content-hash); unchanged hashes skip repeated text-reminder injection. Media contexts remain part of each request that mentions them; ordinary path/MIME context entries are deduplicated within that request. Direct references use their UUID asKEY, so changed contents are attached again without allowing displayed-number reuse to collide. Files use the absolute pathname plus requested line range; MCP resources use server name plus URI. The kind and exact locator prevent cross-kind collisions, while the current content hash causes changed content to be attached again. - Read dedup:
@filerecords reads onmevedel-session-touched-filesso later Read calls short-circuit
Drag/drop grants¶
Dragging local files into the view buffer inserts visible @file mentions
and records pending exact-file grants on the session. During the next send
that still mentions the same expanded path, the grant becomes an
in-memory session-scoped Read grant for that exact path only. It does
not grant the containing directory, does not apply to write tools, and is
not persisted. Explicit deny or ask rules, protected paths, unreadable
files, and missing files still take their normal paths through the
permission chain. Pending grants are cleared when the composer is cleared.
For a queued entry the grant is activated before that entry's mention
expansion and released again if the entry never reaches the model.
Clipboard images¶
C-y in the view composer saves a clipboard image to
<workspace-root>/.mevedel/state/media/ and inserts an @file mention for it.
The saved image follows the same pending exact-file grant and media
attachment path as a dropped file. If no clipboard image is available,
C-y falls back to normal text yank.
Completion¶
mevedel-ref-capf, mevedel-file-capf, mevedel-agent-capf,
mevedel-mcp-capf (two-stage: server names at @mcp:, resource URIs at
@mcp:server:). Font-lock uses success/shadow/link box faces.
Registered in mevedel-install/-uninstall.
@file completion enumerates only inside the session's allowed roots, so
../ stops at each root boundary and an out-of-root or remote-escaping
typed prefix returns no candidates and performs no filesystem probe. Such a
path stays manually typeable and faces the ordinary Read check; candidate
discovery is a bound on what is offered, never a grant. This matches what
resource-address completion already does.
Resource-address completion offers scheme prefixes first and bounded
scheme-specific descendants afterward. It inserts plain canonical text and
does not bind a mention, attach context, invoke a skill, delegate an agent,
read content, refresh MCP, materialize a session, or change durable state. See
address-to-resource.md.