Changelog
All notable releases of trip2g. Newest on top. Each release lists user-facing
changes with What / Why / How to use so an operator or admin can decide
whether to upgrade and how to start using a feature.
Older tags (v0.2.0 and below) live in git history only.
Unreleased
Site search can leave out notes the reader cannot open
- What. A new
SEARCH_HIDE_UNREADABLEsetting (--search-hide-unreadable), off by default. When on, site search returns only notes the reader can open. When off, as before, every note the reader cannot open is listed after the results as a "Закрытый материал." placeholder with its real title and link. - Why. The placeholders show that locked material exists, which suits a paid site. A site whose closed notes should stay unseen had no way to keep their titles and links out of search.
- How to use. Set
SEARCH_HIDE_UNREADABLE=trueand restart. Admins and readers with access are not affected, and neither is MCP search. See How search works.
MCP expand declares toc_path as a list of strings again
- What. The
expandtool's input schema saystoc_pathis an array of strings, andlastis a plain number. Since v0.11.0 the schema carriedtoc_pathas an array with no item type and put that item type onlast. - Why. An MCP client that checks tool schemas strictly could refuse
expand, or the whole tool list, because of it. - How to use. Nothing to do. A client that cached the tool list picks up the fixed schema on its next
tools/list.
A signed-in reader opens a free: true note
- What. A note with
free: trueopens for every reader: a guest, a signed-in user with no access to its subgraph, and an admin. Before, a signed-in user without a grant got a paywall on a free note that a guest could open, and a form on that note answeredform_not_foundto them. The page, the GraphQLnotequery, search, similar notes, MCP, assets, live note updates and form submission all read the same check, so all of them change together. Other notes of a paid or sign-in subgraph stay closed as before. - Why.
free: truepublishes a note for everyone. Signing in must not close what a guest can read. - How to use. Nothing to do.
A guest opens a free: true note in a sign-in subgraph
- What. A
free: truenote in a subgraph marked Require sign-in opens for a guest too. Before, a guest got the sign-in wall on its page and in the GraphQLnotequery, its assets answered 401, it never reached the page cache, and RSS feeds and other.Public()lists left it out, while search, similar notes and MCP already showed it to that same guest. Now every path answers the same. A note in such a subgraph withoutfree: truestill shows a guest the sign-in wall. The sitemap lists such a note too, see below. - Why.
free: truepublishes a note for everyone, whatever its subgraphs say (en/user/subgraphs). The sign-in wall contradicted the rest of the site. - How to use. Nothing to do. Check your sign-in subgraphs for
free: truenotes you meant to keep for signed-in readers, and dropfreefrom them. A free note shows the notes it embeds with![[...]]to whoever reads it, so a free note in a sign-in subgraph that embeds a closed note now shows that closed content to guests too.
The sitemap lists a free: true note in a sign-in subgraph
- What.
sitemap.xml, the main one and each custom domain's, lists afree: truenote of a subgraph marked Require sign-in. Before, it left out every note of such a subgraph, free or not. The sitemap now asks the same question as the page, RSS feeds and the page cache, so it cannot disagree with them again. It still leaves out notes withoutfree: true,noindex: truenotes, hidden notes,.htmlfiles and anything under a/_path. - Why. Such a note opens for a guest (see above), so a search engine should find it like any other free note.
- How to use. Nothing to do. To keep a free note out of search engines, add
noindex: true.
v0.11.1 (2026-10-07)
Payment webhooks moved under /_system/
- What. NOWPayments notifications now arrive at
/_system/nowpayments/ipn, Patreon webhooks at/_system/patreon/webhook. Nothing answers under/api/any more. A NOWPayments notification is refused when--nowpayments-ipn-keyis not set. - Why. Every system endpoint lives under
/_system/; these two were the last ones outside it. - How to use. NOWPayments: nothing to do, trip2g sends the address with every invoice. An invoice created before the upgrade and paid after it still notifies the old address, which no longer answers: after the upgrade, check pending purchases and grant access by hand to any that NOWPayments shows as paid. Patreon: nothing to do, trip2g registers its webhook itself. On a normal restart the old version removes the webhook at the old address when it stops, and the new version registers the new address when it starts. If the old version did not stop cleanly, the new one still registers the new address; the leftover webhook at the old address does nothing and can be deleted in the Patreon creator portal. Patreon members are also refreshed every hour either way.
Revoke a subgraph access from the admin panel
- What.
/api/admin/revokeusersubgraphaccessis removed. In its place, the admin mutationrevokeUserSubgraphAccessand a Revoke button on an access page in the admin panel. A revoke needs a reason; the access page then shows when it was revoked, by whom and why, and the access list gains a "Revoked At" column. - Why. The old endpoint did not check the admin role, and nothing in trip2g called it. A revoke now goes through the
adminnamespace, which does. - How to use. Admin → Users → Subgraph Accesses → open the access → enter a reason → Revoke. Or call
revokeUserSubgraphAccesswith the access id and a reason (en/user/user_management).
Docs: an app on top of trip2g
- What. An app on top of trip2g now covers the whole path of a JavaScript app built on a custom layout: how the app is authorised by the visitor's session cookie, settings from the frontmatter, the site's standard header and footer around the app, saving with
expectedHash, live updates over server-sent events, API calls and admin calls, MCP from the browser over the whole base, an API of your own that asks trip2g who may call it, forms, and a small shop as an example. An API of your own goes under/_system/extra/: trip2g serves nothing there, and a test in its router keeps it that way. Templates shows the standard header and footer in any custom layout. - Why. The page described only the layout, the bundle and saving. Building a real app meant reading the source for auth, live updates and where to mount a backend.
- How to use. Nothing to do. Read An app on top of trip2g before building an app.
Docs: the default template draws no form
- What. Forms now says that the default template only embeds a note's form as JSON: it draws no fields and no captcha. To show a form, use form_template, a ready-made layout that renders the form as a survey, or a layout of your own. The page also documents the
TurnstileRequiredPayloadresult ofsubmitForm. - Why. The page said the default template rendered the form at the end of the page and handled Turnstile. It does neither, so a note with a form showed no form.
- How to use. Put
form.htmlfrom form_template into_layouts/and setlayout: formon the note. See Forms.
v0.11.0 (2026-10-07)
Previous/next links and breadcrumbs
- What. The default template shows breadcrumbs above the title and "Previous / Next" links at the end of a page. They come from the first sidebar note, left then right, that links to the page: the neighbouring links, and the heading above the link. Wikilinks and markdown links both count. Frontmatter
prev,nextandbreadcrumbsoverride them;falseturns each off. - Why. Documentation read in order had no way to the next page except the sidebar, and no sign of which section a page belongs to.
- How to use. Nothing to do if your pages already have a sidebar note. To turn it off for a folder, set
prev: false,next: false,breadcrumbs: falsewith a frontmatter patch. See Default template.
Frontmatter links resolve like in Obsidian
- What. Wikilinks in
header,footer,left_sidebarandright_sidebarresolve by the same rules as links in note text: by file name from the note that has the property, with|aliasand#headingignored. - Why. These links were matched against the page URL only, so
[[Folder/_sidebar]]with a non-Latin folder,[[Site Footer]]or[[Note|alias]]found nothing and the sidebar or header silently disappeared, even though Obsidian showed a valid link. - How to use. Nothing to do. In the default global mode a link that worked keeps pointing at the same file; one that found nothing now works. In scoped mode a bare
[[Name]]now picks the nearest file, as links in note text do. See Wikilink resolution.
Admins can hide notes
- What.
hideNotesand thehidechange inupdateNoteswork for a signed-in admin: a browser session, a personal tokent2g_…in a header or in?token=, or a session token. Thehidechange also works with a webhook token, within its write patterns. The hide is recorded against the signed-in admin, the admin who created the API key, or the admin who created the webhook. - Why. Both failed with an
errorsentry for everyone but an API key, and thehidechange failed for a webhook token the same way: the hide had no admin to record. - How to use. Nothing to do. A webhook token issued before the upgrade still gets
ErrorPayloadfrom thehidechange until it expires. See updateNotes.
A base layout can mark a slot with a bare yield
- What. A base layout may mark a slot with
{{ yield main() }}instead of{{ block main() }}…{{ end }}; the pages that extend it fill the slot with their own{{ block main() }}. Auto-import never pulls in a layout that starts with{{ extends }}: such a file is a page, not a component. - Why. Auto-import found
mainin a page that extends the base and pasted that whole page into the base. The base failed to load, and every page extending it failed withtemplate /base could not be found. - How to use. Use a bare
yieldfor a slot every page must fill, andblockfor a slot with a default. See Templates.
Wider reading column on large screens
- What. The default template's reading column grows on wide screens: 80 characters from 1440px. From 1536px the column is 56rem wide, the layout grows to 1680px with wider side columns, and the base font size stays at 18px instead of growing with the screen. Below 1440px nothing changes.
- Why. On a large monitor text was squeezed into a narrow strip and tables and code examples needed sideways scrolling.
wide: truefixed that but hid both sidebars. - How. Nothing to do.
wide: truestays for pages that need the full width (boards, big diagrams).
Wikilinks to a heading scroll to it
- What.
[[Note#Heading text]]and[[#Heading text]]now link to the heading's id, the same one the page and its table of contents use. The heading text is matched exactly, then ignoring case and extra spaces; an id such asfaq-2or one set with{#id}works too. A fragment that matches no heading now marks the link as broken, andtrip2g lintreports it asbroken link: Note#Heading. - Why. The fragment went into the URL as typed (
#Heading%20text) and matched no id, so the link opened the top of the page, and a link to a heading in the same note was marked as broken. - How to use. Nothing to do: links written the Obsidian way now work. See Markdown syntax.
"Links here" are sorted alphabetically
- What. The backlinks block at the end of a page and
nvs.BackLinks(note)in templates list notes by title (case-insensitive), then by permalink. - Why. The list was built from a map, so its order changed from one render to the next: a link was hard to find and the same page looked different each time.
- How. Nothing to do.
Layouts escape output by default
- What. A custom layout now HTML-escapes what it prints.
{{ note.Title() }}, a frontmatter value,{{ note.ContentString() }}and every other plain string reach the page as text:<,>,&,"and'become entities. HTML the server builds itself is still printed as is:note.HTMLString(),FirstListHTML(), a section'sTitleHTMLandContentHTML, a code block'sHTML,FormSpecJSON(),SubgraphNamesJSON(),asset()anddefaultTemplate.*. - Why. Layouts printed everything raw. A note whose markdown held
</textarea><script>broke out of a<textarea>{{ note.ContentString() }}</textarea>and ran its script, and a title with&or<broke the markup around it. Each layout had to remember| htmlon every value, and one missed value was enough. - How to use. Existing layouts keep working.
| unsafeand| rawstill print a string raw.| htmlandhtml(x)still escape, exactly once, so an RSS layout that writeshtml(n.HTMLString())gets the same output as before; the filter is no longer needed on text.| jsonkeeps escaping<,>and&inside JSON, so{{ x | json }}stays safe in a<script>.| urlescapes once. - Migration. Check a layout that prints markup from a plain string: a block parameter such as
title="Line one<br>line two", or HTML glued with+or returned byreplace(...). That output now shows the tags as text; print it with{{ title | unsafe }}. Values fromhtmlInjectionsHead/htmlInjectionsBodyEndstay plain strings, so keep{{ injection.Content | unsafe }}.asset()now percent-encodes",',<,>, a backslash, a backtick and whitespace in a URL; ordinary URLs, including signed storage URLs with&, are unchanged. See Templates.
Your own heading anchors, and more of a note's content for templates
- What. A heading can set its own anchor:
## Pricing {#plans}getsid="plans";{.class}adds a CSS class. In templates, a section now hasIDandLevel, andSection(...)also finds a heading ignoring case and extra spaces, or by its anchor ("plans"or"#plans"). New:Images()(Markdown images and![[...]]embeds),CodeBlocks(lang),TaskandTaskMarkon list items (Obsidian's custom statuses like[/]included), and theparseJSON,parseYAMLandparseCSVfunctions. - Why. A generated anchor changes whenever the heading text does, and breaks links to it. A template could split a note into sections but could not link to them, find a section by its anchor, or read a note's images, code blocks or task states.
- How to use. Add
{#id}at the end of a heading, see Markdown syntax. Template functions are in Templates. A heading that already ends with braces in this form, such as## Setup {#install}, loses them from its text and gets that id.
An empty frontmatter field no longer breaks sync
- What. A note with
description:orredirect:and no value (ornull,~) now loads, with the field treated as not set. - Why. Such a note used to fail the load, and the whole sync batch of a hundred notes failed with "invalid description type: ". Several pages of Obsidian's own help docs are written this way.
- How to use. Nothing to do. If sync used to fail with this error, run it again.
Search finds a note by its alias
- What. Site search and MCP search now read the
aliases(andalias) frontmatter field. When the query as a whole equals a note's title or one of its aliases, that note gets a boost worth twice a text or vector match, which normally puts it first. Case, extra spaces andё/еdon't matter. - Why. Aliases used to play no part in search: a note everyone calls "Starred" but titled "Bookmarks" could not be found by "Starred". On Obsidian's help docs, queries equal to an alias now find the right page almost every time (nDCG@5 0.81 → 0.98), and results for other queries did not change.
- How to use. List alternative names in the frontmatter, as in Obsidian:
aliases: [Starred, Favourites]. Aliases do not create new URLs.
MinIO image replaced with Silo
- What. The Compose files and the self-hosting guides now run
pgsty/siloinstead ofminio/minio(quay.io/minio/minioindocker-compose.yaml). - Why. MinIO ended its open-source distribution and deleted the
minio/minioimages from Docker Hub on 11 September 2026.docker compose upfails withpull access denied for minio/minio. Silo is the last open-source MinIO server, rebuilt and patched by the Pigsty project: same S3 API, sameMINIO_*variables, same on-disk format. - How. In your own
docker-compose.yml, changeimage: minio/minio:latesttoimage: pgsty/silo:latestand rundocker compose up -d. Keep the volume: Silo reads the existing data as is. See Self-hosting.
Sync no longer mistakes a note for a font or an image
- What. The server no longer rejects ordinary notes with an error like "Unsupported content type: application/vnd.ms-fontobject" (or
audio/mpeg,image/gif,text/xml). It now checks one thing only: the content is UTF-8 text with no NUL bytes. A binary file posing as a note is still rejected, now with "File content must be UTF-8 text". - Why. The file type was guessed from the first bytes, the way a browser does it. For text that check misfires: a note with the capital letters
LPlanding on bytes 35 and 36 (in a title such as "T HELPER" or "NLP", say) counted as an EOT font. A note starting with "ID3" counted as music, one starting with "GIF89a" as an image. Such a note never synced, and the error gave no clue what was wrong with it. - How to use. Nothing to do. If a note used to fail to sync with this error, run the sync again.
Embeds, preview blocks and links render reliably
- What. A note that embeds another note (
![[note]]) no longer comes out blank when the embedded note has no body, when embeds form a loop, or when they are chained many levels deep. A loop now shows up as an "embed cycle" warning on the note instead. The free preview (free_paragraphs,free_cut) keeps tables and callouts, each counted as one block. Wikilinks tojavascript:,vbscript:,file:anddata:addresses render without the address, as ordinary Markdown links already did. An embedded tweet is now X's standard embed, built in the reader's browser: the server no longer fetches it. Quail widgets and ads (quaily.com,quaily://) and Dify chatbots (udify.app,dify://) are no longer embedded; such a URL in image syntax renders as a plain image. - Why. Pages could lose all their content without any warning, a leading table vanished from the free preview, and a tweet embed made every sync wait on X. Pages rendered at the same moment for different visitors could also swap each other's image links in the footer and cards.
- How. Nothing to do. If a note shows the new "embed cycle" warning, remove one of the embeds that forms the loop.
noindex: true keeps a page out of search engines and sitemap.xml
- What. A new
noindexnote property. A note withnoindex: trueis left out ofsitemap.xml(the main one and every custom domain's), out of RSS feeds and any.Public()listing in a layout, and carries no JSON-LD. Its page gets<meta name="robots" content="noindex">in the default template and anX-Robots-Tag: noindexheader on every response, custom layouts included. - Why. A page meant for a direct link only (an offer, a draft, a demo) could print its own robots meta tag from a custom layout, but it still showed up in
sitemap.xml, so its URL was public to anyone who read the sitemap. - How. Add
noindex: trueto the note's frontmatter, or set it for a folder with a frontmatter patch. It is not access control: anyone with the link can still open the page. See SEO.
Closed pages say they are closed, and offer sign-in instead of a wait list
- What. A page a visitor has no access to now reads "This page is closed", tells them to ask the owner of the knowledge base for access, and shows the sign-in form right there. The wait-list block (Telegram bot, e-mail field) is off by default and appears only if you turn on the new show_waitlists config value; it still shows only when nothing is on sale.
- Why. The wait-list e-mail field was the only input on the page, while the text above it said "please sign in", so visitors typed their sign-in address into a wait list and waited for a letter that was never coming. Most knowledge bases are closed to keep them private, not to sell access later.
- How. Nothing to do: closed pages get the new text and the sign-in form automatically. If you do collect contacts while offers are not ready, set show_waitlists to true in Admin → System → Config.
Obsidian on the phone: open a note on the site, and close the warnings view (plugin v0.11.0)
- What. The note's
...menu now has Open on site and Copy site link (only for notes that are actually published), plus a new Open published URL in browser command. The sync warnings view got a Close button, and its toolbar buttons are large enough to hit with a thumb. - Why. Neither was reachable on mobile. The published-URL indicator lives in the status bar, which Obsidian mobile doesn't show, so the only route to a note's page was the "Copy published URL" command; and dismissing the warnings tab meant a detour through the tab switcher.
- How. Update the plugin to 0.11.0 or later, then tap
...on a note → Open on site. For one-tap access, pin Open published URL in browser in Obsidian's Settings → Toolbar (mobile).
Delivery chains: trace every webhook step across the agent graph
- What. Every webhook delivery now records which delivery caused it and which chain it belongs to. A new System → Delivery Chains screen lists the chains, and opening one walks its steps: what each step wrote, which write triggered it, its status, what it cost and how deep in the chain it sat. Chains span separate agent hosts (one host running the code role, another running the LLM roles) without either knowing the other exists.
- Why. When a change triggers a webhook that writes a note that triggers another webhook, the full sequence was previously invisible: you could see individual delivery records but not the chain that connected them. This makes the entire propagation path inspectable from a single admin screen.
- How. Open Admin → System → Delivery Chains and pick a chain to see its steps. Chains that wrote nothing are hidden: a schedule that finds no work still runs, and those runs would bury the rest. Turn on Show empty to see them. Nothing to configure: every delivery made after the upgrade is linked.
Agents report what a run cost in their own units
- What. A webhook agent's response now carries a
costsobject instead of the fixedtokens_usedandstepsfields:{"costs": {"tokens": 5186, "steps": 2}}. The unit is the key, so an agent that bills money reports{"usd": 0.004}and one that counts anything else reports that. The admin shows whatever arrived, per step and summed per unit over the whole chain. - Why. Tokens and steps are one executor's vocabulary. Anything else (a paid API, a credit balance, a queue quota) had nowhere to report itself, and trip2g carried a notion of "tokens" it has no business knowing about.
- How. Return
costsas an object of numbers from your webhook agent. Every value must be a number: a string or an object undercostsmakes trip2g read the whole response as unparseable, so itschangesare not applied either. The oldtokens_usedandstepsfields are gone; an agent that still sends them simply reports no cost.
Idempotent writes no longer fire change events
- What. An
updateNotescall, a sync push or a change in a webhook agent's response whose content equals what is already stored creates no new version and raises no change event. No webhook delivery is queued, no SSE event is emitted, no re-embedding runs, and no Telegram publish-view refresh happens. Changing at least one byte still fires normally. Hiding a note still works and raises no event. Writing to a hidden path brings the note back (the only way to unhide); throughupdateNotesor a sync push that counts as a change, so the restored note fires the usual events. - Why. An agent that re-applied identical content re-triggered the webhook that caused it on every cycle, at full LLM cost, bounded only by
max_depth. The fix stops the loop at the write layer: if nothing changed, nothing downstream needs to know. - How. Automatic. Agents that write back the full note content unchanged no longer cause runaway chains. The
max_depthguard remains useful for agents that do produce genuine changes.
Filter note paths by frontmatter
- What.
notePathsacceptsfilter: { frontmatter: [...] }. Each predicate names akeyand eitherexists: true,exists: falseorequals: "<string>". Every predicate has to match, and they also apply on top ofpaths,likeandsearch. They check the frontmatter of each note's latest version after frontmatter patches.equalsmatches string values only:equals: "5"does not findcount: 5, andequals: "true"does not finddraft: true. - Why. To find every note with a given property (for example, every role with
fleet_id: codellm), a script had to load the notes one by one and parse their frontmatter. - How.
notePaths(filter: { frontmatter: [{ key: "fleet_id", equals: "codellm" }] }), with an API key inX-Api-Keyas before. The server saves the frontmatter of each note version it loads. On startup it fills in the current latest and live versions. Older versions are not processed, so a key that exists only in old versions is not indexed.
A note lookup that finds nothing is nil in a layout
- What. In a custom layout,
nvs.ByPath,nvs.ByPermalink,nvs.ByWikilink, a query's.First()and.Last(),note.LangAlternative(...),PartialRenderer().Section(...), a section'sSection(...)andFirstList()returnnilwhen nothing matches. Both{{ if x }}and{{ if x == nil }}now test the result. - Why. A miss returned an empty pointer of a concrete type.
{{ if x }}treated it as empty, butx == nilwas false, so a template that checkedx == nilwent on to use a note that did not exist. - How to use. Nothing to do: templates written with
{{ if x }}keep working. To look up and test in one tag, write{{ if about := nvs.ByPermalink("/about"); about }}. See Templates.
Layouts with try/catch, exec, range _ or a hyphenated folder now load
- What. A layout that uses
{{ try }}…{{ catch err }}…{{ end }},{{ return }}in a file called withexec("path", data), or{{ range _, x := list }}now loads and renders. A page that starts with{{ extends "..." }}gets its components auto-imported.@lidnow turns every character a Jet block name cannot hold (-,., a space) into_, and puts_in front of a leading digit:my-theme/card.htmlgivesmy_theme_card,2col/card.htmlgives_2col_card. - Why. The loader's template walker stopped on these constructs, and the layout failed to load with a parse error. In a page with
extends, auto-import put the components above theextendsline, where Jet does not accept them. A component in a folder such asmy-theme/got an@lidwith a hyphen, which is not a valid block name. - How to use. Nothing to do.
yield_blocksprefixes follow the@lidform:yield_blocks("_style_my_theme_"). See Templates and yield_blocks.
Default template: code blocks, wide tables and the side menu
- What. A code block marked
jetis highlighted as a Jet template. The copy button no longer scrolls away with a long line; its label follows the pagelang(Russian forru, English otherwise), and "Copied" is announced to screen readers. A block in a language the highlighter does not know is shown as plain text. A wide table scrolls inside its own box instead of widening the page. From 1024px the left menu scrolls on its own, opens scrolled to the current page, and marks the current page with a tinted background. - Why. The copy button sat inside the scrolling code and moved with it. A wide table pushed the whole page sideways. In a long menu the current page could be out of view, and its mark was hard to see.
- How. Nothing to do.
More of the interface in the reader's language
- What. The default template's 404 and 500 pages, the "Read in:" label of the language switcher, the menu buttons' labels, the logo's alt text and the "not supported yet" text for Canvas, Excalidraw and Bases files now follow the interface language. In Russian, the editor's "Edit page" button, file search, save list and version compare, the live-reload and follow-editor toggles and the theme switch are translated too.
- Why. These strings were hardcoded in English, or their Russian translations sat where the frontend build did not pick them up.
- How. Nothing to do. The interface language comes from the language cookie or the browser's
Accept-Language.
Readers no longer download the page editor
- What. The page editor is now a bundle of its own,
/assets/ui/editor/pane/-/web.js. Only the editor frame loads it, when an admin opens the editor; the bundle every reader loads no longer contains it. - Why. The editor was part of the public bundle, so every reader downloaded code that only an admin ever runs.
- How. Nothing to do with the release binaries or the Docker image. A custom layout that prints
{{ defaultTemplate.UserSpaceScripts() }}passes the editor's URL along by itself. If you build the frontend yourself, also buildtrip2g/editor/pane(npm start trip2g/editor/pane), as the Dockerfile now does.
Name the SSO button, and turn off sign-in by email
- What. The OIDC sign-in button can carry its own label instead of "Sign in with SSO": set
OIDC_DISPLAY_NAMEfor the provider configured in the environment, ordisplayNameincreateOIDCCredentialsfor one created through the API. Sign-in by email code can be turned off:DISABLE_EMAIL_SIGNIN=truein the environment, or theemail_signin_enabledswitch in Admin → System → Config. When it is off, the sign-in screen hides the email field, its button and the captcha, andrequestEmailSignInCodeandsignInByEmailanswer with theemail_sign_in_disablederror. New GraphQL:Query.emailSignInEnabled,OAuthUrlPayload.label,AdminOIDCCredentials.displayName. - Why. "SSO" names a protocol, not the place the reader is about to go. On an instance where every account comes from an identity provider, the email form invited people to request a code for an account that does not exist.
- How. The environment variable wins: while
DISABLE_EMAIL_SIGNINis set, the admin switch cannot turn email sign-in back on. Make sure a provider works before you turn email off, or nobody can sign in, you included. An empty label keeps the default wording. See OIDC and OAuth.
Sign-in returns to the page it started on, and redirects stay on this site
- What. After signing in with Google, GitHub or OIDC, the reader lands back on the page where they pressed the button, not on the home page. Sign-in redirects now send a relative
Location, so behind a proxy that terminates TLS they no longer switch the browser from https to http. Every redirect target built from a request (the OAuth return address, and the clean URL after a?hat=sign-in link from a payment) is parsed and rebuilt as a path on this site; anything naming another host, a protocol-relative//host,/\hostor a URL with control characters becomes/. - Why. The button sent the full page URL and the server threw away every absolute URL, so each sign-in ended on
/. Behind a TLS proxy the redirect pointed athttp://, the browser did not send theSecurecookie it had just received, and a sign-in that worked looked like one that failed. The?hat=redirect copied the request line as is, so a request with an absolute-form URL could send the browser to another site. - How. Nothing to do.
Backlinks in GraphQL respect read access
- What.
NoteView.inLinkslists only the linking notes the caller can read. - Why. It returned every note that links to the current one, with its
contentandhtml, including notes in subgraphs the caller has no access to. - How. Nothing to do.
Vector search sees new notes without a restart
- What. Embeddings computed after a sync now reach vector search and similar notes while the server runs. The server re-reads the embeddings and chunks from the database after the embedding jobs finish, at most once per
EMBEDDING_RELOAD_INTERVAL(default5s) while jobs keep finishing, and once more after the last one. No note is re-rendered. - Why. Embeddings were loaded into memory with the notes, before the background jobs had written them, so a note synced after boot was invisible to vector search until the next restart.
- How. Nothing to do. During a long push search improves as jobs finish instead of after it ends.
MCP: expand reads a leaf, lists only the ends of a long note, and federation errors name the bases
- What.
expandon a section without subsections returns the section itself, the same textnote_htmlgives for thattoc_path, plussection_htmlin the structured payload. Newfirstandlastarguments onexpandandfederated_expandlist only the oldest N and newest N subsections; the summary says the listing is partial ("newest 30 of 365 subsection(s)", "… 330 subsection(s) not listed …" between the two ends) and the payload carriestotal_childrenandomitted. Afederation_not_configuredanswer now lists the bases the agent can address (connected_kb_ids), and when a peer reports the miss it also lists the bases connected directly to this hub. Search results and notes that point to another base show theirkb_idin the text. - Why. An agent reading a leaf had to call
note_htmlwith the same arguments. A note that gains a dated section a day returns a listing of tens of thousands of characters to someone who wants the latest entry. A not-configured error named no validkb_id, so the agent could not correct itself. - How.
{ "name": "expand", "arguments": { "path": "log.md", "first": 5, "last": 30 } }. Ends that meet or overlap return the whole listing. See expand.
A knowledge base in one container
- What.
Dockerfile.docsbuilds an image with a vault baked in.docker runstarts trip2g, pushes the vault into it once with the sync client from the instance's own onboarding archive, and keeps serving. No volume, no sign-in, no configuration: the database and the secrets are created inside the container at start. The internal listener is bound to127.0.0.1. By default the image holds this documentation. - Why. Shipping a read-only knowledge base (for a team, or for an MCP client) took a running instance plus a separate sync step.
- How.
docker build -f Dockerfile.docs -t trip2g-docs .thendocker run --rm -p 8080:8080 trip2g-docs. Replace theCOPYlines with your own folder, and pass-e PUBLIC_URL=...for the public address. See A knowledge base in one container.
Fleet: pause a role, and safer code runs
- What.
- A role note with
enabled: falseleaves the registry on the next poll, and its webhooks are removed, so a cron role stops firing; the note stays.--dry-runshowsSTATUS: DISABLED (enabled: false). A value other than true/false (yes/no,on/off,1/0are accepted) is a parse error. - A retried call to codellm no longer runs the code twice: fleet sends one
Idempotency-Keyper call and repeats it on every retry, and codellm answers a repeat from its record for 10 minutes. Replays are counted incodellm_exec_replays_total. - Final stdout over the limit now fails the run with "stdout limit exceeded" (HTTP 422 from
/v1/chat/completions) instead of passing on a cut-off prefix. The default limit is 10 MiB (was 1 MiB); set it withCODELLM_MAX_STDOUT_BYTESor--max-stdout-bytes. - The writes returned by the
exectool are checked as one batch before any is applied: an out-of-scope path is dropped and named in the tool result, and a patch whosefindis missing or not unique rejects the whole batch. - codellm refuses to unseal a role whose
unseal_env_keyis covered byCODELLM_EXPOSE_ENV_PREFIX, as it already did for exact names inCODELLM_EXPOSE_ENV. - The agent system prompt puts the fixed text first and the per-delivery instruction last, so a provider's prompt cache can reuse it;
fleet_llm_tokens_totalgetskind="cached"(a part ofprompt, not extra spend).
- A role note with
- Why. A cron expression that never comes due is not a reliable pause. A network retry could run code with side effects a second time. A truncated stdout looked like a result. A batch could end with half its writes applied and a success line in front of the model. A prefix allowlist could hand the seal key to the code it protects.
- How. Add
enabled: falseto a role's frontmatter to pause it, remove the line to resume. See Fleet and codellm secrets.
Admins can make a sign-in link for a user
- What. The user page in the admin has a Sign-in link button, and the admin GraphQL API has
createHatLink(input: { email, redirectUrl, expiresInMinutes }). It returns a one-click/_system/hat?token=...URL that signs that person in. The link lives 5 minutes by default and at most 60, andredirectUrlmust be a path on the same site. It signs in an existing user only: it never creates an account and never grants admin. A dead link now opens a short page that says what to do ("This link has expired", "This link doesn't work", "No account for this link") instead of an error. - Why. Getting someone in without a mail round trip meant the server CLI and its signing secret. When a link failed, the visitor saw a raw error with nothing to act on.
- How. Open Admin → Users → the user → Sign-in link, copy the link and send it. The request is written to the audit log; the link itself is not.
Personal access tokens for other users
- What. An admin can issue a personal token (
t2g_...) for any user withadminCreateUserToken(input: { userId, name, expiresInDays })(expiry 1 to 365 days, optional) and revoke any token by id withadminRevokeUserToken. Admin → Users → Personal Tokens lists every token on the instance; the Personal tokens link on a user page opens it filtered to that user, with the new-token form prefilled.CreateUserTokenPayloadhas a newinstructionsfield: a ready text with the MCP endpoint and the token, to hand to whoever will use it. In the account dialog, tokens got their own screens, and the account home shows the signed-in email and starred notes. - Why. A token could only be created by the user for themselves, so giving an agent access meant signing in as that person first.
- How. Admin → Users → the user → Personal tokens → new token. Copy the instructions block once: it already carries the token. See MCP.
Closed pages answer 401 or 403 and leak no description
- What. A page shown behind the sign-in wall or the paywall now answers
401to a visitor who is not signed in and403to one who is signed in but has no access, withCache-Control: no-storeon both. The page body is the same wall as before. A closed note also gets no automatic<meta name="description">from its first paragraph; only an explicitdescriptionin its frontmatter is used. - Why. Walls were served with
200, so crawlers, link previews and API clients took them for the real page. The description fallback was built from the note body and printed in the<head>of the wall itself, so the opening lines of a closed note were readable without access. - How. Nothing to do. To give a closed page a description for search results, set
descriptionin its frontmatter.
Site logo loads for anonymous visitors
- What. Images used in
_header.mdand_footer.md(in any folder) are served to visitors who are not signed in, whether or not those notes arefree. Assets of other system notes follow the same rule as a normal note: anonymous when the note is readable without a session. - Why. The site logo answered
401to every anonymous visitor: assets were public only when an owning note could be listed publicly, and system notes such as_header.mdnever can. Admins are always signed in, so they did not see the problem. - How. Nothing to do. Chrome attached under another name with the
header:frontmatter field still needsfree: truefor its images to be public.
Links keep the reader on the host they came to
- What. On a custom domain, the header, footer, sidebar, backlinks and template queries now render links the same way as the page body: a link to a note on the same host is relative, not an absolute URL. A note reached through its own
routefrontmatter is served at that path instead of being redirected to its permalink. - Why. Shared chrome was rendered once for the main domain, so on a custom domain its links pointed back to the main domain or to an absolute URL that does not resolve on an internal or preview host. The route redirect depended on whether the file name had characters that get transliterated, and on a custom domain it led to a 404.
- How. Nothing to do.
rel=canonical,og:urland JSON-LD stay absolute.
MCP: standard transport, federated arguments and search fixes
- What. The MCP endpoint
/_system/mcpnow runs on the official Go MCP SDK (Streamable HTTP, stateless, JSON responses). Tool names, descriptions and schemas did not change. Along with it:- Federated calls pass every argument through to the peer.
federated_searchused to send only the query, solimit,detail_limitand similar were lost;federated_note_htmlnow takestoc_path. A call to a singlekb_idhas the same timeout as a fan-out call. - An API key sees KB-notes (
mcp_federation_kb_url) that are notfree, as it sees every other note. Before,federated_*calls on such a KB answered "not configured". - Search chunks no longer start with a broken character in non-Latin text.
expandleaves out the title H1 and shows a short preview after headings that are too short to tell apart.toc_pathfromsearchis trimmed the same way.- Notes registered as tools (
mcp_method) no longer show up in search results. - Each search match has
section_url, a link to the matched heading on the site.
- Federated calls pass every argument through to the peer.
- Why. The hand-written protocol layer drifted from the spec, and federated agents got different results from the same call made locally.
- Migration. A request must send
Accept: application/json, text/event-stream; without it the endpoint answers400. Real MCP clients send it. Update curl scripts and custom integrations (the stdio adapter in the docs is updated). An instance on an older version calling this one through federation does not send the header, so update both sides. Other visible changes:notifications/initializedanswers202with an empty body,tools/listis sorted by name. See MCP.
Reranking is a per-search choice
- What. With a reranker configured, each search can ask for it: a
rerankargument on the MCPsearchandfederated_searchtools, andrerankon the GraphQLSearchInput. A newvector_search.reranker.defaultinFEATURESdecides what happens when a request says nothing; it isfalse. The MCP argument is shown only when a reranker is configured. - Why. The cross-encoder costs about a second per candidate on CPU, so a search with
top_n20 took about 20 seconds. That is acceptable for an agent doing research, not for a person at a search box. - Migration.
reranker.enabled: trueno longer reranks every search. To keep the old behaviour, add"default": trueto thererankerobject. This also applies tomemcli up --reranker.
Full-text search index can live on disk
- What. A new
SEARCH_INDEX_PATHsetting (--search-index-path) keeps the full-text index in that directory instead of in memory. A restart reopens the existing index instead of rebuilding it, and notes deleted while the server was down are removed from it on the next load. Empty keeps the in-memory index, as before. - Why. The in-memory index costs about 35 times the size of the text. On a vault with 10 MB of markdown it held 350 MB of heap, and large vaults ran out of memory on restart. On disk the same vault takes 4 MB of heap and about 45 MB of disk, and a restart opens the index in milliseconds.
- How. Set
SEARCH_INDEX_PATHto a directory on a persistent volume. Do not point two instances at the same directory.
Federation: one handover key, rotated on install
- What. Adding an inbound secret now gives one handover key that already carries the address and key ID; the peer pastes it into Add Outbound. Before the row is stored, the receiving hub asks the issuing peer to adopt a fresh random key, so the value that went through a chat stops working. An outbound key's page has a Rotate key button (also
rotateFederationSecret), shows when the key last rotated, and lists what the peer granted, read from the peer's new/_system/mcp/federationendpoint. Subgraphs have a one-line description that the peer sees next to the name. The admin list shows each key's direction. - Why. A shared key handed over in a message stays in that message history, and in an agent's transcript when an agent relayed it. Operators also had no way to see what a pairing actually granted.
- How. Add Inbound → copy the handover key → send it → the other side pastes it into Add Outbound. The Replace the key before storing it switch is on by default; turn it off for a peer that cannot rotate (a public base, an adapter, an older instance), otherwise the peer refuses and nothing is stored. Rotation is refused against an
http://peer unless private federation addresses are allowed. See Federation.
Webhook agents: run logs, longer history, private addresses
- What. A webhook agent can return
logs, a list of{ts, level, msg, data}entries, and the admin shows them on the chain step under Log. Up to 500 entries or 64 KB are kept per delivery; the rest is replaced by one entry saying how much was dropped. Delivery records and their request and response bodies are now kept 90 days, set with--webhook-deliveries-retentionand--webhook-delivery-logs-retention. A new--webhook-allow-private(WEBHOOK_ALLOW_PRIVATE) lets webhooks reach private and internal addresses without turning on dev mode. Notes that an agent returns in its response body now trigger change webhooks, SSE and frontmatter indexing like any other write, and count one level deeper in the chain. - Why. An operator could see that a run failed but not what the agent did. Records went away after 30 days and the bodies after one day, before anyone looked. Agents running next to trip2g on a private network were blocked by the SSRF guard. Notes written through the response body did not trigger the next role in a chain.
- How. Return
logsnext tochangesandcosts. Bodies are now stored 90 times longer, so plan disk space or set the retention lower.
Fleet signs in with a personal token, and agents cannot write role notes
- What. Fleet authenticates with an admin's personal token (
--trip2g-admin-personal-token,TRIP2G_FLEET_TRIP2G_ADMIN_PERSONAL_TOKEN) instead of the server's JWT secret. SetOWNER_PERSONAL_TOKEN_VALUEon the server to at2g_value and it seeds that token for the owner (OWNER_EMAIL) at boot;memcli upgenerates one. Fleet refuses a write or patch from an agent that would create or edit a role note (any note withfleet_idin its frontmatter), and a patch applies only if the note is unchanged since that check.--allow-role-authoringturns the guard off. Fleet and codellm also serve Prometheus/metrics, pprof and health probes on a loopback listener:--metrics-addr(fleet127.0.0.1:18090, codellm127.0.0.1:18087,CODELLM_METRICS_ADDR); empty turns it off. - Why. With the JWT secret, fleet could mint a session for anyone and could not be cut off without changing the secret. A role declares its own
write_patterns, so an agent tricked by note content into writing a role note could give itself more access. - Migration.
--jwt-secret,--admin-emailand--admin-api-keyare gone from fleet. SetOWNER_PERSONAL_TOKEN_VALUEon the server and pass the same value to fleet. Revoking that token in the admin stops fleet within about half a minute. See Fleet.
Code roles: a toolbox, fleetkit and sealed secrets
- What. The codellm image is now Debian-based with Python 3, Node 24 and common tools (
jq,sqlite3,git,gh,ripgrep,curland others) plus libraries such asrequests,httpx,pydantic,jsonschema,axios,zodandajv. Afleetkithelper for Python and Node builds the output a code role prints (note,write,patch,emit) and reads its input (bag,frontmatter,secrets,note_frontmatter). A role note can carry encrypted values: list them inunseal, and codellm opens them with itsSEAL_KEY(or the key named inunseal_env_key) for that run only. Values are sealed withcodellm sealor the form at/_system/codellm/seal(CODELLM_SEAL_PATH). A role can narrow the env vars its code sees withenv_passthroughandenv_prefix, within the operator's allowlist. - Why. Code roles had bare Python and no libraries, assembled notes and JSON by hand, and every credential meant editing the codellm deployment and restarting it.
- How. See Fleet and Secrets for code roles.
Re-embed all notes from the admin, and copy a heading link
- What. The admin Note Views page has a Re-embed all notes button. It calls
regenerateNoteEmbeddings(input: { force: true }), which queues every note even when its stored hash says it is up to date; chunks whose own hash did not change are not re-sent to the model. In the default template a#link appears next to a heading on hover. - Why. After a change to chunking, every note still looked up to date, so the new chunks never reached the vector index. Linking to a section meant reading its id from the table of contents.
- How. Admin → Note Views → Re-embed all notes. Needs vector search to be on.
trip2g-sync CLI: a graphql command, and a failed fetch stops the sync
- What.
trip2g-sync graphql '<query>' ['<variables>']runs a GraphQL query against the instance the vault is set up for, using the key in.obsidian/plugins/trip2g/data.json;graphql --introspect '<pattern>'shows matching schema types. Queries go through MCP, where an API key carries its admin rights. If the CLI cannot fetch the server's note list, it now stops with an error and exit code 1. Bare asset links such as![[logo.png]]resolve to the file anywhere in the vault (the shallowest match wins), in the CLI and in browser sync. The downloaded onboarding vault now includesAGENTS.mdandCLAUDE.mdfor AI agents working in it. Thetrip2g-syncCLI (plugin 0.12.0) prints what will happen to each non-empty line of the sync plan, for example that "Remote only" files are downloaded as new local files and "Local deleted" notes are hidden on the server. Its help now says that deleting a file locally hides the note on the server with no flag needed, and that--two-waydownloads notes that exist only on the server. - Why. A failed fetch was read as an empty server: the run reported every local note as deleted on the server and exited 0. An asset link without a folder found nothing unless the file sat next to the note.
- How. Run from the vault folder, for example
node .obsidian/plugins/trip2g/trip2g-sync.mjs graphql '{ viewer { id } }'. These CLI changes ship with plugin and CLI 0.12.0.
Files in notes are served by the site, with access checks
- What. Images and other files from notes and layouts are served at
/_system/assets/<sha256>/<file name>. They are no longer presigned links to the storage server (MINIO_PUBLIC_URL) or, with local storage,/_assets/.... A file used by a layout or by a note anyone can read is public and cached for a year as immutable. Any other file needs a signed-in reader who can read at least one note that uses it, a valid API key inX-API-Key, or an admin. Such files are cached privately for 5 minutes, so the access check runs again after that. A file that no current note or layout uses is for admins only. Range requests andETagwork. URLs that leave the site (Telegram posts,og:image, JSON-LD, RSS, the asset URLspushNotesreturns) are made absolute with the site's public URL. - Why. A presigned link expired after a few days and worked for anyone who had it, even for a file from a paid or closed note. It also needed the storage server to be reachable from readers' browsers.
- How. Nothing to do for content: pages use the new URLs as soon as the server runs the new version.
MINIO_PUBLIC_URLandMINIO_URL_EXPIRES_INare now ignored, and the storage server no longer needs a public address. Old links copied from pages before the upgrade stop working.
Forms: submit checks note access, a misconfigured captcha blocks, forms: works
- What.
submitFormfirst checks that the visitor can read the note the form belongs to. If not, it answersform_not_found, the same as for a missing form. Every text and email field is capped at 8192 characters, andmin_length/max_lengthnow count characters, not bytes. Ifturnstile-site-keyis set butturnstile-secret-keyis empty, the Turnstile check now fails instead of letting every request through; it also guards the email sign-in captcha. Several forms on one note through theforms:key now work. - Why. The note version id in a submit is easy to guess, so a guest could post to a form on a paid, sign-in-only or subgraph note. A half-configured Turnstile looked like it was protecting forms but checked nothing. A field had no size limit other than the request body limit.
forms:was never found in real frontmatter, because the YAML parser returns its keys in a type the code did not expect. - How. Nothing to do. If you set only the Turnstile site key, add the secret key, or remove both to turn the captcha off. Non-Latin text that was close to its
max_lengthnow has more room, since the limit no longer counts bytes. See Forms.
Without SMTP, sign-in codes go to the server log
- What. When
SMTP_HOSTis empty, requesting an email sign-in code works again. The code is written to the server log at warning level together with the email.LOG_SIGN_IN_CODES=truestill logs codes when SMTP is configured. - Why. The previous release made a code request without SMTP return an error unless
LOG_SIGN_IN_CODESwas set, which locked dev setups and fresh installs out of email sign-in. - How. On a public server, configure SMTP: anyone who can read the server log can sign in as any user who asks for a code. See SMTP.
A custom layout that fails returns an error page
- What. A custom layout is rendered into a buffer first. If it fails, nothing from it reaches the reader. The response is status 500 with the default template's error page. An admin sees the layout name and the Jet error with its line on the same page. Everyone else sees a generic error.
- Why. Jet writes everything up to the failing expression before it stops. Visitors could get half a page, and admins got the raw error text glued to the end of that half page.
- How. Nothing to do. Open the page as admin to see what broke.
Layout warnings reach the sync tool, which stops on critical ones
- What.
pushNotesnow returns the warnings of each layout file. Before, they were replaced with an empty list. The CLI sync tool (trip2g-sync.mjs) prints warnings grouped by file, withCRITICALin red, and exits with code 1 if any warning isCRITICAL(for example, a layout that does not parse). A newWARNINGreports a layout that writes its own expanded@lidname as plain text (mesh-barinmesh/bar.html): a renamed file keeps the old name there, while the placeholder would follow the rename. Matches inside HTML comments and URLs are ignored. - Why. A broken layout was pushed without a word, and the site found out at render time.
- How. Update the CLI and run it as before. In CI, a critical layout warning now fails the job. See CLI sync tool.
coalesce() in layouts
- What. A new global function for Jet layouts:
coalesce(a, b, ...)returns the first argument that is set and not empty, otherwise the last argument. A missing map key,nil,"", an empty list and an empty map count as empty;0andfalsecount as values. - Why. Jet's
||returns a boolean, sovideos[lang] || videos["en"]could not pick a fallback value. - How.
{{ coalesce(videos[note.Lang()], videos["en"]) }}. See Jet functions.
Mermaid diagrams: a second renderer, zoom, fullscreen and export
- What. Flowchart, state, sequence, class, ER and xychart diagrams are drawn by beautiful-mermaid. Other types (gantt, pie, gitGraph, mindmap and so on), and any diagram it fails to parse, use mermaid.js as before. Each diagram gets buttons for zoom in, zoom out, reset, fullscreen and PNG / SVG export, and can be dragged. A diagram is at most 80% of the screen height and is scaled to fit its box.
- Why. Dragging a diagram never worked. Large diagrams shrank until they could not be read, and wide, short ones became a thin strip.
- How. Nothing to do. See Mermaid.
Magazine: choose how many cards are large, medium and small
- What. Three frontmatter keys on the page that hosts the magazine:
magazine_featured(large cards, default1),magazine_grid(medium cards after them, default4) andmagazine_grid_columns(columns in the medium grid, default2). The rest of the notes go to the list.0skips a tier, and counts larger than the number of notes are capped. Card excerpts also skip tables now. - Why. The layout was fixed at one large card, four medium ones and a list. A table at the start of a note broke its card.
- How. For example,
magazine_featured: 0andmagazine_grid: 8give no large card and eight cards in the grid. The defaults keep the old layout. See Default template.
Widgets in the content: list
- What. The default template's
content:list now acceptstoc,backlinks(orinlinks),outlinksandsimilar, the same keywords as the sidebars. Each one renders its block in the main column. - Why. The template could already draw these blocks in the main column, but
content:treated the words as file names and showed nothing. - How. For example,
content: [self, backlinks, similar]puts backlinks and similar notes under the article.
Callouts in Telegram posts
- What. Obsidian callouts (
> [!note] Title) in a Telegram post become a quote with the title in bold on the first line. A callout without a title uses its type, capitalized. A collapsed callout ([!type]-) becomes an expandable quote that shows only the title until opened. - Why. The Telegram converter did not know callouts: it dropped the whole block and logged
unexpected markdown node. - How. Nothing to do: write callouts as in Obsidian.
Webhook tokens stay inside their write scope
- What. The short-lived token a webhook passes to an agent (Pass API key) is now held to its
write_patternseverywhere it can write.uploadNoteAssetchecks that the note belongs to a path the token may write.pushNotes,hideNotesandcommitNotesrefuse such a token withErrorPayload, because they have no per-path scope. InupdateNotes, a token with nowrite_patternscan no longer write anywhere. - Why. These calls let a token write outside its
write_patterns: attach files to any note, push or hide any note, or, in some cases, write anywhere throughupdateNoteswhenwrite_patternswas empty. - How. Nothing to do for agents that use
updateNoteswithin their scope. An agent that calledpushNotes,hideNotesorcommitNoteswith its webhook token must switch toupdateNotes(thehidechange hides a note). See Webhooks.
Note graph: groups, saved positions and live readers
- What. The admin note graph (Notes & Content → Note Graph) has Auto layout, Save positions and Public outside in its header, and a Group by: field. Notes with the same value of that frontmatter key (default
subgraph) are pulled into one cluster. A dragged node keeps its position. While the graph is open, signed-in readers moving between notes show up as animated moves, also available as the admin-only GraphQL subscriptionreaderMoves(admin session or instance API key). A reader appears under an anonymous key that changes every hour, never as a user id. The server does no extra work for this while no one is subscribed. - Why. Notes were spread at random, the layout was lost on every reload, and there was no way to see how people move through the site.
- How. Open the graph, type a key into Group by: and press Auto layout. To group whole folders, add a frontmatter patch. See Note graph.
Fleet: roles pick a fleet by fleet_id, code runs in codellm
- What. A role chooses the fleet that runs it with the
fleet_idfrontmatter key. Fleet's--fleet-id(TRIP2G_FLEET_FLEET_ID) is now required and has no default (it used to be a marker that defaulted tofleet1). A fleet runs only roles with its ownfleet_id. A role withoutfleet_idruns nowhere, and fleet reports it as an error during discovery. Theexecutorkey is gone. Code roles run in codellm, a separate service with an OpenAI-compatible API (cmd/codellm, imageDockerfile.codellm). A fleet whose--llm-base-urlpoints at codellm runs role bodies as code. Fleet no longer runs code itself: its--allowed-programsflag is gone, and the interpreter list, sandbox and network access are codellm settings (CODELLM_ALLOWED_PROGRAMS,CODELLM_SANDBOX,CODELLM_SANDBOX_NETWORK). codellm can require a key (CODELLM_API_KEY), which fleet sends with--llm-api-keyor--exec-api-key. Fleet also serves a read-only GraphQL API with the roles and how they trigger each other (--graphql-addr, default127.0.0.1:9093), open only to admins of the hub. - Why. Code execution and its sandbox lived inside the agent process. Moving them to their own service lets a code role and an LLM role run on different hosts, and a role moves between them by changing one line.
- How. Before upgrading fleet: add
fleet_idto every role, start one fleet perfleet_idwith a matching--fleet-id, and for code roles run codellm and point that fleet's--llm-base-urlat it. Move--allowed-programsto codellm'sCODELLM_ALLOWED_PROGRAMS. See Fleet.
Smaller fixes
- A revoked personal token stops working at once.
revokeUserTokenandadminRevokeUserTokennow drop the token from the server's cache. Before, a revoked token kept working for up to 30 seconds. - A custom domain with nothing to index gets an empty sitemap. Its
/sitemap.xmlis an empty<urlset>. Before, it served the main domain's sitemap, with the pages of another site. See Multi-domains. - Browsers pick up new built-in scripts and styles after an upgrade. Files under
/assets/now carry anETagand aCache-Controlheader: a URL with the file's current?h=hash is cached for a year as immutable, any other URL for an hour and then revalidated. Before, the server answered everyIf-Modified-Sincewith "304 Not Modified", because embedded files have no modification time, so a browser could keep an old script after an upgrade. - Faster asset sync.
uploadNoteAssetchecks whether the file is already stored before it parses the note, and a layout version loads only the_layouts/files instead of every note. Re-syncing a vault full of images no longer parses each note once per image. - Commit in the startup log. The server logs
trip2g startingwith the gitcommitit was built from. - Email check. Creating or editing a user in the admin, and
createHatLink, check only that an address is well formed. They no longer look up the domain's MX record, so a domain without mail no longer fails and the form no longer waits on DNS. - Long inline code. In the default template a long URL or path in inline code wraps instead of making the page wider.
- Admin dates. The audit log and the wait-list screens show their dates correctly; they were formatted twice.
- $mol in the header. A $mol component in the header no longer takes the whole header row while its styles load.
- Admin Sign out. The Sign out button in the admin panel signs you out. Before, it did nothing.
- Callout titles. In the light theme, callout titles use the callout's own color and are readable. A callout type that starts with a non-Latin letter (
[!заметка]) gets a correct capital letter in its default title, on the site and in Telegram. - Telegram rate limits. Sending and editing Telegram posts (by bot and by account) makes at most three attempts when Telegram answers with a rate-limit error, and the wait between them stops when the server shuts down. Before, the retries had no limit.
- Storage at startup. The server waits up to 45 seconds for the S3 storage to come up instead of exiting on the first failed bucket check. Wrong credentials or a bad bucket name still fail at once.
MINIO_SECRET_ACCESS_KEYis accepted as the secret key name. - Missing files in storage. When a client uploads a file whose record already exists but whose data is gone from storage (for example, after the bucket was wiped), the server stores the file again. Before, it only linked the record and the file stayed missing.
- Starter vault name.
/_system/onboarding-vault?name=<name>sets both the zip file name and the folder inside it. Letters, digits, dot, dash and underscore, up to 64 characters. An invalid name returns 400.
v0.10.0 (2026-07-13)
RSS is now a template, not a Go feature
- What. The old automatic
<permalink>.rss.xmlfeed (one per note, items = the note's links) is gone, along with itsenable_rsssite setting. RSS is now a normal Jet layout: a note withlayout: rss+content_type: application/rss+xml; charset=utf-8frontmatter renders an RSS 2.0 feed, served wherever you route it (default:/feed.xml). Which notes appear is controlled byrss_glob/rss_limitfrontmatter, and only publicly readable notes (free, not sign-in-gated, not system) are ever included. - Why. The per-note curated-links feed was rigid and undiscoverable. A template-based feed is fully customizable — edit
_layouts/rss.htmlto change item shape, or build several feeds with different scopes — using the same layout system as everything else on the site. - How. See en/user/rss. Existing
.rss.xmlsubscriber URLs 404 now; there's no redirect — set up a/feed.xmlnote to replace them.
Theming the default template + a live theme editor
- What. Two new pages document how to re-skin the default template. en/user/themes explains that the whole look comes from Pico CSS
--pico-*variables — override them in a<style>block pasted into admin HTML Injection and the site re-themes at once, no rebuild or fork. en/user/theme-editor renders a live editor right on the site: drag the variables, watch the preview, and (signed in as admin) click Save to write the theme into your site's HTML injection. The editor is a custom Jet layout (layout: theme_editor), installable on any site from trip2g/theme_editor_template. - Why. Changing the look used to mean reading the stylesheet and guessing which variable to touch. The editor makes theming a drag-and-save loop, and doubles as a worked example of a custom template that talks to the admin API (same pattern as the kanban board).
- How. Read en/user/themes for the variable list and where to paste. To try the editor, open en/user/theme-editor; to install it on your own site,
curlthe layout from the template repo and add a note withlayout: theme_editor.
Embedded notes with a custom CSS class
- What. New page en/user/embedded-notes documents
![[note-name]]— the target note's content renders inline, wrapped in<div class="embedded-note">. Addembed_class: my-classto the embedded note's frontmatter and the wrapper also getsembedded-note__my-class, a hook you can style from the admin. Any note becomes a reusable, styled block: a callout, a promo card, a shared footer. - Why. Reusable partials (a signup banner, a shared header) had no documented pattern. Combined with
_-hidden notes, one edit updates the block everywhere it's embedded. - How. See en/user/embedded-notes. Keep the block in a
_hiddennote, addembed_class:for styling, and embed it with![[_block]]wherever you need it.
Telegram group ↔ subgraph access (two-way)
- What. New page en/user/telegram-access documents linking a Telegram group to a subgraph so access flows both ways: group members can read the subgraph's notes on your site, and readers with subgraph access can be invited into the group. Each direction is a separate switch, and both are live — leave the group and the notes close; let access lapse and a background job removes you from the group. Backing this, membership now tracks Telegram
chat_memberupdates so joins and leaves register immediately, and the bot's/contentmenu points at the right subgraphs. - Why. Gating content by paid-group membership was possible but underdocumented, and membership changes weren't always reflected. This makes a Telegram group a first-class access source, on equal footing with a paid offer or a hand grant.
- How. Add your bot as a group admin, link the group to a subgraph in Admin → TG bots (one section per direction), tag the notes with
subgraph:, and members use/contentto open the gated notes. Full walkthrough in en/user/telegram-access.
Git access to your site
- What. New page en/user/git documents that every trip2g site is a git repository:
git clone https://your-site.com/_system/git, edit markdown with any tool or agent, commit, and push — the server applies your commits to the live site. Auth is HTTP Basic (user+ a git token created in Admin → Integrations → Git tokens), and token scopes now enforce pull vs. push separately, so a read-only token can clone but not write. - Why. Batch edits, CI jobs, and coding agents need standard git tooling, not a bespoke plugin. Scoped tokens make it safe to hand a read-only clone URL to an automation.
- How. Create a git token in the admin,
git clonethe_system/gitURL, and push to themasterbranch (the only branch the server accepts). For day-to-day writing in Obsidian, the sync plugin is still the better fit. See en/user/git.
Subgraph recipes: private-by-default, all-free, and _-hidden notes
- What. en/user/subgraphs gains three practical recipes. A private-by-default frontmatter patch assigns every untagged note to a reserved subgraph nobody is granted, so nothing leaks unless you publish it explicitly. A symmetric all-free patch sets
free: truevault-wide for a fully public site. And a clarified section documents that any file or folder whose name starts with_is hidden from listings, search, and RSS (but still reachable and embeddable). - Why. "Which notes are visible to whom" is the most common subgraph question. These give copy-paste starting points for the two ends of the spectrum — private-first and public-first.
- How. In Admin → Notes & Content → Frontmatter Patches, create a rule with the jsonnet shown in en/user/subgraphs (private-by-default or all-free), and create the reserved
privatesubgraph with no grants.
v0.9.0 (2026-07-12)
First login on a fresh self-host box
- What. Bringing up the first admin login on a brand-new box no longer needs an email server. The recommended path:
trip2g-server login-linkprints a one-time, 5-minute sign-in link you open straight from the terminal —/_system/hatnow accepts the token over GET, so the link is clickable into a browser. Prefer the email-code flow without a mail server? SetLOG_SIGN_IN_CODES=trueand the server writes sign-in codes to its log (read them fromjournalctlordocker logs). Two related fixes back this up: requesting a code with no email transport now returns a clear error instead of silently pretending to send one, andSMTP_STARTTLS=falseis honored so a local plaintext relay (Postfix, MailHog) works. - Why. First login on a fresh box — no domain, no email service — used to be a dead end: the server accepted the request but the code never appeared anywhere, and even a local relay failed on an unwanted STARTTLS upgrade. Now one command gives you a working login link, with a log-based fallback and honest errors.
- How. Run
trip2g-server login-linkon the box (ordocker execinto the container) and open the printed URL within 5 minutes; re-run it anytime for a fresh link. Fallback: setLOG_SIGN_IN_CODES=true, request a code from the login page, and copy it from the log. Remove these bootstrap options once you set up OAuth or a real email transport — see en/user/smtp.
Search: one retrieval engine for site and MCP
- What. The site (GraphQL) search and the MCP
searchtool now share one retrieval engine (text + vector + rank fusion) instead of two divergent copies. Two behavior changes for MCP clients: anonymous (and federation) clients now search published (live) notes, like anonymous site visitors — previously they searched the latest versions, including drafts of public notes; and vector search now skips stale embeddings after an embedding-model switch instead of ranking them arbitrarily. The unused server-rendered/searchpage is removed (the site search widget uses GraphQL and is unaffected). - Why. Retrieval fixes and tuning landed on one surface and silently missed the other; agents and visitors could see different rankings for the same query. On sites with draft previews enabled, anonymous MCP clients could read draft content of public notes.
- How. Automatic. If an agent integration relied on anonymous MCP search seeing drafts, authenticate it with an API key — API-key clients still search the latest corpus.
SMTP provider guide
- What. A new bilingual documentation page, en/user/smtp, covers how to pick a transactional-email provider for a self-hosted trip2g instance. It lists the envelope configuration (
SMTP_HOST,SMTP_PORT,SMTP_FROM, etc.), rates the main provider categories (dedicated transactional services, domain registrar mail, free tiers), and gives the verdict: don't run your own outbound MTA. - Why. Email deliverability for self-hosted instances is a frequent setup question. The available config keys were documented piecemeal; there was no single place explaining the tradeoffs.
- How. Read en/user/smtp before configuring email on a new instance.
Instagram frames (instaframes)
- What. The instaframes feature now has a bilingual user guide and a gallery landing page. Carousel pages also gain an in-page edit widget: a button that opens the slide deck for editing without leaving the published page. The deprecated reel/teleprompter layout is removed — the carousel layout is the only instaframes layout.
- Why. The guide and gallery make the feature discoverable. The edit widget closes the feedback loop between publishing and editing: you see a slide, notice a typo, and fix it on the spot. The reel layout had no active users and duplicated the carousel's code path.
- How. See en/user/instaframes for the full guide and gallery. The in-page edit widget appears automatically on carousel pages when you are logged in as the site owner.
Admin dashboard fixes
- What. Two small corrections to the admin dashboard: the onboarding documentation link now points to the correct page (the previous link was a typo that 404'd), and the storage-usage figure is now labeled MiB to match the actual byte count displayed.
- Why. The onboarding link was the first thing a new operator clicked; landing on a 404 was a poor first impression. The old "MB" label implied decimal megabytes but the code reported binary mebibytes.
- How. Automatic on upgrade.
MCP / federation improvements
- What. Four targeted improvements to the MCP endpoint and federation layer. (1) A new
federated_instructionstool lets a connected client fetch the instruction text for any route, with a per-route cache to avoid redundant fetches. (2) Federation hop depth now uses inclusive semantics: a request withmax_depth=2may traverse exactly 2 hops; previously it stopped one hop short. Requests that exceed the limit now return a clean rejection instead of an opaque error. (3) Hop-rewrite propagates the caller'skb_idframe into federated results and errors, so the caller can always tell which peer an answer came from. (4) Note-read tools (note_html,note_text) now steer bypathormatch_idand document thatnote_idis an internal integer not suitable for long-term references. - Why. The depth off-by-one meant that
max_depth=1allowed zero federation hops — a configuration that appeared to work but never reached any peers. The hop-rewrite makes it possible to build attribution chains across federated vaults. Clear note-ID guidance prevents agents from caching internal IDs across syncs. - How. No configuration change needed. If you relied on the old depth semantics, decrement your
max_depthvalue by one to preserve the previous behavior.
MCP graph-walk visualizer
- What. A new interactive documentation page, en/search_visualizer, lets you watch an AI model walk the federation graph in real time. It shows each tool call as a node, a mini-map of the federation topology, model chips to switch between providers mid-session, a step-spine axis timeline, and a trace-import panel to replay downloaded walk JSONs. The step budget is 50, enough for deep multi-hop descents.
- Why. Federation graph traversal is opaque: it is hard to reason about why an agent took a particular path or where latency came from. The visualizer makes the walk inspectable.
- How. Open en/search_visualizer, pick a model and a starting knowledge base, and run a query. The walk unfolds node by node. Download the trace JSON to replay or share it.
memcli: local embedded vector search
- What.
memclinow bundles an arm64-native retriever sidecar that runs vector search locally, without sending text to an external service. The sidecar handles both embedding and (optionally) reranking. - Why. Vector search on a local memcli instance previously required a network call to an embeddings server. The bundled retriever makes local semantic search self-contained on Apple Silicon.
- How. The sidecar starts automatically with
memcli upon arm64 macOS. No extra configuration is required. Guide: en/user/memcli.
Retriever: Metal/MPS support and UTF-8 snippet safety
- What. The retriever sidecar now runs inference on Metal (Apple GPU) and MPS backends with fp16 precision and micro-batch stability fixes. Search result snippets are cut on rune boundaries so non-ASCII text (Cyrillic, CJK, emoji) is never truncated mid-codepoint.
- Why. Inference on CPU was 3–10× slower than on the GPU. Truncated UTF-8 caused garbled snippet text in multilingual vaults.
- How. Automatic. The retriever selects the best available device at startup. No configuration change is needed.
Ops metrics: embedding pipeline and job queue
- What. Two new Prometheus metric groups are now exposed on the internal metrics endpoint. (1) Embedding-pipeline metrics track chunk count, latency, and error rate for each embedding pass. (2) Job-queue depth metrics report how many jobs are waiting in each named queue. Additionally, chunk embedding requests are now sub-batched to fit the retriever server's declared maximum batch size, preventing a re-embed storm when the batch limit is lower than the chunk count.
- Why. Without queue-depth metrics, operators had no early warning of a backing-up embedding pipeline. The sub-batching fix prevented a crash loop where an oversized batch caused the embedder to reject the request, which triggered a full re-embed, which produced another oversized batch.
- How. New metrics appear on the existing internal metrics endpoint with no configuration change. Update your dashboards to add queue-depth and embedding-latency panels.
Telegram: navigation browser limited to public live notes
- What. The in-Telegram navigation browser now shows only public, live notes. Previously it could surface notes that were private or in draft state.
- Why. A Telegram user clicking a navigation link should land on content that is actually accessible to them. Draft and private notes produced broken or access-denied pages.
- How. Automatic. Draft and private notes no longer appear in the Telegram navigation browser.
v0.8.0 (2026-07-01)
Fleet agent runtime
- What. Trip2g notes can now trigger LLM agents. You write "role notes" in a folder (default
roles/): frontmatter is the config (model, tools, read/write path patterns, trigger, budget limits, concurrency policy), body is the instruction rendered as a Jet template against the trigger context. Thefleetdaemon discovers role notes, reconciles change/cron webhooks to itself, and when a trigger fires runs a scoped agent loop with tools: search, read_note, patch_note, write_note. An optionalexecutor: codetool (e.g., Python) is off by default and gated by--allowed-programs/TRIP2G_FLEET_ALLOWED_PROGRAMS. - Why. A vault becomes an automation surface (transcript ingestion, knowledge-base construction, triage, summaries) without external orchestration. Trip2g stays a plain event source; the fleet is the only moving part.
- How. The
fleetbinary ships inside the trip2g image at/fleetand as a standalone downloadable. Configure withTRIP2G_FLEET_*env vars (orcmd/fleetflags); auth uses an admin HAT minted from the app's JWT secret. Guide: en/agents_how_it_works.
Downloadable binaries with checksums
- What. Prebuilt archives are now attached to every GitHub Release: linux amd64/arm64, macOS arm64/amd64, Windows amd64. Each archive contains
trip2g-serverandfleet, plus a.sha256checksum file. - Why. Run trip2g without Docker: a single binary on any of the five supported platforms.
- How. Download the archive for your OS from the Release page, verify with
sha256sum -c *.sha256, extract, and run. macOS binaries are unsigned; Gatekeeper warns on first launch (right-click the binary and choose Open to allow it).
trip2g lint docs
- What. A new
lint docssubcommand runs the real note-loader over a docs tree and reports issues: cross-language wikilink leaks (aru/note linking to anen/page), layout render errors, and broken links (advisory). It replaces the oldcheck-doc-lang-links.shbash script and is now wired into CI. - Why. Catches publish-time problems before they go live: a Russian note accidentally linking to an English page, a layout that fails to render.
- How.
trip2g lint docs(orgo run -tags dev ./cmd/server lint docsfrom source). Exit code is non-zero on warnings; broken links to not-yet-created notes are advisory and do not block the check.
Database metrics and stricter SQL
- What. Three new Prometheus metrics are now exposed on the existing internal metrics endpoint: DB connection pool stats, database file size, and WAL file size. Double-quoted string literals in SQL are rejected at startup via the
_dqspragma to surface typos early. The last directmattn/go-sqlite3import was dropped from test tooling; the app has run on pure-Go modernc since well before this release. - Why. The metrics give operators visibility into DB pressure before it becomes an outage. The
_dqspragma turns a class of silent SQL bugs into a startup error. - How. Metrics appear on the existing internal metrics endpoint with no config change. The
_dqsrejection may surface a pre-existing SQL typo on upgrade; check startup logs if the server refuses to start.
Search: cross-encoder reranker removed
- What. The optional second-stage cross-encoder reranker (shipped off-by-default in a prior release) is removed, along with the Python sidecar (
reranker-server/). - Why. Two rounds of benchmarking showed it hurt search quality: nDCG dropped from 0.9221 to 0.8881 with 512-char passages and to ~0.39 with full-note passages. The cross-encoder over-weighted surface term overlap and promoted near-neighbour distractors that the existing bi-encoder + BM25 + RRF first stage had correctly ranked lower. Rationale is in
docs/dev/reranker.md. - How. Nothing to do. The feature was off by default. Any existing
vector_search.reranker.*config keys are now ignored.
v0.7.1 (2026-06-26)
Wide pages (wide: true)
- What. Any note can now render full-width by adding
wide: trueto its frontmatter. The page drops both sidebars and the narrow reading column so the content fills the whole main area. - Why. Wide content (kanban boards, big Mermaid diagrams, large tables) was cramped in the default 65ch reading column.
- How. Add
wide: trueto the note's frontmatter. Guide: en/user/default-template.
Live layout preview tool: trip2g-preview, replaces renderlayout.py (19483421, d0fe4179)
- What. A new standalone node CLI,
scripts/trip2g-preview.mjs, renders a Jet layout against a note via/_system/renderlayoutand adds a--watchmode: it re-POSTs on every save while a browser parked on the?liveURL reloads itself. It replaces the oldscripts/renderlayout.py(removed). No Python dependency. - Why. Tightens the layout-developer loop: edit a
_layouts/*.html, see the result live, catch Jet errors in the terminal. It targets any server: a local memcli (auto-readsdata.json) or a remote/staging instance via--api-url/--api-key. - How.
node scripts/trip2g-preview.mjs --watch --layout-file _layouts/article.html --note-path /about, then open the printed?liveURL (with memcli, runmemcli openfirst so the browser is signed in). Guide: en/user/renderlayout; the two local design loops are documented indocs/dev/local_design_iteration.md.
memcli: isolated instances with --name (5f0542f8, 353841e0)
- What.
memcli up --name <id>boots a second instance in its own containertrip2g-memory-<id>. Pass the same--nametodown/status/logs, and give it a distinct--port. With no--namethe default (trip2g-memory) is unchanged. - Why. Lets several local memcli instances run side by side (e.g. one per vault) instead of fighting over the single hardcoded container name.
- How.
node cli/memcli/dist/memcli.js up --folder ./v2 --name v2 --port 24381. State stays per--folder. Guide: en/user/memcli.
v0.7.0 (2026-06-23)
Read replica mode (a4423895)
- What. A second trip2g instance can now run as a read-only replica. Set
TRIP2G_LEADER_ADDRon it and it will serve all GET requests from a LiteFS-replicated local SQLite copy and forward every write to the primary. - Why. This halves read latency when the primary is under load, lets you restart the primary without dropping a single reader request, and opens the door to horizontal read scaling on separate machines. During a leader restart in tests, zero read failures were observed on the replica.
- How. Run LiteFS on both machines so the replica gets a streaming copy of the SQLite WAL. Start the replica with
--leader-addr=http://10.x.x.x:8082 --leader-shared-secret=.... The--leader-shared-secretmust matchTRIP2G_LEADER_REPLICA_SECRETon the primary. Full wiring guide: en/user/read-replica.
OIDC / Corporate SSO login (1d2b12b5, b6446f76, e6d7afe9)
- What. Users can now sign in with a corporate identity (Authentik, Keycloak, any standards-compliant OpenID Connect provider). A "Sign in with SSO" button appears on the login page when an OIDC provider is configured. Auto-provisioning is optional: turn it on and a valid IdP identity creates the trip2g user automatically; leave it off and only pre-existing accounts are admitted.
- Why. Teams that already have a company IdP no longer need to manage separate trip2g passwords. One click, IdP MFA included.
- How. Set
OIDC_CLIENT_ID,OIDC_CLIENT_SECRET, andOIDC_DISCOVERY_URLon the server. No database row is needed; the provider is read from env at request time. Admin credential CRUD is available via GraphQL mutations for setups that need it. Full guide: en/user/oidc.
Local filesystem storage backend (0f5b84e2)
- What. Trip2g can now store uploaded assets (images, attachments) on the local filesystem instead of S3 or MinIO. Set
STORAGE_BACKEND=local(or--storage-backend=local) and optionallySTORAGE_LOCAL_PATHto pick the directory. - Why. Running without an object storage service removes the main external dependency for single-server self-hosting. A $4 VPS with a volume attached is now a complete, standalone deployment.
- How. Add
STORAGE_BACKEND=localto your env file. The default path is./data/storage; override withSTORAGE_LOCAL_PATH. Switching from an existing S3 setup requires copying the bucket contents to the local path first. See en/user/local-quickstart for the full single-server setup.
Zero-downtime deploys (08a6cfde, 22dd8cf3)
- What. Trip2g now supports systemd socket activation (
LISTEN_FDS) and exposes two health probes on a separate internal port:/livez(always 200, even during warmup) and/readyz(503 until the instance is warmed up and again during drain). Together they give a load balancer or orchestrator the signals it needs to cut traffic from old to new without dropping requests. - Why. Before this, a rolling restart caused a gap: the old process was gone before the new one finished warming the note cache. Now the new instance warms up in parallel and traffic only moves once
/readyzreturns 200. - How. Add the socket unit to systemd so the OS holds the port during the restart gap. Set
--internal-listen-addr=:8081and wire your load balancer to wait for/readyzbefore routing.--shutdown-grace-periodcontrols how long the old instance keeps serving after/readyzflips to 503. Use--simple-backup-on-shutdown=falseto skip the shutdown backup during rolling restarts. Full recipes (Nomad/Traefik, Caddy, k3s): en/user/zerodowntime.
memcli: agent-memory bootstrap CLI (a4e6fd1e, 951d52ed)
- What.
memcliis a single-command tool that boots a trip2g instance as persistent long-term memory for AI agents. One command (node memcli.js up --folder ./memory) starts the Docker container with local storage, mints an admin key via HAT, starts thetrip2g-sync --watchsidecar, and writes ahub.mdfederation note into the memory vault. The newhubsubcommand andmemory_bind_hubMCP tool bind the memory instance to a remote federation hub, so agent searches reach federated knowledge bases through a single MCP endpoint. - Why. Setting up a standalone trip2g for an agent previously required four separate manual steps (server, key, sync, MCP config). memcli collapses them to one command. The compiled
dist/memcli.jsships in the repository; no build step is required. - How.
node cli/memcli/dist/memcli.js up --folder ./memory-vault. When it finishes, the MCP endpoint is athttp://localhost:24081/_system/mcp. Add it to your agent's.mcp.json. Full guide: en/user/agent-memory.
gitapi mirror stability (2845bb82, 8082c24d)
- What. Two fixes to the DB→git mirror (gitapi). When a
materializecall fails mid-way, orphaned loose objects are now cleaned up before returning the error. After a successful materialize, gc runs immediately to pack loose objects. Previously, failed runs could leave orphaned files that accumulated across nightly rebuilds and eventually filled the disk; objects were packed only on the next gc cycle. - Why. On instances with many notes, repeated materialize failures left the git data directory growing unbounded. Operators with a small VPS saw disk pressure that required manual cleanup.
- How. No action needed. Both behaviors are automatic after upgrading. If you saw disk growth from orphaned objects, a
git gc --prune=nowin the repo directory (or a server restart that triggers the next materialize) will clear the backlog.
v0.6.1 (2026-06-22)
Live updates in the Obsidian plugin (plugin v0.5.0)
- What. The trip2g sync plugin now consumes the
noteChangessubscription (shipped in v0.6.0) and pulls server-side changes into your vault in real time. No sync click, no waiting for the periodic check. - Why. For multi-device and agent-driven editing, a change made on the server (or another device) shows up in Obsidian within a second instead of after the next poll. The benefit is instant freshness and less idle traffic. This is a UX change, not a backend speed gain: the underlying note-list query was already a ~10 ms indexed read.
- How. Update the plugin to 0.5.0 (via BRAT), turn on Two-way sync, then set Live pull patterns in the plugin settings (include/exclude globs, e.g.
**, orblog/**excludingdrafts/**). Safety is preserved: local edits are never overwritten (you get a conflict prompt), and server-side deletions ask before removing locally. The 60-second background poll becomes a lighter 5-minute reconciliation backstop. User docs:docs/en/user/two-way-sync.md. The story:docs/en/thoughts/sync-benchmark.md.
Much faster bulk and CLI sync of notes with assets
- What. Syncing many notes that embed images is dramatically faster from the CLI and browser-sync. A cold push of 2000 notes with 2000 images dropped from 231.8 s to 8.8 s (~26×).
- Why. Each asset upload was triggering a full server-side note reload, because the CLI and browser-sync didn't batch uploads the way the Obsidian plugin already did. One missing flag (
skipCommit) turned 2000 uploads into 2000 full reloads. - How. No action needed beyond updating to plugin/CLI 0.5.0. The interactive Obsidian plugin was already unaffected.
Stability: the real-time subscription could crash the server (c283963b)
- What. A race in the in-process event bus behind the
noteChangessubscription could panic the whole server when a subscriber disconnected during a save (send on closed channel). - Why. It never fired while nothing subscribed, but the new live-pull plugin connects and disconnects routinely, making it a real risk for anyone running the subscription.
- How. No action needed; the bus now uses a per-subscriber done channel and is race-tested under stress. A Sync now command was also added to the plugin (command palette).
v0.6.0 (2026-06-17)
Mermaid diagrams
- What. A
```mermaidfenced code block in any note renders as a diagram on the published page: flowcharts, sequence diagrams, pie charts, Gantt charts, class diagrams, state diagrams, ER diagrams, and every other type Mermaid supports. - Why. Write diagrams the same way Obsidian renders them. The block just works.
- How. Add a code block with the
mermaidlanguage tag and paste your diagram syntax. The Mermaid library loads lazily and only on pages that contain amermaidblock; pages without diagrams pay no loading cost. User docs:docs/en/user/mermaid.md,docs/ru/user/mermaid.md.
Charts from datachart blocks (7cadad3e, cb1403f5)
- What. A
```datachartfenced code block becomes an interactive chart on the published page, powered by Apache ECharts. Data can come from four sources:inlinerows bundled in the block, a vault file referenced via a frontmatter[[wikilink]](frontmatter), an external HTTP-JSON endpoint fetched on the server and cached (url), or your site's own content via SQL (internal, coming soon). URL fetch errors are recorded and surfaced to authors so a broken endpoint is visible at sync time, not only in the browser (97997bb4). - Why. Publish live charts as naturally as you write any other Obsidian note. The ECharts widget loads lazily, only on pages that contain a
datachartblock. - How. Add a fenced block with the language tag
datachartcontaining a JSON object withdataandconfigkeys. Theconfigobject is passed directly to ECharts, so any chart type and option it supports works here. User docs:docs/en/user/chartdata.md,docs/ru/user/chartdata.md.
MCP Federation: admin topology endpoint and configurable kb_id (4485b6cd, 5c7492ae)
- What. Two additions to the federation layer introduced in v0.4.1. A new admin-gated
GET /_system/federation/adminendpoint returns the full federation topology: all registered KB peers, their scopes, and their reachability status. Thekb_idfor your instance is now configurable; if not set explicitly it falls back to the public URL host. - Why. Operators running multiple federated instances can inspect the topology without digging through the database. A configurable
kb_idlets you assign a stable, human-readable identifier that stays correct regardless of which domain the instance answers on. - How. The topology endpoint is admin-only (requires an admin API key or session). Set
kb_idin your instance config to override the default host-derived value. No changes needed to existing federation setups.
Live note-change SSE subscriptions (854c56b0)
- What. A new
noteChangesGraphQL subscription streams note upsert and removal events to connected clients over SSE, with optional glob filtering. Each event carries the changed HTML selectors diff so clients can patch the DOM without a full page reload. - Why. Enables live-updating UIs (an admin editor, a preview pane, or a custom dashboard) that reflect vault changes the moment they land on the server.
- How. Subscribe to
noteChanges(glob: "posts/**")via the GraphQL SSE endpoint. ThechangedHtmlSelectorsfield on each event lists the CSS selectors whose rendered HTML changed, making surgical DOM updates possible.
In-browser editor: Ctrl+Click wikilink navigation (a065fea2)
- What. In the in-browser file editor (introduced in v0.5.0), Ctrl+Click (or Cmd+Click on macOS) on a wikilink opens the linked note directly.
- Why. Matches the Obsidian editing experience and removes the need to search for a linked file manually.
- How. Open the editor, hover over any
[[wikilink]]; the cursor changes to a pointer. Ctrl+Click to navigate.
v0.5.1 (2026-05-27)
Per-webhook secrets injected into delivery payload (0b72acf2)
- What. Each change webhook and cron webhook now has a Secrets panel in the admin. Add named key-value pairs (e.g.
auth_token,api_key); they are stored encrypted and sent in every delivery payload underpayload.secrets. The webhook consumer can read them without any extra API calls. - Why. Cron webhooks are the foundation of a plugin system. A plugin is a web server (or serverless function) that receives a payload with a short-lived API token and processes it (often in the background), then patches the vault when ready. Because plugins are stateless, they have no safe place to store their own credentials. Secrets solve this: trip2g holds them encrypted and delivers them on every call, so the plugin stays credential-free on its end. A plugin that needs more time can use the API token to push updates back asynchronously; secrets give it everything else it needs to talk to external services.
- How. Open Admin → Change Webhooks (or Cron Webhooks) → select a webhook → scroll to the Secrets section. Enter a name and value, click Add Secret. To update a value, type in the row's value field and click Save. To remove, click the trash icon (confirm on second click). Secrets appear in the delivery payload as
{ "secrets": { "auth_token": "...", "api_key": "..." } }.
v0.5.0 (2026-05-26)
In-browser file editor (admin)
- What. Admins can now edit any page right on the site. An editor icon appears in the top-right of the admin panel and next to the search on every page. Open it to browse every uploaded file as a folder tree, view and edit any one, and save. You can also roll a file back to an earlier version.
- Why. Fix a typo or update a page in seconds. No Obsidian, no re-sync.
- How. Click the editor icon (admins only). The current page's note opens by default; pick any other file from the tree on the left. Edits stay in your browser until you press Save, and the versions panel lets you load and restore an older version.
Public hub of curated bases
- What. A
hub/section with a bilingual index of the knowledge bases reachable through the hub (first entry: the Nick Senin Journal (filtered Code with Claude 2026 cases)). - Why. A browsable, public entry point to federated bases.
- How. See
docs/en/hub/_index.md; add your own withdocs/en/hub/_create.md.
v0.4.1 (2026-05-25)
MCP Federation: one hub across many knowledge bases
- What. Your instance can act as a federation hub. A KB-note (a note with
mcp_federation_kb_urlin frontmatter) registers another MCP-compatible base, andfederated_search/federated_similar/federated_note_htmlreach across all of them through your single MCP endpoint. Public bases need no auth; private peers use a shared HMAC secret. - Why. One endpoint, one auth surface: your agent searches your own notes, partner instances, and external adapters (GitHub, Telegram) together, without rewiring
.mcp.json. - How.
- User docs:
docs/en/user/federation.md,docs/ru/user/federation.md - Public base: create a note with
mcp_federation_kb_url(+ optionalmcp_federation_kb_id) andfree: true. - Private peer: exchange a federation secret in Admin → Federation, then add the KB-note.
- User docs:
Canvas files (Base & Excalidraw coming later)
- What.
.canvasfiles sync and render..baseand.excalidrawfiles are accepted by sync too, but rendering them is planned for later; for now they show a clear placeholder instead of breaking the page. - Why. Canvas vaults work today; Base and Excalidraw vaults sync without errors while full support is on the way.
- How. Just sync. The plugin and CLI accept all three extensions; Canvas renders now.
Telegram navigation & canvas bots
- What. A wikilink-browser bot and canvas-driven navigation over a Telegram business connection.
- Why. Readers can browse your vault graph from inside Telegram.
- How. See the Telegram docs; enable on a business connection.
Admin & config
- What. GraphQL API for note version history; admin filter for form submits (status / date / processed); environment variables accept a
TRIP2G_prefix (unknown vars warn). - Why. Inspect and roll context, triage submissions, and configure self-hosted instances more safely.
- How. Admin panels; prefix any env var with
TRIP2G_.
Obsidian sync plugin + CLI
- What.
- Accepts
.canvas,.base, and.excalidrawfiles. - Surfaces GraphQL error details on a failed push (no more silent failures).
- New
--exclude <glob>flag (repeatable). Excluded paths are never pushed; if they already exist on the server they are hidden. A bare name likedevmatches that directory and everything under it. Default: nothing is excluded. Everything uploads.
- Accepts
- Why. Keep test/demo or internal folders (e.g.
dev/,demo/) in your repo but out of the published site, and reversibly hide them on the server. - How.
trip2g-sync ./docs --exclude dev --exclude demo. Re-including a path re-publishes and automatically unhides it.
v0.4.0 (2026-05-21)
updateNotes mutation: atomic find/replace across notes
- What. New GraphQL mutation that patches multiple notes in one transaction via a
PathMapof{path → [{find, replace}]}. - Why. Lets external tools, agents, and scripts apply consistent edits across a vault without orchestrating per-note round-trips. Avoids partial states when one of the replacements fails.
- How.
- User docs:
docs/en/user/update_notes.md,docs/ru/user/update_notes.md - Example: send
updateNotes(input: { pathMap: { "notes/post.md": [{ find: "old", replace: "new" }] } }). - End-to-end spec:
e2e/updatenotes/*(seetest(e2e): add updateNotes e2e spec and demo fixture).
- User docs:
Forms admin: submit processing
- What. New
markFormSubmitProcessedmutation andprocessedfields on form submits; admin can mark a submit as handled, the UI hides processed entries by default. - Why. Closes the form-handling loop inside the admin instead of forcing external triggers.
- How.
- User docs:
docs/en/user/forms.md,docs/ru/user/forms.md - Dev reference:
docs/dev/forms.md - Use
can_submit/success_urlfrontmatter on form notes to gate submissions and customize the thank-you page.
- User docs:
Layout smoke-render: surface Jet runtime errors at load
- What. When notes are loaded, each parsed Jet layout is executed against up to 10 first notes that select it via frontmatter
layout:. Runtime errors and panics becomeNoteWarningentries on the layout. - Why. Previously, a broken layout that parsed (e.g.
{{ note.NoSuchField }}) only failed when a user opened the page in the browser. Agents pushing notes had no signal. Smoke-render moves the failure to load time so warnings show up in the same channel as parse errors, visible viapushNotes/ admin layout listings without a browser request. - How. Automatic; no flags. Watch for
smoke render error/smoke render panicin layout warnings after a sync. Layouts without parsed templates and layouts no note uses are skipped.
Template debugging: Meta.Debug() and global debug()
- What. Inside Jet templates:
{{ Meta.Debug() }}dumps note metadata; global{{ debug(<any>) }}prints type, value, and the method set of any expression via reflection. - Why. Removes the "guess what the template sees" loop when authoring layouts and components.
- How.
- User docs:
docs/en/user/jet.md,docs/ru/user/jet.md(debugging section). - Example:
{{ debug(note.M()) }}or{{ debug(note.Title) }}.
- User docs:
renderlayout.py CLI: render a layout against a note
- What. Standalone script in
scripts/renderlayout.pyplus acheck_templatesagent skill. - Why. Lets you preview layouts and reproduce template issues from the terminal, useful in CI and when iterating on
_layouts/. - How.
Fixes
layoutloader: nil-guard forYieldNode.Parameters; layout ID normalized in preview to match production.renderlayoutpreview: autoimport,yield_blockswiring, andhtmlInjectionsnow match production behavior.renderpreview: parses YAML frontmatter fromnote.srclike the real loader.templateviews.GetStrings: returns empty slice instead ofnil(no more nil-iteration surprises in templates).
Docs & chore
- Forms dev reference + roadmap, BEM rendering skill, Jet
debug()section. - Lint passes across
updatenotes,layoutloader,noteloader.
v0.3.1 (historical backfill)
Forms in notes (initial)
- What. Forms can be embedded in vault notes via frontmatter and rendered by the default template. Multiple forms per note supported,
note_version_idandform_idtracked per submit. - Why. Lets a published site collect input (signups, contact, polls) without an external service.
- How. See
docs/dev/forms.mdanddocs/{en,ru}/user/forms.md.
Layouts get HTML injections
- What. Custom HTML injections (head / body_end placements) now apply inside Jet layouts, not just the default template.
- Why. Analytics, custom scripts, and SEO tags work for custom-layout pages.
- How. Admin → HTML injections; pick placement
headorbody_end.
Language switcher rework
- What. Dropdown showing full native language names with normalization; US flag for English; multilingual docs.
- Why. Multilingual sites look correct and pick the right alternate per language.
- How. Use
lang:/lang_redirect:frontmatter; switcher renders automatically in default template.
renderlayout preview endpoint (/_system/renderlayout)
- What. Admin endpoint to render an arbitrary layout against a note for preview.
- Why. Editor / IDE integrations can show a live preview of
_layouts/*changes. - How. Spec:
docs/superpowers/specs/2026-05-10-renderlayout-endpoint-design.md.
Admin API keys: enable/disable
- What. GraphQL mutation + admin button to toggle API key state; auto-cleanup of API key logs after 90 days.
- Why. Pause a key without rotating it; keep logs bounded.
- How. Admin → API keys panel.
Onboarding vault: agent config files
- What. Vault download now ships with
.mcp.json,codex.json, antigravity config andAGENTS.md. - Why. Drop-in setup for AI agents over an Obsidian vault. No manual wiring.
- How. Download the onboarding vault; configs are already inside.
Cronjobs lock by default
- What. Cronjob editing is disabled unless
--cronjobs-allow-editis passed. - Why. Reduces footgun in shared / production deployments.
- How. Pass the flag in your start script if you do want to edit cron entries through the admin.
Notable fixes
- Backlinks / similar notes exclude system notes (paths starting with
_). - TOC anchors work again (heading
idemitted in HTML). - Mesh template:
yield_blocksmoved to<head>with documented limitations.
v0.3.0 (historical backfill)
Self-hosted deployment path documented
- What. End-to-end deployment guide for running trip2g on your own infrastructure.
- Why. Reproducible, hands-off setup outside the hosted offering.
- How.
docs/{en,ru}/user/hosting.mdand related guides.
Sign-in wall + captcha (Auth phase 1A)
- What. Per-note sign-in requirement, captcha on auth flows, hardened sessions.
- Why. Gate private content; resist abuse on public auth endpoints.
- How. Frontmatter / subgraph
require_signin; default template renders the wall.
Vault-based layout sections
- What. Header / footer / sidebar can be sourced from vault notes.
- Why. Authors edit chrome the same way they edit content. No template hacks.
- How. Place notes in the conventional paths (see
docs/dev/default_template.md).
Vault-based frontmatter patches
- What. Markdown files in the vault can declare patches that apply to other notes' frontmatter at load time.
- Why. Bulk-tag, set layouts, or normalize metadata without editing every note.
- How.
docs/dev/frontmatter_patches.md.
Search: bge-m3 embeddings + embedding microservice
- What. Switched to
bge-m3embedding model; introduced an embedding-server microservice; vector search top-K results withmatchOrigin. - Why. Better semantic retrieval and decoupled embedding workload.
- How. Configure embedding endpoint; vector search exposed via existing search APIs.
MCP server upgrades
- What. MCP search results are addressable, self-describing, and openable as focused chunk reads; text-search hits map to nearest chunks.
- Why. RAG clients get richer, navigable results.
- How. Connect via MCP; consult
docs/user/ai-agent-docs-setup.md(if present in your vault) for client wiring.
"Read in Telegram" + Telegram UX
- What. Button on note pages, public TG links preferred for published posts, UTM tags on Telegram-originated traffic.
- Why. Closes the loop between site posts and Telegram audience attribution.
- How. Automatic for posts published through TG; UTM scheme documented in
docs/superpowers/specs/.
exporttgchannel CLI
- What. New CLI command to export a Telegram channel to Obsidian-flavored markdown.
- Why. One-step import of an existing channel into a vault.
- How.
go run ./cmd/exporttgchannel --help.
Notable fixes
- Configurable URL normalization with 301 redirects for alternate variants.
- Audio rendered as
<audio>; documents rendered as links. - Queue: prevent goroutine leak on double start and deadlock on stop.
- Wikilink
[[slug#anchor]]: strip anchor before note lookup. - All
golangci-lintwarnings resolved, pre-push hook added.