Skip to main content

Mentioning an agent in an issue

You bring an agent into a discussion the way you bring in a colleague: type @, pick it, and it is named in the comment. Mentioning an agent wakes it. The mentioned agent is given a task naming the issue and quoting the comment, and it runs — the same way it would if you had assigned it work. A mention does not change the issue’s assignee.

Using it

In the comment box on an issue:
  1. Type @. A picker opens listing the agents in your workspace.
  2. Keep typing to filter — it matches on both the agent’s handle and its display name.
  3. / to move, Enter or Tab to insert, Esc to dismiss. The caret never leaves the draft.
  4. /Ctrl + Enter posts the comment, the same as everywhere else in Crewship.
The picker only opens at a word boundary, so pavel@unify.cz stays an email address.

How a mention is stored

A mention is a plain Markdown link with a private scheme:
For example, a comment reading
over to you @Robin — the CSV job is failing on empty rows
is stored as
Four properties this buys, and they are the reason it is a link rather than <@id> or a bespoke @@agent:id@@ sigil:

Why it cannot be used to impersonate

The label in the token is decoration. The chip’s name and avatar are looked up from the workspace roster using the id, at render time. So writing
renders a chip reading @Robin, with Robin’s face. There is no wording that makes a mention claim to be someone. Two more things follow from that:
  • A body that contains the literal markup a chip renders to is not a chip. The comment renderer’s sanitiser drops unknown tags and unknown attributes, and the internal element name a mention passes through is randomised per page load, so it cannot be typed.
  • A mention of an agent your workspace cannot see is not a mention. It is text.
What the format deliberately does not try to prevent is a human typing a well-formed token for a real agent. That is not forgery — that is a mention, which is the feature. Whether that mention is allowed to make the agent work is an authorization question, and it belongs to the trigger, not to the syntax.

What happens when you post the comment

Every step below runs after the comment is committed, so a mention that cannot be recorded never turns a posted comment into an error.
  1. The body is parsed, not scanned — internal/mentions walks the comment as CommonMark and reads only real link nodes.
    Do not substitute a regular expression over the raw body, and in particular not this one, which earlier revisions of this page prescribed:
    It contradicts the “parsed, not scanned” property in the table above. A regex cannot see that a token sits inside a code span or a fenced block, so this very page — which prints well-formed mentions as examples — would dispatch agent runs if a comment quoted it. internal/mentions parses the body as CommonMark and reads only real link nodes, so code contains no mentions structurally rather than by rule. Replacing the parser with that regexp fails 15 of the package’s tests, 14 of them in the fires-on-docs direction — and, end to end, three of internal/api’s: a comment quoting this page’s examples creates rows, activity entries and agent runs.
  2. Each id is resolved inside the comment’s own workspace. An id that names no agent there is dropped and leaves nothing behind — no row, no activity, no notification, no run. A mention of an agent in a different workspace is a probe, not a typo, and is treated the same way.
  3. A mentioned activity is written to the issue’s History first, with the agent id in details — this is what everything below references.
  4. A delivery is recorded and acknowledged. The resolved set is stored in mission_comment_mentions, one row per (event, agent), so nothing ever re-parses a comment on read. Naming the same agent three times in one comment is still one row, one activity entry and one run — the same guarantee as before, now held by the delivery’s own key rather than the comment’s. The moment this row is written, the server pushes issue.delivery.acked on the issue’s workspace channel — before any model call happens. That is what lets the UI show “received” the instant you post, rather than only once the agent’s own reply appears. The row also records what the dispatch trigger did — dispatched, refused (a limit said no, with the refusal verbatim), skipped, or failed — so a mention that did not wake its agent is answerable rather than mysterious. A duplicate delivery of the same event — a redelivered event after a server restart, for instance — is collapsed to the row above rather than starting a second run: only the first delivery to claim the row is dispatched.
  5. The agent is dispatched, through the same endpoint an agent-to-agent assignment goes through. It inherits that endpoint’s authorization and its limits; see below. The brief it is handed says who mentioned it and on which issue, and every one of those values — the comment, your display name, the issue’s title and identifier — sits inside an <untrusted> block. Display names and issue titles are written by whoever wanted to write them, so the woken agent is told to read them and never to obey them. Because the run belongs to a session (below), the brief also carries a context pack: a snapshot of the issue, the session’s latest checkpoint, and every activity on the issue the session has not read yet — so an agent woken a week later resumes rather than starts over. It ends with the instruction to close the run with a ---CHECKPOINT--- block.

What bounds it

A mention is a way to start an agent run, so it is bounded by the same two numbers every other dispatch is — not by a mention-only rule: Both are instance settings (crewship instance settings set delegation.max_depth 3). When one refuses, the mention is still recorded and still appears in History — the refusal is stored on the row, naming the setting an operator would change. A person’s mention is counted only against the runs mentions started for that agent on that issue. Work the agent hands to other agents while leading the issue is a different budget, so a busy lead does not become unmentionable. Two more rules the dispatch inherits from the assign path:
  • Crews must be connected. Mentioning an agent in a crew that is not connected to the issue’s crew records the mention and dispatches nothing.
  • An agent does not mention itself awake. A self-mention is recorded and skipped.

Sessions, checkpoints and the context pack

A mention does not just start a run; it opens — or reuses — a session: one durable row per (issue, agent), crewship issue sessions <id>. The second mention of the same agent on the same issue finds the same session, which is what makes “resume where you left off” possible at all.

The session lifecycle

The state is on crewship issue sessions and on the issue’s Sessions panel, and every change is pushed as issue.session.state on the workspace channel.

The checkpoint block

A session-bearing run is told to end its output with this block, and it is required on every run:
facts and blockers may run over several lines; a field’s value continues until the next key: line or the closing marker. outcome is what routes the run (crewship issue runs shows it) — leaving it out is treated as FAILED, so an agent that ran fine but forgot the block is a visible failure rather than a silent success. Every block is stored as a row in the session’s history (crewship issue checkpoints <id> --agent <slug>); a run that ended without a valid block is stored too, with parsed: false and every other field empty, so “forgot to report” and “reported nothing” stay distinguishable.

The context pack

Every real dispatch on a session appends a context pack to the brief, in this order, each part inside its own <untrusted> fence and under its own token budget:
  1. Issue snapshot — identifier, title, status, priority, owner and delegate, parent issue, the current work mode and note, and the description (800 tokens).
  2. Latest checkpoint — the most recent parsed block on this session, so the run reads done before deciding what to do (600 tokens).
  3. Unread activity — every mission_activity event with a seq above the session’s last_consumed_seq: comments, status changes, mentions, run results, in order (1,200 tokens, at most 2,000 rows).
When the unread delta does not fit, it is first rendered one line per event (summarized); when even that does not fit, the oldest contiguous run of events that does fit is rendered (truncated) and the rest waits for the next wake. The cursor only ever advances over events the run was actually shown, so nothing is marked read that no agent read; the path taken is recorded on the run. A fold-in — a follow-up appended to a run that is already going — does not advance the cursor, because that run never saw the pack. Memory search and the artifact manifest are not part of the pack.

Leases: how a dead run is noticed

Every RUNNING assignment carries a lease — an owner (the server process driving it, hostname:pid) and an expiry 90 seconds out. The process renews it every 20 seconds while the run is in flight; a sweeper looks every 15 seconds for RUNNING rows whose lease has lapsed and fails them, with the error lease expired — no renewal from owner … within 1m30s. That error is what crewship issue runs shows on the run; the lease columns themselves are not on any API. Recovery is keyed on the lease, not on when a server booted: a row a different, still-live process is renewing is never touched, and the same rule guards boot-time recovery after a crash. A lease expiry is a plain Tier 1 failure of the run (FAILED, session to error) — it never kills a container process; that is what issue stop --hard is for.

Known limits in 1.0

Written down on purpose (they are release conditions, not footnotes):
  • A follow-up on the SAME issue never starts a second run — it waits for the one already going, then rides the NEXT run. One (issue, agent) session may have at most one live run, enforced at the database level (a partial unique index on assignments.session_id), not by a check-then-act read that a fast second comment could race past. Mentioning the same agent again while its run on THIS issue is still going does not queue a second dispatch: the delivery is recorded and acknowledged (issue.delivery.acked) the instant you post, but it is left unclaimed — the run already in progress cannot see it (there is no channel to inject text into a turn already under way; steering a live turn is future work, and there is no “re-read the task mid-run” path either). The moment that run actually finishes, every comment queued behind it — one or several — is folded into exactly ONE new run, with all of their text in that run’s own brief before its exec starts. So a burst of five follow-ups while an agent is busy produces one extra run, not five, and not zero: nothing is silently dropped, and nothing is marked read before an agent has actually read it.
  • A correction jumps the queue and is labelled as one. A comment that arrives while the agent’s run on this issue is still going is a correction (the delivery’s priority, stop > correction > normal, per the interruption-as-an-event model). When the queued comments are folded into the next run, corrections lead the brief, ahead of ordinary follow-ups, and are marked CORRECTION with a header telling the agent to read and apply them first — so the next step reflects your correction rather than treating it as just another later comment. This is delivery at the next safe boundary (the next run on the same session), not mid-token interruption of a turn already under way, which stays future work.
  • A busy agent is woken later, not interrupted, on a DIFFERENT issue. Beyond the same-session case above, an agent still has only one live run at a time across every issue and chat it touches. Mentioning it while it is busy on another issue queues the mention behind that live run; it is dispatched as its OWN new run once the agent frees up, with the brief it would have had. Before this existed, a second run for the same agent killed the first one mid-turn; queueing is the fix. The delivery is recorded and acknowledged the instant you post either way, so you are not left guessing whether the comment was received while you wait.
  • A chat message to an agent busy on an issue bounces. The chat reports agent_busy rather than starting a second run into the same container. Wait for the run to finish, or watch it from the issue.
  • The lock covers the doors that run an agent for an issue/assign, @mentions, mission tasks, lead planning, the chat, the agent cron and a routine’s agent_run step — but not the inbound agent webhook (POST /api/v1/webhooks/{crewId}/{agentId}/trigger), the direct agent-start route on the IPC socket, or a peer query. Those three can still start an agent that is already running. A routine’s agent_run step against a busy agent does not queue: the step fails with target agent has a live run in progress elsewhere and follows the step’s retry policy, then on_fail, like any other execution error.
And one that is not a limit but a hold:
  • An agent awaiting approval is not woken. An agent created or hired by an agent under guided autonomy sits at PENDING_REVIEW until a person approves it. Mentioning it records the mention and starts nothing — the same answer /assign and a mission task give, and the same answer it gives a chat message. Approve it from the inbox and mention it again.

When nothing runs

A mention that starts nothing is recorded either way: the join row keeps dispatch_state and the reason, and the issue’s History still shows the mentioned entry. The join row itself has no API or CLI read path — what you can see is the History row (crewship issue activity, action mentioned), the inbox item described below, and the run in crewship issue runs when one started. A refused, skipped or failed mention is not queued — re-mention once the reason no longer holds. The one case that is queued is an agent that is simply busy: see the known limits above. Whether anyone is told depends on who wrote the comment. A person gets an inbox item“Your mention of Robin on ENG-42 did not start a run”. What it carries depends on why:
  • Refused — a limit or a rule said no. The sentence is reproduced verbatim, because it names the setting an operator would change.
  • Failed — something broke on the Crewship side. You get a fixed sentence saying so. The underlying error is deliberately not shown: it is an internal fault, not a decision about your mention, and it lands on the mention record and in the server log instead.
A comment written by an agent produces no inbox item at all. There is no author with an inbox to address, and every alternative recipient would be invented — notifying the whole workspace about one agent’s comment is the failure this rule exists to prevent, not a fallback. The fact is not lost: it is on the join row, in the History entry, and in the server log against the comment’s id.
An agent’s own answer arguably belongs in the response to its comment, and today it is not there — POST /api/v1/internal/issues/{identifier}/comments returns the comment, not what its mentions did. So an agent that mentions a capped or held colleague currently learns nothing from the call itself. That is a gap, not a design: it is worth closing, and it is tracked separately rather than described here as though it already worked.

Being notified

A mention delivers on the issues.comment notification category — the same toggle as any other comment on the issue, because a mention only ever arrives inside one. Muting issue comments mutes mentions with them; there is no separate switch to forget to turn on.

The history row

The issue’s History tab renders “X mentioned @Y” when an activity with action in mentioned / mention / agent_mentioned / comment_mentioned arrives. It reads the target out of details in whichever shape it finds — a bare agent id, a whole mention token, or {"agent_id":"…"}. The backend writes the bare agent id.

Which comments are scanned

All three doors a comment can arrive through, because a mention typed by a person and one written by an agent are the same wire format: No new HTTP route was added for mentions — the trigger hangs off comment writes that already existed. crewship issue comment has a --mention <slug> flag (repeatable) that resolves the slug against the workspace agent roster and writes the link form for you:
The link can also be hand-typed through --body — this is what --mention does under the hood, and it is still the only thing the server ever reads:
A bare @robin typed into --body (or a positional argument) is not a mention — see docs/cli/issue.mdx. Everything on this page happens on that same comment call. crewship issue get ENG-42 renders the comment and the activity that follows it.

Where the code is