Every source a turn consults gets one number, and that number is the one Lam cites.
Numbering where the tools run.runAgent owns a per-turn list, so three searches in one turn no longer produce three runs of 1, and a page found by a search and read again by web_fetch keeps the number it already had. The number is printed in the tool result, so the model cites a page rather than guessing a position.
Stored, not re-derived. Each record carries only the entries its own call added, and the turn's list is the flattening of its records. sources is optional in the zod schema, so a conversation stored before this change still loads and simply has no list.
Vicinae cannot draw a footnote. It parses markdown with CMARK_OPT_DEFAULT and attaches only the table and strikethrough extensions (vicinaehq/vicinae, src/server/src/ui/quick/markdown-model.cpp:330-340), so [^1] arrives as literal text. The whole job is done in this pipeline: linkCitations turns [1], [^1] and [1, 3] into links, and renderSources builds the list from the entries the tools registered.
All or nothing. A marker stays as the model wrote it unless every number in it is a source with a destination this window should open, so half a link never reads as a citation that works, and a javascript: address out of a search result stays inert text.
Maths runs before citations. Forced by a collision found in testing: an escaped link label is exactly LaTeX's own \[...\] delimiter, so the maths pass would otherwise eat [\[1\]] and hand back a bare 1.
Fences and code spans are skipped, a footnote definition that would repeat an entry we list is dropped, and a markdown reference definition ([1]: url, no caret) is left alone because it backs a link the model wrote.
The list shows the sources the answer cited, falling back to everything consulted when it cited nothing, and waits for a first citation before appearing at all so it never arrives in full and then shrinks.
Every source a turn consults gets one number, and that number is the one Lam cites.
- **Numbering where the tools run.** `runAgent` owns a per-turn list, so three searches in one turn no longer produce three runs of `1`, and a page found by a search and read again by `web_fetch` keeps the number it already had. The number is printed in the tool result, so the model cites a page rather than guessing a position.
- **Stored, not re-derived.** Each record carries only the entries its own call added, and the turn's list is the flattening of its records. `sources` is optional in the zod schema, so a conversation stored before this change still loads and simply has no list.
- **Vicinae cannot draw a footnote.** It parses markdown with `CMARK_OPT_DEFAULT` and attaches only the table and strikethrough extensions (`vicinaehq/vicinae`, `src/server/src/ui/quick/markdown-model.cpp:330-340`), so `[^1]` arrives as literal text. The whole job is done in this pipeline: `linkCitations` turns `[1]`, `[^1]` and `[1, 3]` into links, and `renderSources` builds the list from the entries the tools registered.
- **All or nothing.** A marker stays as the model wrote it unless every number in it is a source with a destination this window should open, so half a link never reads as a citation that works, and a `javascript:` address out of a search result stays inert text.
- **Maths runs before citations.** Forced by a collision found in testing: an escaped link label is exactly LaTeX's own `\[...\]` delimiter, so the maths pass would otherwise eat `[\[1\]]` and hand back a bare `1`.
- **Fences and code spans are skipped**, a footnote definition that would repeat an entry we list is dropped, and a markdown reference definition (`[1]: url`, no caret) is left alone because it backs a link the model wrote.
The list shows the sources the answer cited, falling back to everything consulted when it cited nothing, and waits for a first citation before appearing at all so it never arrives in full and then shrinks.
627 tests (45 new). Spec: `docs/superpowers/specs/2026-10-07-source-citations-design.md`, plan: `docs/superpowers/plans/2026-10-07-source-citations.md`.
One number per page per turn, assigned where the tools run so the model and
the reader agree on what [3] means. The module also owns the marker pass and
the list under the answer, because Vicinae cannot draw a footnote: it parses
markdown with CMARK_OPT_DEFAULT and attaches only the table and strikethrough
extensions, so [^1] arrives as literal text.
A marker stays text unless every number in it is a source with a destination
this window should open, and a footnote definition for a source we already
list is dropped while a markdown reference definition is left alone.
Each result now carries the number the model cites, assigned from the turn's
list rather than from its position in one call, so three searches in a turn no
longer produce three runs of 1. The entries a call added come back beside the
content, for the transcript to store and draw.
runAgent owns the list, so every call in one turn shares its numbering, and each
record carries only the entries its own call added: the turn's whole list is the
flattening of its records. The schema accepts the field as optional, because zod
drops unknown keys and a reopened conversation would otherwise lose every link.
renderModelOutput now takes the turn's sources and reports back which numbers it
linked, so the list has one source of truth for what the answer actually cited.
Maths runs before citations, not after. A link label has to escape its brackets,
and that escaped form is exactly LaTeX's own display delimiter, so converting the
model's maths first is what stops the maths pass from eating a marker and handing
back a bare 1.
The list belongs to the answer rather than to the tool line, so it is drawn with
tool calls hidden too. It waits for the answer to cite something before appearing,
because otherwise it arrives in full with the first token and then shrinks as
citations arrive. A copied conversation carries the same list, worked out from a
plain scan: it is markdown on its way somewhere else, where a [1] inside a snippet
counting as a citation costs nothing.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Every source a turn consults gets one number, and that number is the one Lam cites.
runAgentowns a per-turn list, so three searches in one turn no longer produce three runs of1, and a page found by a search and read again byweb_fetchkeeps the number it already had. The number is printed in the tool result, so the model cites a page rather than guessing a position.sourcesis optional in the zod schema, so a conversation stored before this change still loads and simply has no list.CMARK_OPT_DEFAULTand attaches only the table and strikethrough extensions (vicinaehq/vicinae,src/server/src/ui/quick/markdown-model.cpp:330-340), so[^1]arrives as literal text. The whole job is done in this pipeline:linkCitationsturns[1],[^1]and[1, 3]into links, andrenderSourcesbuilds the list from the entries the tools registered.javascript:address out of a search result stays inert text.\[...\]delimiter, so the maths pass would otherwise eat[\[1\]]and hand back a bare1.[1]: url, no caret) is left alone because it backs a link the model wrote.The list shows the sources the answer cited, falling back to everything consulted when it cited nothing, and waits for a first citation before appearing at all so it never arrives in full and then shrinks.
627 tests (45 new). Spec:
docs/superpowers/specs/2026-10-07-source-citations-design.md, plan:docs/superpowers/plans/2026-10-07-source-citations.md.