All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
- Markdown embedded-image analysis (Issue #608). Opt-in local vault image embeds (
![[...]]and) are resolved through Obsidian and processed in 20 MiB visual-evidence packages, so a source has no image-count limit. Each image remains capped at 10 MiB; GIFs send a static first frame; remote URLs are never downloaded. The visual request includes each image's nearest Markdown paragraphs. An additional default-off setting saves per-image context, evidence, and skip reasons in a replaceable collapsible source-page audit section.
39 merge commits since v1.27.1 (2026-09-06 → 09-15, 128 files, +4243/−1802 LOC, 3993 → 4144 tests). PATCH — a rewrite cut off at the token limit no longer overwrites a page (#704), one shape for updated_pages so link repointing sees every page (#713), provenance footnote brackets repaired (#702), corporate-gateway structured-output demotion (#711), strict structured-output as a negotiated tier (#658), ingest lifecycle released on skip (#688), cancel reaches the running model call (#646), plus the wave-C correctness wave (#644-#684) and a main-is-red regression fixed at the source (#722).
- A body rewrite cut off at the token limit is no longer adopted — and the guard now returns the page path, not a boolean (Issue #704, PR #705; regression fixed in PR #722). A rewrite that stops because the model hit
max_tokenswas written over the page as if complete: measured on a rebuilt vault, 13 body rewrites grew past 8 KB, 10 of them within 4 minutes of a"finish_reason": "length", one page going 32 KB → 127 KB with 300–760 provenance footnotes and 2463 of 2703 prose links dead. The truncation is knowable after the fact, so it is checked there: newcaptureFinish()inllm-sdk/finish-reason.tsreturns anonFinishsink plus atruncatedgetter (only'length'counts; a client that reports nothing leaves the reason'unknown', so legacy and mock clients keep their exact behaviour). The guard sits at the three call sites that write over an existing body —merge-bodyandreviewed-appendinpage-factory/merge-page.ts,related-pageinpage-factory/related-page.ts— and on truncation keeps the frontmatter write, restores the existing body, logs a warning, and adopts nothing. PR #722 fixes a regression the two halves introduced: #705 wrotereturn truewhile the function still returnedboolean, and #714 changed that return type tostring | null; each passed Gate 1 on its own branch, and merged together they left main red for five consecutive commits (error TS2322). The runtime half is the worse one —updatedPathbecametrue, soupdated_pagesgained a boolean that thep.endsWith('.md')filter drops silently, which is the #713 shape reappearing on the path #705 had just repaired.return page.pathis the correct value rather than merely the correct type: the guard runs aftercreateOrUpdateFile, so the frontmatter landed and the page genuinely is updated. analysis.updated_pagesnow carries one shape, so the run-end link repoint sees every page (Issue #713, PR #714). Two producers disagreed:wiki-engine.tspushed vault paths for created and updated pages, but the related-page branch pushed the baretask.name.repointLinksAfterRunfiltersp.endsWith('.md'), so every bare name was skipped in silence — the pass documented as reading "every page the run created or updated" was reading a subset. Fixed at the fill site, not at the readers: newrecordUpdatedPage()inengine-internals/dedup-pages.tsde-duplicates on insert, which also keeps the cancelled and failed paths correct (they handupdated_pagesstraight toonDonewithout passing a reader, so a reader-side dedup would have left the count too high exactly where it is hardest to notice). Second fix in the same change:log-writer.tsgainedpageLinks(), and updated pages now strip thewikiFolderprefix like created pages always did —[[wiki/concepts/X.md]]does not resolve where[[concepts/X.md]]does, so updated pages were rendering as dead links in the ingest log.- Provenance footnote brackets are repaired on the single write gate (PR #702). A page's inline provenance marker (
^[<label>: [[Name]]]) is whatparagraph-provenance.tsreads to decide which paragraph a rewrite may drop, and the model gets the brackets wrong often enough to matter: measured over a rebuilt vault about a third of markers came back with two closing brackets instead of three, and one in five with the opening bracket doubled (155 of 791 on one run). Every malformed form is invisible to the guard, so the paragraph it belongs to loses its owner and becomes droppable — and Obsidian reads the doubled form as a link to a page named<label>: [[Name, one ghost node per source. Newcore/provenance-marker.ts(normalizeProvenanceMarkers) repairs the four written forms (^[,^[[,[^, and the caret lost with the nested[[or a trailing^) to the canonical shape, called fromwiki-engine.tsbesidenormalizeHeadingSpacinginside the content-folder write gate. The semantic half — which paragraph, which source — is the model's judgement and is left alone; only the syntax is repaired, the label is carried through verbatim, and the function is idempotent (returning the same reference when the content holds no[[, so callers detect a no-op cheaply). False positives are guarded deliberately:NESTEDrequires the nested[[andTRAILING_CARETrequires the trailing caret, which is what keeps an ordinary wikilink whose name contains a colon ([[Study: Berberin 2021]]) out of the rewrite. - Corporate-gateway
response_formatrejections now engage the demotion chain (Issue #711, PR #712). A gateway that rejects the wholeresponse_formatenvelope without namingjson_schema— "This response_format type is unavailable now" — matched no classifier, so the rejection surfaced raw and the provider was unusable behind such a proxy.OutputModeProber.isJsonSchemaFieldErrornow recognises that phrasing while leaving the #658 strict-dialect body ("Invalid schema for response_format …") toisStrictSchemaRejection, which owns it; negative cases ("This feature is unavailable now", "max_tokens is invalid") keep the match from widening into a generic 400 catch-all. - Strict structured output is a negotiated tier at the schema boundary, not a per-provider guess (Issue #658, PR #686). New
llm-sdk/strict-schema.tswithLLMClientgaining anoutputModeof'json_schema' | 'json_schema_strict' | 'json_object' | 'text_prompt', so a provider that rejects the strict dialect demotes one tier instead of failing the call. - Delete Empty Stubs collects the stubs it is named for, and leaves the gate stubs alone (Issue #678, PR #691). Two writers set
stub: true:fix-dead-link.tswrites a bare placeholder, whilestub-page.ts'sbuildDissentStubContentwrites a placeholder plus a summary, a quote block, or a fifth-gate form carrying neither — a dead-link shape produced on purpose and therefore not empty. The composite predicate isisStubPage(fm) && isEmptyStub(content), whereisEmptyStub(one heading, one quoted line, no prose) is deliberately fail-safe: a stub it cannot prove empty is kept. Test names were corrected to match their assertions in the deletion path, and the predicate is mutation-verified — sparing the gate stubs fails exactly the test that guards them. - Two contradiction paths only the model could feed are gone (Issues #604, #666, PR #684).
updateContradictionStatusonly ever passes'resolved'or'pending_fix', never'review_ok', andclampPageSectionsis reached only fromresolveContradiction— the cluster was unreachable end to end. - The source page's head comes from the code, not the model's copy (Issues #679, #670, #661, PR #681). Title, note path and date are stamped deterministically;
SourceAnalysis.contradictionsis removed as a field only the model ever fed. - A skipped file releases the ingest lifecycle (Issue #688, PR #690). The status bar stayed stuck after a skip because the teardown path was not reached; extracted as
endIngestion(). - One label table for the ingest log, read by writer and parser alike (Issue #667, PR #685). Three supported languages fell back to English.
- One-token JSON defects are repaired before spending a model call (Issue #682, PR #683). The structural repair runs first; the JSON-repair model call remains for defects that need reasoning, since disabling its thinking budget produces structurally-valid-but-wrong content.
- The related-sibling cap holds at three and no longer forms a clique per note (Issue #644, PR #645). Siblings rescue an orphan; they do not become a complete graph.
- Case is folded when checking the page's own aliases (Issue #674, PR #675), the create path's alias floor applies on the source path too (Issue #671), a two-character name is created rather than sent to semantic dedup (Issue #661, PR #663), the Related sections render after a complementary append (Issue #659, PR #660), model-written
sources:entries are dropped before the true one is stamped (Issue #650, PR #651), a baresources:line no longer emits a stray[[]](Issue #648, PR #649), and a cancel reaches the running model call and stops at the next page write (Issue #646, PR #647). - Frontmatter seams no longer grow a blank line on every call (PR #649).
frontmatter.tsnormalised the seam by pastingfmBlockand the body with one\n; sincefmBlockalready ends on---and the slice begins with whatever followed the old delimiter, each call added a blank line. The seam is now normalised to the one-blank-line shape the constraint enforcer already produces, which also makes the function idempotent. - The schema-suggestion wording reads as a proposal, and the schema diff modal no longer crashes on first render (Issues #594, #593, PRs #655, #654).
- The Five-Gate now runs on pushes to
main, not only on PRs (PR #698). Merged commits were previously unverified; the push trigger is what surfaced the #722 regression, whose five failing runs had no PR page to show a red mark on. package-lock.jsonis gated againstpackage.json(PR #693), and the lockfile is regenerated for the vitest 5 bump (PR #692). Dependency bumps:actions/checkout4 → 7 (#710),pnpm/action-setup4 → 6 (#709),actions/attest-build-provenance1 → 4 (#639),actions/setup-node4 → 7 (#638),vitest4.1.10 → 5.0.0 (#652),yaml2.8.3 → 2.9.0 (#642).
- Where a document is processed, per PDF path (Issue #657, PR #694). Both the English and Chinese guides, plus the changelog line, now state that residency is US by default for Anthropic, OpenAI and Google, the configured region for Bedrock, and that OpenAI offers EU residency — replacing an earlier claim that all four are US-only.
README.md's cross-file link is absolute, as the #375 guard requires. - Four maintainer-lesson entries (PRs #689, #695, #696, #700). ROADMAP wave E and the PATCH ROI board; a close keyword in prose closes a PR, not just an issue; a declined PR is closed rather than left open; and
AGENTS.mdcorrectedrequire_last_push_approvaltofalse.
46 merge commits since v1.27.0 (2026-08-27 → 09-06, 194 files, +10487/−3257 LOC, 3677 → 3993 tests). PATCH — deterministic related lists (#636), sourced-paragraph rewrite guard (#631), vault-wide folder-link repoint (#626), stream-path thinking policy (#629), local-calendar date stamps (#612), two-gate contradiction records (#610), stop-word/lex-PPR query fix (#625), 3-phase repo audit cleanup (#632/#633/#634), plus the wave-A/B correctness wave (#557-#622).
- Related lists are now deterministic: siblings link each other, a name the vault answers keeps its own title, and the two Related sections are rendered from the lists, not transcribed by the model (Issue #635, PR #636). The extraction named anything from the note in
related_entities/related_conceptswith nothing downstream checking whether a page exists or will — measured on a 413-note German vault rebuild: 676 dead related entries on 870 pages (24 %), 103 pages with no live outgoing link (101 of which had a sibling born from the same note), and 265 dead links were spelling variants of pages the vault already held (Interleukin 6forInterleukin-6,H₂O₂re-encoded). Three deterministic layers, no model call, no new setting:core/related-shaping.ts(shapeRelatedLists— after the candidate gate, before planning) adds sibling edges by kind and re-routes vault-answering names to their own title+kind, drops tag values (prefixedThema/Therapie,Th.Therapiein any Unicode script, or the bare leaf only the vocabulary knows) against the active tag vocabulary, and keeps names nothing answers as written while counting them in the log;core/related-sections.ts(renderRelatedSections) writes the two sections from the typed lists — vault path when known, planned path otherwise, name as display text, rewrite-kept entries re-resolved in front, one target once across both sections, everything else byte-identical;page-factory/related-links.ts(applyRelatedLinks) collapses the create/merge/related-page call sites to one line each. Headings match with thesection-header-canonicalizertolerance. Measured same vault: 0 pages without a live link (was 103), sibling lists complete 246/246, dead related entries 1 % (was 24 %). Unicode-safe end to end (\p{L}prefix + NFC slugKeys — verified主题/糖尿病-style tags drop likeThema/Therapie); the one edge (space-containing tag groups likeHealth Sciences/Neurologyfalling through to the unanswered-kept path) is the same non-loss behaviour as before, filed as a backlog note not a follow-up commit. Deliberately out of scope (follow-ups): the generation/merge prompts still ask for the two sections (drop them to save output tokens),buildVaultResolvercould return{path, title, kind}, the section-scoped half ofcorrectRelatedLinkPrefixesis now redundant. Default behaviour change without a switch, argued and five-line opt-in available.
- A concept page reaches the answer prompt as its Description, not its one-line Definition (Issue #628, PR #630). The merge template writes a
## Description(where the substance accumulates) above the one-sentence## Definitionon every multi-source concept page, butextractSummaryFromPagepreferred the Definition for concept pages — measured 183 of 241 dual-section concept pages handing the answer prompt one sentence instead of the substance. Description is now primary for both page types, Definition the fallback;pageTypestays in the options for callers (no signature change). The old pinned rule ("concept with both uses Definition") is inverted inside the test with the reason recorded; a single-Definition page still returns it. - The per-step thinking policy now applies on the stream path too (Issue #627, PR #629).
taskPolicies(e.g.*=default:off) reached bothcreateMessagepaths but nevercreateMessageStream— the path the user waits for: measured 2026-09-04, the keyword call sentreasoning_effort: nonewhile the streamed answer sent nothing, so the model thought for 43 of its 55 seconds.createMessageStreamnow spreadsenableThinkingfromapplyTaskPolicy(params.task)after the advanced-settings overlay (same precedence as the other two paths); without a policy the request is byte-identical (pinned by test). This is the root cause behind the "streamed answer thinks" report, not commitb302aab(verified: reasoning never entersonChunk).reasoningEffortstays off the stream — its interface carries no effort field. - Stop-word substrings and the lex/PPR merge no longer hide the page the user named (Issue #623, PR #625). Measured on a 3,025-page German vault: "Kann man bei eingeschränkter Nierenfunktion Creatin nehmen?" loaded Kahneman, Karpman, Salbei and Pekannüsse while the page titled "Creatin" was absent. Three mechanisms fixed in
ppr-cascade.ts:tokenizeQueryword runs are now\p{L}\p{N}\p{M}(RegExp constructor, the formcandidate-gate.tsalready uses under the ES6 target — "über" no longer splits into "über"+"ber", NFD combining marks stay inside the word, Cyrillic/Greek/Arabic keep their words, edge punctuation stripped); new exportedneedleHitsrequires a word start for space-delimited scripts ("man" ↛ Kahneman, "creatin" ↛ Phosphocreatin — a different substance; Han/kana/Hangul/Thai keep substring semantics);mergeWithPPRreplacesmax(lexRank, ppr)withppr + hint × 0.1 × maxPpr(unreached pageshint × minPpr / 2) so PPR ranks and the lex hint only orders within a tenth of the top mass.PageMatch.scorescale unchanged (read only in a debug line). One existing assertion changed honestly:ppr-cascade.test.tstoBe('P0')→toContain(P0/P1/P2)— the old pin held only through themax()defect. Tail compounds ("insuffizienz" in Niereninsuffizienz) stay with aliases/keyword/graph by design. - 314 folder-wrong links re-pointed vault-wide, on the source page, and once more when the run is over (Issue #624, PR #626). The extraction's folder is decided at dedup (#589), so a page written early in a run links to a sibling that does not exist yet — three quarters of the 314 measured folder-wrong links were invisible to the write-time corrector. Two commits:
correctRelatedLinkPrefixesnow re-points folder-typed links in prose and on source-page sections (title-then-alias against the engine's cached page index; bare[[X]]untouched,sources/never re-typed, unknown names left alone, case-only differences not rewritten); new Stage 4.5repointLinksAfterRunreads every page the run created/updated once, resolves folder-typed links against the run's own pages (title from filename, aliases from frontmatter), and writes only changed pages through the single write gate (no model;reviewed: truepages lend titles but are never touched; per-page failure isolation).[[concepts/X#heading]]anchors not resolved (no template emits them); the write-time pass stays (a deferred whole-run correction would move the corrector out of three writers — too large for this fix). - A paragraph another source footnoted no longer vanishes from a rewrite (PR #631). Replay over 571 rewrite pairs (720 footnoted paragraphs): 15% came back with less than half their words anywhere on the page — 102 of those 111 dropped by a rewrite a different source triggered; of surviving paragraphs 23% lost their footnote. New deterministic layer
preserveSourcedParagraphsinsrc/core/paragraph-provenance.ts: every paragraph/list item ending in an inline footnote carrying a wikilink is matched into the same section of the rewrite by word overlap (PARAGRAPH_KEEP_OVERLAP0.5); found-without-footnote → footnote re-attached; not found → restored after the nearest surviving preceding paragraph (unless every footnote on it names the source being merged — own-source exemption, folder/case/NFC-aware).guardBodyRewritecomposes the three layers (sections #618, sourced paragraphs, H1 #419); both rewrite paths (merge-page.ts,related-page.ts) call it.sectionIdentityKeyextracted insection-header-canonicalizer.ts. 13 tests + replay: 103 rewrites touched, 120 paragraphs restored, 164 footnotes re-attached. Deliberately out of scope: the footnote convention itself (no-op without it),mergeDuplicatePages/resolveContradictionfull-body rewrites (separate note), the 0.5 threshold as regulator (a 45%-overlap rewording comes back next to its rewrite — known trade-off).
- Three-phase repo audit cleanup — 8 no-op changes + SDK wrap unification + T1/T3 lossless pass (PRs #632/#633/#634). Phase 1 (#632):
stub-page.tsUTC-day site missed by #612 →localDateStamp();LLMWikiSettings.language+ settings onChange re-typed askeyof typeof TEXTS(auto-follows new locales; the hand union was missingru);LANGUAGE_NAMESgainsru; deadappendGranularityToPrompt(0 callers since v1.16.2) + byte-identical privatebuildKnownTargets(folded onto thescanners.tsexport) removed; threemakeCtxbodies →createMergeCtxin__support__/link-vault.ts; flatbatch-limitstwin folded into the inline suite;graph-cache.test.tsrenamed towiki-engine-graph-warmup.test.ts. Phase 2 (#633):openai-sdk-clientdrops its privatewrapReasoningContentcopy (missing</thinkescape; stale "circular dep risk" comment) onto the canonicalmarkdown.tsencoder, which gains the idempotence guard — all four SDK clients share one wrap contract. Phase 3 (#634):escapeRegExp4th copy → canonicallint/utilsescapeRegex(re-export alias keeps the API); 7 inlinesrc/core+ 15llm-sdktests relocated into the__tests__mirrors (zero test loss); 12 dead exports privatized;controller.tsreport round-trip (−85L:extractProgReport+ manual summary/alias re-assembly were vestiges of the #473-removed LLM prompt) collapsed into onebuildLintReportcall; zero-runtime-consumerOUTPUT_MODESconst deleted. 3932 → 3932 tests across the three phases (relocations are moves). Deliberately deferred as needs-design: makeCtx unification (semantically-customized fixtures),onFixAllrunner (i18n plan), indexLabels/logLabels schema, dedup-retry extraction (perf-sensitive). - Vault dates are now the local calendar date, not UTC (Issue #611, PR #612). Every
YYYY-MM-DDthe plugin writes —created:/updated:on generated pages, the ingest log header, contradiction records, schema-file dates, the welcome note — was built fromnew Date().toISOString(), the UTC calendar date, so east of UTC the first hours of each day were stamped with yesterday (a UTC+8 user loses the first 8 hours of every working day). All 17 write sites now route through one helper,localDateStamp()insrc/core/format.ts(localgetFullYear/getMonth/getDate). Full-ISO instants (convertedAt, apply-suggestion, source-analyzer) are deliberately untouched — UTC is correct for them. The three tests that computed expected dates with the same UTC expression were updated with the helper; boundary tests pin 00:30 local. - Related-page rewrites no longer land on the source page that shares the entity's name (Issue #613, PR #615).
updateRelatedPageresolved its target by bare title over the whole wiki folder; whensources/Zytokineandentities/Zytokinecoexisted,findreturned whichever the vault listed first — measured 573 related-page body rewrites landing on source pages in a 413-note rebuild, 168 of which lost content. The lookup is now scoped toentities/andconcepts/; a name that exists only as a source page returns "not found" (a source page is written by its own note's ingest and nothing else). Twin tests verify the entity is rewritten and the source stays byte-identical. - Re-emitting accumulated Mentions no longer caps the block at the fresh-page 500 characters (Issue #614, PR #616). The #267 "non-lossy" union of accumulated mentions was re-injected through the formatter's fresh-page budget, so every rewrite cut a page's Mentions block back to 500 (measured: 180 of 180 audited losses on 180 pages, blocks of 374–2029 chars coming back at or under 500).
assembleFinalContentnow injects withmaxChars: existingLength + DEFAULT_MENTIONS_MAX_CHARS— what the page already holds is the floor, the default budget is what the current source may add. Fresh pages (existing length 0) are unchanged. - A rewrite that kept a section's header but collapsed its content is now treated as a dropped section (Issue #617, PR #618).
preserveExistingSections(#419) restored sections the rewrite dropped outright, but a section the rewrite kept and emptied passed as "kept" — measured 9 canonical## Beschreibungsections of ≥800 chars coming back under half their size (34,454 chars lost; worst caseconcepts/Stress5046 → 335). A canonical section whose content falls belowSECTION_SHRINK_FLOOR(0.5) of its previous length, once it held at leastSECTION_SHRINK_MIN_CHARS(400), is now treated as dropped and the previous block is restored in place, with a warning naming the section and both sizes. - The candidate gate no longer prunes links to pages the vault already has (Issue #620, PR #621). With
skipMentionOnlyCandidateson, dropping a candidate also removed its name from every survivor'srelated_*list ("no gate ever manufactures a dead link", #514) — but the prune was vault-blind: it never asked whether a page for the name already exists, and a link to an existing page is not a dead link. Measured: 782 of 2612 entity/concept pages with no outgoing link (vs 95 of 2386 pre-gate). The gate now takes anisKnownPagepredicate backed by the same title-then-alias resolver the related-link corrector uses (buildVaultResolver, extracted — code moved, not changed); a dropped name that resolves keeps its edge and is reported inlinkedAnyway, a genuinely unknown name is pruned as before. Without the predicate, behaviour is unchanged. - The ingest log and report now count the merge triage's contradictions too (Issue #605, PR #606). Both counted only
analysis.contradictions(the extraction lane); the merge triage records contradictions of its own — item-levelkind: 'contradictory'and page-levelstrategy: 'contradictory'stamp the marker and write the record file — and neither reached the log or report. Measured: one night of 163 ingests logged "no contradictions" while four record files were written by the triage lane.mergePagenow reports each recorded contradiction through an optionalonContradictioncallback (same shape asonFileWrite); the engine collects them for oneingestSourcerun and builds log entry and report from both lanes. Nothing about what is written changes.
- Two gates before a contradiction record — the page sentence must exist, and the source must hold the claim (Issue #609, PR #610). The item-level contradiction lane recorded on the model's word alone; measured 7 of 9 records were false — 3 reported positions the note argues against (a critic's view), 4 conflicts with nothing on the page (the model compared the source with its own knowledge). A prompt rule made the model blind (0/3 on a planted true positive), a stance field inside the triage prompt went unanswered. What works, measured: Gate 1 requires
existing_statementin the triage output, checked deterministically against the page (statementOnPage: the #244 quote fold + wiki-link/footnote markup removed + NFC; exact match or 6 consecutive words) — the model quoted an exact page sentence 13/13 when one existed, and could not when the conflict was with its own knowledge. Gate 2 asks the stance question as its own small call (task: 'source-stance', 600 tokens) over the note excerpt + claim — 72/72 across the real, reported, and planted cases. Anoacts only when the evidence is a sentence (≥4 words) actually in the excerpt; anything else isunverifiedand stays. Demotions are never silent:MergeTriageResult.demotedcarries gate + evidence,mergePagelogs one line per demotion, and the demoted item still lands on the page as a plain fact — only the record file and thecontradictions:marker are withheld. - Cross-folder dedup routes same-designator pages through the semantic call — the folder never decides (Issue #588, PR #589). The opposite folder was never consulted in either direction; a cross-folder slug/alias hit now routes the resolution through the semantic dedup call with that page in the candidate list (measured: 18 twin pairs in one 83-note batch, every one the same referent). Conservative by construction: no-decision keeps the same-type target (a failure never crosses the folder on its own), and a create-with-cross-claim falls back to its own folder's slug where the miss is visible as a twin.
ConflictResolutiongainscrossFolderCandidates; a direct file probe backstops the page index for pages born moments earlier in the same run. - The file picker shows what the vault knows, not what the session remembers (Issue #598, PR #600). Rows were resolved from the session's queue membership, which silently discarded selections made while a batch ran. New
core/ingest-state.ts(pure, no IO) answers disk state per candidate —none/ingested/drifted— reusing the same page-ownership code asisAlreadyIngested(pageBelongsToNote); drift (noteHasDrifted) compares the page's storedcontentHashagainst the note body, answeredfalsewhen undecidable (no stored hash, or more than one recorded origin) rather than guessed. Selection lives in aSetthat survives the search filter; a scan token drops results from scans that outlive their modal. - Ingest ownership resolves from
source_file, notsources:(Issue #595, PR #596). Thesources:frontmatter holds only[[sources/X]]links by contract — it can never contain the note's own path — so every multi-source page read as "not ingested" and was re-processed. NeworiginNoteRefs()incore/frontmatter.tsreads the scalarsource_file:first and falls back tosources:; three readers (ingest skip, drift scanner, picker) use the same helper. Origin-less pre-#164 pages fall back to the existence check (conservative). - Contradiction
source_pageis resolved against the page index, never trusted from the model (Issue #601, PR #602). The write path did string surgery on the model'ssource_page(String.replacewith no match returning the input unchanged), so a bracket-less value — the plugin's own record-file prose prompt encouraged it — wrote the conflict block into a user note: 3 user notes hit on 07-16/17 and 08-31, violating the "never write user notes" hard rule. NewresolveContradictionTarget()resolves model output against the real page index (rel path → title → alias, case-insensitive), refuses ambiguity (>1 match → null), and discards + warns on no resolution (zero writes). The call site passessourceNotePathexplicitly; the record file's frontmatter carriessource_note:and the prose block thatstripUnknownSectionswould delete is removed.
- Fix Dead Links' dev instrument can retarget body links (Issue #590, PR #591).
tools/dev-instrument/src/vault-fs.tsgainslinkCacheOf(body wiki-links + embeds, parsed on demand, scan starts after the closing frontmatter---, mirroringfrontmatterOf) andgetFirstLinkpathDest, soretargetLinksToPagein the instrument is no longer a silent no-op. - Co-maintainer credit (PR #619).
manifest.json/package.jsonauthor becomes "Greener-Dalii, DocTpoint"; README maintainer line lists both with roles; NOTICE entry gains the role.
- README Marp URL + LICENSE relative links fixed (PR #622, community first PR by NotAFlightRisk). The Ecosystem bullet pointed at
samuele-cozzi/obsidian-marp(404; the plugin repo isobsidian-marp-slides), and the docs/ READMEs linkedLICENSE/NOTICEin the wrong direction (they live at the repo root, so../LICENSEis correct there, plainLICENSEin the root README). - 8 fast-uri Dependabot alerts closed at the root — not by another pin (PR #637). The recurring vulnerable-transitive-dep cycle (fast-uri 3.1.5 → 3.1.6 → 3.1.7, four GHSA advisories 2026-09-03) exposed a mechanism defect, not a version lag: a manual
overridespin guaranteed the next advisory would need the same manual pass. fast-uri moves to^3.1.7(3.x latest) and its redundant override is dropped (every consumer declares^3.0.1, which natively permits the fix — verified; thebrace-expansionoverride is kept because legacy minimatch@8/9/3 peers genuinely need pinning against vulnerable^2/^1instances). CI now runspnpm audit --audit-level highper PR so a vulnerable dep fails the build the day it lands;.github/dependabot.yml(new) opens weekly auto-PRs for npm + github-actions, so the fix path no longer depends on a human noticing an alert. DevDependency-only — zero production code touched.
36 merge commits (181 files, +11197/-3158 LOC, 3434 → 3677 tests). MINOR — Bedrock SSO/IAM, MinerU multi-format ingest, source-page verbatim quotes, Fix Dead Links leave-it, ingest candidate gate, per-step taskPolicies UI, plus a long tail of frontmatter / alias / dedup correctness fixes from the community wave (Issues #467/#468/#469/#485/#491/#496/#501/#506/#507/#510/#511/#513/#514/#515/#516/#517/#518/#519/#520/#521/#522/#523/#525/#527/#528/#530/#531/#532/#534/#535/#536/#537/#538/#540/#542/#544/#545/#546/#547/#549/#550/#551/#552/#553). Closed-by-PR totals include 21 community contributions.
-
AWS Bedrock auth: SSO and IAM modes join API key (Issue #425, PR #540). Settings → Provider → Bedrock now picks one of three auth modes; the provider row asks only for the inputs that mode needs. API-key mode is byte-for-byte identical to v1.26.4 (Stage 1). SSO runs the IAM Identity Center device flow (RegisterClient / StartDeviceAuthorization / polling CreateToken) — every step uses platform-native
crypto.subtleand stays platform-neutral. The plugin stores SSO and IAM secrets only in Obsidian SecretStorage (karpathywiki-bedrock-sso/karpathywiki-bedrock-iam), never indata.json, logs, or docs. The data-plane call signs every request with a hand-rolled SigV4 (canonical request → string-to-sign → HMAC-SHA256 chained for the AWS scope), zero AWS SDK, ~+10–15 KB on the bundle. Three isolated constants (BEDROCK_MANTLE_SIGNING_SERVICE='bedrock', thecontent-sha256switch, the portal-host bearer scheme) are the only dials a real-AWS E2E would need to flip. Stage-1 fix rides along: the sync factory previously ignored the user'sbedrockRegionand fell through tous-east-1for every call; it now forwards the region every code path reads. CI-side#540ran with the three constant values unverified against a real AWS account — those constants are the only thing an account-holding user would have to flip. Sign-out clears the SSO/IAM secret and the in-memory cache, same discipline as Codex OAuth. -
Markdown conversion backend: MinerU (PDF + images + Office) joins native (Issue #404, PR #511 CLI follow-up). Settings → Wiki Configuration → Markdown Conversion Backend picks Native (Anthropic Vision / OpenAI Vision / Bedrock direct) or MinerU. The MinerU backend accepts PDF + images (PNG/JPG/JPEG/JP2/WebP/GIF/BMP) + Office documents (DOC/DOCX/PPT/PPTX/XLS/XLSX) through MinerU's Precise parser — best path for scientific papers, scanned documents, and Office files where layout preservation matters. The API token lives only in SecretStorage (
karpathywiki-mineru-api-token); server caps are 200 MB / 200 pages per PDF, 256 MB / 10,000 files per archive. The native backend is still PDF-only by design (provider PDF input surface); multi-format routing under native is not in scope — switch backend to MinerU when needed. -
Source pages now carry verbatim quotes, routed from data already captured (Issue #496). A measured vault showed the
Mentions in Sourcesection on 96% of concept pages but 0 of 1,045 source pages — the page representing the underlying document was the only one with no verbatim path. Three causes fixed together. First, the extraction's quotes existed only per item and were never aggregated onto the analysis, so there was nothing to route;buildSourceAnalysisnow collects every entity/concept's provenance-or-legacy quotes into a deduplicated pool. Second,createSummaryPageinjects that pool through the same programmatic route entity pages use; on a source page the quotes are the payload, so its section budget is raised to 2000 chars (the default 500 would ellipsize exactly what this route preserves). The summary model still sees only the first 500 characters of the source, so its prose cannot fabricate beyond them, and when nothing was captured any Mentions section the model wrote itself is stripped rather than kept. One[MENTIONS-CAPTURE]debug line per ingest reports how many verbatim quotes were captured across how many extracted items, making "the model skipped the field" visible without making it required (a hard-fail risk on weak models). Third, the issue's quote-grounding finding verified true and now load-bearing: lint built its grounding map from generatedsources/pages only, so quotes cited to PRIMARY notes (#244 style) were checked against nothing and flagged ungrounded — which after this change would have hit every fresh capture on re-lint. The programmatic lint phase now reads each cited primary note once and grounds against the underlying document (unreadable notes keep their flags), and passes the localized Mentions label so non-English wikis stop silently no-oping the check. Also: the Minimal extraction-granularity description now states that it caps items per source, not detail per item, in all 11 locales. Refs #496. -
Fix Dead Links gains a leave-it outcome: stub creation is now optional (Issue #485). An unresolvable link always became an empty placeholder page — in both branches (the model's create_stub answer and the deterministic fallback), with no way to decline — and since the #484 title-resolution index such a page absorbs same-name references vault-wide while erasing the dead-link signal that produced it.
createStubsForUnresolvableLinks(default ON, so every existing vault keeps its current outcome; toggle in Settings → Advanced settings) routes both creation sites through a single greppable sibling of #197's never-LLM-expand gate — the two gates answer different questions ("may an LLM fill the stub?" vs "is the stub page written at all?"). When off, the link stays as it is and keeps surfacing in every lint report until a real source defines it; ingest creates pages through normal channels and never needed the pre-existing stub. Correction outranks the gate either way: the deterministic pre-check and the post-model alias safety net keep retargeting links regardless of the setting. Also in this change: the per-step policy control's description now names common step labels (extract,merge-triage,dedup, …) — previously a user had to know internal pipeline label strings to configure it at all, and a misspelled label silently matched nothing. Closes #485. -
The Query Wiki stream path now lands in the per-step LLM accounting table under its own label (Issue #469).
createMessageStreampredated per-step accounting and carried notaskfield, so every streamed Query answer — a known step — recorded under the single mergeduntaggedrow, indistinguishable from genuinely unlabelled calls, and any future streaming call site was type-forbidden from passing a label. The interface now carriestask?(same contract ascreateMessage), the wrapper's stream block drops its hardcodedundefinedin favour of the caller's label, and the only stream caller (QueryView-class.ts) passestask: 'query-wiki', distinct from the separatequery-view-evaluatesave-judgement call. SDK clients forward the field for interface conformance (the wrapper owns accounting, not the client). Unlabelled stream calls still record asuntagged— no fabricated suffix. Closes #469. -
Ingest: opt-in candidate gate skips candidates the source only mentions (Issue #514, PR #521 DocTpoint). On a measured German vault 28.7% of extracted candidates were named but never treated by their source (9.6% name absent from the body, 19.1% present only inside parentheses/enumerations/short list items) — each still cost a page plus dedup and generation calls.
skipMentionOnlyCandidates(default off, Settings → Advanced) runs a deterministic gate between analysis and page planning: prose candidates keep their pages, and gated names are pruned from other candidates'related_*lists so the gate never manufactures a dead link. Keyed onwikiLanguage:deis measured; en/fr/es/pt/nl/ko are estimated with pinned edge cases; zh/ja character-script thresholds are unmeasured (first thing to measure on a Chinese vault); stem-changing languages deliberately get no profile rather than a bad guess. Cross-language notes are not gated (their names are translations). A wiki language without a profile is reported once per ingest, never silently skipped. Off by default: fewer pages is a behaviour change, so it is the user's choice. Closes #514.
-
Prompts: one ranked candidate window for the dedup and Fix Dead Links prompts (Issue #519, PR #520 DocTpoint). Both prompts showed the model an "existing pages" list built ad hoc: semantic dedup fell back to the FULL same-type list whenever the candidate's name shared no title token (~40K prompt tokens per call, firing for 61% of entity candidates on a 2,800-page vault), and Fix Dead Links took every page in vault order cut at 3,000 rendered characters. New
core/candidate-window.tsranks K=30 pages by the existing lexical matcher plus one point per context keyword found in the page's own prose (a document-frequency cap replaces a language-specific stop list; pool order among equals preserves the ctime ordering the KV-prefix cache relies on). Measured over 3,309 hidden-alias trials: target-in-window 25% → 43% (entities) / 24.5% → 41% (concepts). The decisive cell: at a local 26B the model found a synonym's target 0 of 18 times when it sat in the full list, and 9 of 9 in a 30-entry window that contained it — the fallback protected nominal recall at real cost. Gate 4 note:getExistingWikiPagesnow retains ~2KB of page text (~5.6MB peak at 2.8K pages), cheaper than re-reading files to rank. Closes #519. -
Tools: the in-tree CLI is replaced by
tools/dev-instrument/, an UPSTREAM DEV-ONLY INSTRUMENT (Issue #507, PR #511). The production CLI has been the sibling repo (npx karpathywiki-cli) since the v1.26.x migration; what only the in-tree copy could do — run the realWikiEngineheadless against real LLM spend — survives as a measurement instrument for engine contributors (tools/dev-instrument/run-instrument.mjs <vault> <source>), with per-step token/latency accounting preserved (the 979s → 365s → 151s evidence chain) and environment-driven measurement arms (WIKI_THINKING_MODE/WIKI_TEMP/WIKI_TOP_P, fail-fast validated, echoed in the[cli]header next to the effective data.json task-policy map). Eliminates 49 of ~52 Obsidian Bot findings ontools/.package.jsondropsbin.llm-wiki+scripts.llm-wiki; users mid-transition can check out the frozenlegacy/cli-v1.26.4-snapshotbranch. Deprecation notice ships in the v1.27.0 release notes. A bundle smoke test in Gate 1 pins the esbuild obsidian→shim alias so a missing shim export can never ship green again. Closes #507. -
Dev instrument: the process exit code follows the ingest report (Issue #417 secondary, split out of PR #418 as its own decision).
tools/dev-instrument/run-instrument.mjsexits0when the engine's report sayssuccess: true,1when it sayssuccess: false, when no report was emitted, or when the run throws before the engine, and2when<vault> <source>is missing (usage now goes to stderr, nothing to stdout). Before, the code was whatever the last throw decided — a failed ingest exited1only becauseingestSourcerethrows after reporting, a run that ended without a report exited0, and a run without arguments printed usage on stdout and exited0.exitCodeForReport(tools/dev-instrument/src/exit-code.ts) is the single seam; a process-level test drives the real launcher and pins no positionals →2and a report withsuccess false→1against a vault whose local provider points at a closed port.
-
Complementary appends no longer leak the model's visible reasoning into wiki bodies (E2E 2026-08-25). The per-section append — the path that splices new facts into an existing page's section on re-ingest — was the one raw-prose writer without thinking-block stripping: every other LLM-text consumer goes through
cleanMarkdownResponseorparseJsonResponse, both of which strip<think>blocks, but this one only trimmed. A reasoning model emitting visible reasoning in its content channel had its full planning prose appended verbatim into the target section, and a think-wrappedNO_NEW_CONTENTwas spliced as if it were content instead of short-circuiting. The strip now runs at the fragment via a single exported home in core/markdown (shared withcleanMarkdownResponse, which now calls it internally), and the append call pinsenableThinking: falseunconditionally — appending a formatted fragment is mechanical text work; per-call thinking only added latency and was the leak's source. Found during v1.27.0 E2E on a page-update flow with a visible-thinking model. -
Five default-schema sections now reach the tasks that need them (Issue #491).
schema/config.mdwrites eleven sections and promises "edit it freely", but the per-task delivery whitelist named only six — Source Page Template, Date Fields, Mentions Format, Content Rules, and Multi-Source Merge Rules reached just the twofullcallers (contradiction resolution, fill-empty-page) and never ingest, generation, or merge. A user writing merge policy into the section named Multi-Source Merge Rules was editing the one task that would not see it, and the entity template's "see Mentions Format" pointer dangled. Ruled an oversight (consistent with the #328 Phase 1 contract that schema edits must take effect) after a full call-site audit; the map now delivers Content Rules to analyze/entity/concept, Mentions Format to entity/concept/summary, Source Page Template to summary (audited as the task whose call writessources/<slug>.md), and Multi-Source Merge Rules + Date Fields to merge. Measured prompt growth from the default body: +73 tokens on analyze, +193 on entity/concept, +336 on summary, +204 on merge — under 10% everywhere against ≥5K-token prompts. The unchanged tasks are pinned by a snapshot matrix so they cannot silently grow. Closes #491. -
Reasoning-model empty responses: the typed path now returns what it already recovered, and Welcome translation stops inviting the failure (Issue #506). On reasoning-capable openai-compatible backends the model can spend its whole output budget in the reasoning channel and leave
contentempty; when nothing parseable results the AI SDK throwsNoOutputGeneratedError, and the typed-output client returned{ text: '' }unconditionally — even when it had just composed the reasoning-channel payload into a local variable for exactly this purpose. The catch now returns that composite so caller-sideparseJsonResponsecan recover the embedded JSON (Layer 3), keeping the quiet path only for the origin wheregenerateTextrejected outright (no result exists in scope, and the error class carries no.responseto mine — which is why the recovery reads the in-scope composite rather than the wire body). Both quiet-path debug lines gain an actionable hint naming the per-step policy escape hatch. Root-cause half:welcome-translatenow pinsenableThinking: false— translation is a text task, and reasoning budget spent there was the first-run onboarding failure's cause; a full-pipeline test drives the request through a real client and assertsreasoning_effort: "none"reaches the body. Closes #506. -
Follow-ups from the PR #525 review: the text-mode pin now reaches the Codex provider, a source-borne repetition no longer burns the retry, and a duplicated policy entry is an error. Six items the review filed as non-blocking, fixed together because they are all consequences of the same PR. (1)
openai-codex-sdk-client.tsnever readoutputModeOverride, so the built-inextract→text_promptpin was honoured by every provider except this one, which keptOutput.json()— the SDK's structured-output path, which also routes the reply around the plugin's own JSON repair. It now drops the format when the mode is pinned and carries the JSON instruction in the system prompt instead;forcedTextPromptSystemmoved fromopenai-compat-sdk-client.tstojson-prompt-prefix.tsso both clients apply the identical rule rather than one of them having a private copy. (2) A note that repeats a phrase itself — a refrain, a quoted chorus, a tabulated column — produces a faithful echo the loop detector cannot tell from degeneracy; halving changes how many items are asked for and never the note, so the retry was spent on a certainty and the same batch merged afterwards anyway.isSourceBorneLoopchecks the note before the retry is spent. It requires the note to repeat the unit at least four times, not merely to contain it once: skipping the retry on a genuinely damaged batch costs items silently while spending it on an echo costs one call, so the test has to be the conservative one —Vitamin D,occurs in half a vault and would otherwise suppress the retry for every real loop built from it. This also neededRepetitionLoop.rawUnit:unitis shortened to 40 characters for log lines, so a lookup using it would silently never match a longer refrain. (3) The exhaustion arm — halve budget spent, damaged batch parsed and merged rather than dropped — had no coverage; one test now pins it. (4) ThetaskPoliciesplaceholder was hardcoded English in an otherwise localized control; it is a text key in all 11 locales. (5)parseTaskPolicySpecaccepted a task named twice and let the last entry win silently, which is the manifest lie the parser refuses everywhere else — it now throws. The task NAME stays unvalidated on purpose: every label is a string literal at its own call site, so a list in the parser would be a second copy that drifts, and a stale list rejecting a policy for a step that does exist is worse than the typo it would catch; the behaviour (an unknown name is inert) is now documented where the parser explains itself. 13 regression tests.Upgrade note. Switching LLM Advanced back to Default clears
taskPoliciesalong with the other fields in that block — the control is rendered only inside it, so leaving a value behind would strip its only affordance. If you hand-edited a policy string, copy it before switching modes. -
Constraints pass turned a block-form unknown field into a YAML null (Issue #522, PR #523 DocTpoint).
enforceFrontmatterConstraintscollected its passthrough lines inside its own line walk, skipping every-item — a block-form list under a user-owned key came back as its header alone. It now takes its lines from the sharedextractPassthroughLines, same semantics as every sibling writer. One disclosed side difference: a literalreviewed: falseline is omitted rather than carried verbatim, matchingmergeFrontmatter's long-standing behaviour. With #513 this closes the #356 parity chain end to end. Closes #522. -
Duplicate merge dropped every user-owned frontmatter field of the surviving page (Issue #512, PR #513 DocTpoint).
mergeDuplicatePageswas the one frontmatter writer whoseserializeFrontmattercall carried nopassthroughLines, so a lint duplicate merge silently stripped fields likeredirect_to:orparent_org:from the survivor — the #356 invariant ("re-touching a page never strips fields the plugin does not own") had exactly one writer missing, and it is the path a user is least likely to re-check afterwards. Same helper, same semantics asmergeFrontmatter; the absorbed page's unknown fields stay out on purpose (it is deleted, and merging two differently-authored metadata sets is not this path's decision). Red/green test plus byte-identical control. Closes #512. -
Merge stored whichever source's type arrived first (Issue #509, PR #510 DocTpoint).
mergeFrontmatterunionedsources:two lines above but had no parameter that could carry the incoming source's extracted type — on a measured vault 34.3% of multi-source pages had a classification computed and dropped at every merge, and the stored tag was a function of arrival order. An optional third parameter unions the incoming term (first occurrence wins, idempotent, order-invariant by test);incomingTypeTagwithholds types the active vocabulary does not admit, so custom-vocabulary mode behaves exactly as before. Decision recorded on the PR: union kept over first-wins — order-invariance outweighs single-term purity, and the retag pass remains the normalizer of last resort. Closes #509. -
Test fixture documented OpenRouter's Anthropic baseURL as a path that 404s (Issue #515, PR #516 anavalo).
https://openrouter.ai/api/v1/anthropicin the pass-through table was invisible to CI because the assertion only checks pass-through. Verified live through the repo's own@ai-sdk/anthropicwith a tracing fetch:/api/v1/anthropic/messages→ 404; the/apivalue from OpenRouter's docs (for clients appending/v1/messagesthemselves) → HTML site page;/api/v1→ 200 valid Anthropic-format body, which is also whaturl-fallback.ts:buildModelsPathsalready assumes. Test-only;PREDEFINED_PROVIDERS.openrouterwas already correct. Fixes #515. -
OpenRouter model variants containing
:are visible in Fetch Models (Issue #534). The provider-specific model filter treated:as a disallowed separator for OpenRouter, hiding every:freeand:batchentry returned by its catalog. OpenRouter now keeps both/and:model IDs while the existing Ollama and LM Studio separator rules stay unchanged. A three-provider regression matrix covers the boundary. -
Create path persisted an alias equal to the page's own filename (Issue #536). The model routinely lists the page name among the aliases it writes;
appendAliasesrefuses exactly that viafilterRedundantAliases, butenforceFrontmatterConstraints— the other writer ofaliases:oncreateNewPage,fillEmptyPageandmergeDuplicatePages— did not know the page path and kept it (20 % of the pages in a 2.4 K-page build, 8 of 11 under 1.26.x). The function now takes an optionalpagePathand applies the same gate; callers without a path are unchanged. Space/hyphen variants stay, as before. 5 regression tests. -
Test Connection reported a model that was never selected as
Invalid URL:(Issue #517).DEFAULT_SETTINGS.modelis''andtestLLMConnectionbuilt its probe plan straight from it, so the fresh-install path — pick a provider, paste the key, press the button — put"model": ""on the wire. OpenRouter answers that with HTTP 502{"error":{"message":"Invalid URL: "}}, 502 is retryable, so the AI SDK spent three attempts and thecatchreportedFailed after 3 attempts. Last error: Invalid URL:verbatim: a message that points at the Base URL, which was correct, and never mentions the model.isUrlErroronly rescues 404, so nothing intercepted it on the way out. A blank or whitespace-only probe model now short-circuits before the network with a newerrorNoModeltext (all 11 locales), in the same shape as the existingerrorNoApiKey/codexAuthRequiredearly returns. Per-task mode is covered by the same guard: a task whose model resolves to''aborts before any probe is issued, not after the valid ones. 5 regression tests. Closes #517. -
Custom tag vocabulary: extracted types outside the vocabulary are resolved at intake, not born as lint violations (Issue #527). The extraction prompt lists the active vocabulary and the wire schema enforces nothing (
typeisz.string()), so under a custom vocabulary about one item in ten arrives with the model's built-in taxonomy (person,theory,method— measured 10.0 % of 10 669 items, plus 1.6 % near-miss spellings) and becametags: [person]on the new page:enforceFrontmatterConstraintskept it with aconsole.debug,scanTagViolationsreported it, and the hand-triggered retag later decided the tag from 400 characters of the page's own prose — the source's summary never reached that decision.SourceAnalyzer.repairTypesAgainstVocabularynow runs after accumulation: a deterministic fold onto the vocabulary (case, diacritics;foldToVocabularyincore/tag-vocab.ts), then for what the fold cannot place one shorttype-repaircall with the item's own summary and the allowed terms (TypeRepairLLMSchema,TOKENS_TYPE_REPAIR). Every doubt — parse failure, answer outside the vocabulary, call error — keeps the extracted value, i.e. the previous behaviour; the repairs are logged under[Type repair]. Under the default vocabulary the step is a no-op for every in-vocabulary item and repairs only free-form types. Tests: fold (5), schema (2), analyzer wiring (fold / model answer / unusable answer keeps the value / default vocabulary makes no call). -
Extraction runs in text mode again by default, and a damaged batch is no longer accepted as a short one (Issue #524). Since
266d641(#443, v1.26.3) theextractcall carriedresponse_format: json_schema. Replaying the real pipeline request on LM Studio /gemma-4-26b-a4b-qat, the extraction under that schema degraded in 3 of 3 draws — twice silently, as schema-valid JSON with 4–5 items where text mode returned 17–30 — and the server logs of one vault show 14 repetition loops in 125 schema-mode extraction calls against 9 in 2,368 text-mode ones. Nothing in the plugin could see the silent form:checkEmptyBatchstops on zero items, halving fires onlength, repair on unparsable JSON. Three changes: (1)BUILTIN_TASK_POLICIESincore/task-policy.tspinsextract/extract-retrytotext_promptbelow any user policy — the wire shape every user had before 1.26.3 for the one long-output step; the short judgement calls keep the prober's default and a user entry or*wildcard still wins. (2)core/repetition-loop.tsdetects a unit of 3–120 characters repeated at least four times in a row over ≥ 200 characters (calibrated on 2,319 clean text-mode responses: zero flagged);source-analyzer.tstreats a flagged batch like truncation — halve and retry, else parse what arrived and say so — and logsfinish,reasoning_tokensand the first batch's item counts. (3) ThetaskPoliciesfield from #490 gets a control in the LLM Advanced section (specstep=mode:thinking, parsed on change, an unreadable spec is reported and not saved) — without it no user could move a step off the baseline. Left out on purpose: item-levelrequiredfields in the Zod schemas (the #463 "no scope creep" stance stands) and an item-count plausibility check (a short note gives the same signal as a degraded batch). -
cacheBreakpointwire contract is now pinned at the wire, and two names in the v1.26.4 entry corrected (Issue #493 partial). Every existingcacheBreakpointtest asserts the shape handed togenerateTextthrough a module-level mock, so it cannot see what the SDK core does with that shape — the distinction that produced the original Issue #449 no-op.anthropic-cache-control-wire.test.tsdrives the real@ai-sdk/anthropicadapter through a stubfetchand assertscache_controlin the request body: present on the first of two blocks for a non-zero offset, absent for offset 0 and for no breakpoint, and positioned by UTF-16 code unit (42'Ü'= 42 units / 84 bytes).buildMessagesWithCacheControlnow declines the split when the clamped offset is 0 instead of building a prefix part the SDK core drops — measured as no wire change on the official adapter (the suite passes identically with and without the guard); it removes the empty text block from the shape rather than relying on the adapter to filter it, and gives the condition a documented place. The caller-side half of Issue #493 (markerIdx === -1truncating the template and zeroing the offset) is untouched and stays with that issue. Two corrections to the v1.26.4 entry below:cacheBreakpointis a UTF-16 code-unit offset, not a byte offset, and Anthropic's minimum cacheable size is model-dependent (512-4096 tokens, not monotonic across generations) rather than a fixed 1024. -
Vault root as an ingest or watch target now follows the picker's own exclusion rule (Issues #502 / #505, PR #504).
path.startsWith('/')never matched a real vault path, so a/entry inwatchedFolderswas a silent no-op; choosing the vault root for "Ingest from folder" reinstatedwiki/as source material that the file picker already excludes.isIngestableSourcecomposes the two rules in the same IO-free module asisExcludedFromSourcePickerandisInFolderScope, so all three entry points agree. Upgrade note: a/entry that previously did nothing now watches the whole vault exceptwiki/. If you set/as a placeholder, remove it or narrow it to the folders you actually want ingested before the next run.
npm auditHIGH→0 onmain(Issue #501). Thepnpm.overridesblock introduced by the v1.26.x Pin (164b06a) is not visible to npm — npm reads only the top-leveloverrideskey — sopackage-lock.json(which Obsidian'srelease.ymlinstalls from) kept three vulnerable transitive copies ofbrace-expansion(1.1.16, 2.1.2, 5.0.7) across five paths even after the pin was merged.package.jsonnow declares both keys (overridesfor npm,pnpm.overridesfor pnpm) with the same flat values, andpackage-lock.jsonresolvesbrace-expansionto a single5.0.9line.npm audit --audit-level=highreports 0 vulnerabilities; the four affected paths (eslint-plugin-import,eslint-plugin-n,eslint-plugin-react,eslint-plugin-json-schema-validator) drop the HIGHGHSA-mh99-v99m-4gvgandGHSA-rgw5-rvv9-x895advisories. pnpm side is unchanged. Note for future maintainers: the same flat-form values are intentional — pnpm does not honour the top-leveloverrideskey, and npm does not honourpnpm.overrides. The two fields must stay in sync; the lockfile-regen step inobsidian-plugin-releaseStep 3b (npm install --legacy-peer-deps --package-lock-onlyin the project directory) is the one place this is enforced.
20 merge commits (74 files, +3786/-1816 LOC, 3290 → 3434 tests). Six-axis fix batch: silent-bug cluster, wire-shape accuracy, lint report UX, extraction prompt bloat, contradiction data-loss, per-step measurement scaffolding.
-
Settings: per-task model fields wiped on every commit (Issue #456, PR #462).
commitTempSettingsran a v1.24.1 PATCH Phase 5.5.0 belt-and-suspenders cascade that fired acommit-time model changeevent on every commit, overwriting per-taskmodelOverride/temperature/topP/seed/enableThinkingoncommitTempSettings. The 3 direct-write sites that bypasssetFieldValue(provider change / Bedrock region change / post-test sync) all hit it. Fix removes the cascade entirely; per-task fields are owned bysetFieldValueandcommitTempSettingsis a pure pass-through. 3 unit + 2 integration regression tests pin the contract. Closes #456. -
Wrapper:
createMessageStreamdropped every advanced setting (Issue #451, PR #465).wrapWithAdvancedSettingsusedObject.create(client)to inheritcreateMessageStream, which sidestepped the settings-injection seam entirely — temperature / top_p / seed / repetitionPenalty / enableThinking were silently dropped on the Query Wiki streaming path (the only user-facing stream surface). Fix mirrorscreateMessageWithOutput's wrapping shape: hook the stream method, build sampling args + provider options identically, apply at the seam. 4 wire-body regression tests pin injection reaches the call. Closes #451. -
LLM semantic dedup:
parseJsonResponseparse failure read as "no match" (Issue #407 Stage 1, PR #444).path-resolution.ts:220collapsed{ok: false, reason}fromparseJsonResult(the union added in PR #436) into the samematch: falsebranch as a legitimate negative answer. Fix routes{ok: false}through a newparse-failurelog + the slug-path fallback (named failure rather than silent answer); thematch: falsebranch is reached only when the reply parsed. Counter-test pins the new branch: a well-formed{match: false}is not reported as a parse failure. 2996 → 3000 tests. Stage 2 of #407 follows in v1.27.0. -
Output schemas: extraction wire schema had no top-level
requiredarray (Issue #463, PR #476).SourceAnalysisLLMSchemacarried.passthrough()AND marked every top-level field.optional(), so the wireadditionalProperties: truehad norequiredto enforce and the model's reply (source_title_,summary=, phantom", ": "") was formally valid. Drop.optional()fromentities+conceptsonly;.passthrough()stays (per the user requirement "针对一些格式内容多变的属性,必须留好冗余空间").source_title/summary/key_points/related_pages/contradictionsstay.optional()becausenormalizeBatchResponsedoes not consult them for batch validity (only entities + concepts do) and the runtime has explicit fallbacks. The "accepts{}" test is inverted:{}is now REJECTED (the constraint that was missing);{entities: [], concepts: []}is the empty-batch signal. 3320 → 3328 tests. Closes #463. -
Lint prompt:
fillEmptyPagehardcoded default tag taxonomy contradicted runtime injection (Issue #459/#460, PR #460 DocTpoint). The fix line atsrc/wiki/prompts/fixes.ts:47enumerated ≥3 default-taxonomy values, silently breaking disjoint custom vocabularies (biochemistry, legal domain, etc.). Defer to system-layer Active Tag Vocabulary (the same path runtime uses). 68 regression tests pin the runtime vocabulary reaches the lint task + noFIX_PROMPTSline enumerates default-taxonomy values. Closes #459/#460. -
Path resolution: drop the alias latch on ambiguous fallback (Issue #446 follow-on, PR #478).
path-resolution.ts:174previously latched an extracted name as a wiki alias whenever a name-collision fallback was taken, polluting the alias index with single-extraction noise. A name claimed by two pages resolved through the latch to whichever one extracted first; re-extraction then hardened the wrong answer. Fix: drop the latch entirely. The ambiguous case now reports "neither" (per #446's lesson) and the typed-list fallback takes over. 3000 → 3004 tests. Closes #446 follow-on. -
Extraction: freeze the slug catalog per run so the prompt cache survives (Issue #452, PR #483). The catalog is the first block of
analyzeSourceand ~91% of its characters — the span every prefix cache (Anthropic / OpenAI / llama.cpp KV reuse) would otherwise re-prefill every note.buildCompactSlugListre-sorted per call, so pages the previous note created sorted into the middle and the reusable span ended there. Measured on LM Studio (gemma-4-26b-a4b-qat, 2844 slugs, 24.5 K prompt tokens,max_tokens=1): 23.5 s prefill/note against 4.39 s when new slugs are appended instead. Fix: a folder/batch ingest carries aRunSlugCatalogon itsBatchRequirementsContext— sorted snapshot at run start, pages that appear during the run appended in first-seen order so an earlier append never shifts a later one. Single-file ingests pass no catalog and get the freshly sorted list as before. Two consequences, both deliberate: deleted-mid-run pages stay in the catalog until the run ends (dropping them reintroduces mid-list divergence; stale targets resolved downstream byPageFactory.resolvePagePath), and re-ingesting a source inside the wiki folder still excludes its own slug per call (one-line shift; sources outside the wiki folder, the normal case, were never in the catalog). This makes the block prefix-stable within a run — Direction 1 of #449; Direction 2 (cross-run caching) is thecacheBreakpointwire-up below. Closes #452. -
AI-SDK migration: reasoning-only guard dropped (Issue #470, PR #488). The v1.26.0 Batch 6 4-layer force-disable thinking lost Layer 1 (
reasoningEffort: 'none') when the AI-SDK v6 client migration reshuffled the buildProviderOptions destructure:reasoningEffortwas no longer threaded through to the wire on any openai-compat provider. Fix: threadreasoningEffortthroughbuildProviderOptionsfor every openai-compat provider that does not have a Zod-declared equivalent, with the reasoning-strip-probe cache unchanged. The 400-strip retry path also acceptsreasoningEffortso a backend that rejects the field gets a clean retry rather than a second 400. 2 regression tests pin the wire-body assertion (reasoning_effort: 'none'IS on the body). Closes #470. -
Three-layer repair for DeepSeek reasoning-model ingest (Issue #474, PR #486). Three failure modes collapsed into "Failed to connect to deepseek API". Layer 1 — prose reasoning pollution:
prependReasoningForParsealways prependedreasoning_contentbefore visible text; when reasoning is prose (deepseek-v4-flash narrative thinking) and visible text is JSON, the parse target became prose + JSON and everyparseJsonResultlayer walked into the prose first. Fix: drop reasoning when it has no<think>wrapper AND visible text is non-empty; the Qwen3.5 JSON-in-reasoning case (text='' + reasoning=JSON) is preserved (still prepends); the R1 / o-series<think>-wrapped case is preserved (still wraps). Layer 2 —NoOutputGeneratedErrormisclassification: AI SDK's step-retry exhaustion path throwsNoOutputGeneratedError(sibling ofNoObjectGeneratedError; both extendAISDKError); the catch only checkedNoObjectGeneratedError.isInstance(err), so the sibling slipped through andmapAiSdkErrorrewrote it as "Failed to connect to deepseek API" — a budget problem misreported as a connectivity error. Fix: catchNoOutputGeneratedErrorin bothcreateMessageandcreateMessageWithOutput; return''/ empty shape so the caller'sparseJsonResponseempty-input path handles it;finishReason: 'stop'is the right semantic. Layer 3 — output mode reporting honesty:OutputModeProberdefaulted tojson_schemafor every provider, butOpenAICompatSdkClientis constructed withsupportsStructuredOutputs: falsefor 5 cloud openai-compat providers (deepseek / kimi / glm / minimax / openrouter); the SDK encodesjson_objecton the wire regardless, silently dropping caller-supplied schemas. NewgetCurrentOutputMode(model)pre-seeds the cache tojson_objecton the first call when!supportsStructuredOutputs— initialization, not demotion.outputModereports the wire shape the SDK actually emits; side benefit is the wastedjson_schema → json_objectdemotion cycle on first 400 is skipped (1 HTTP call saved per first failure on these providers). 3328 → 3391 tests. Closes #474. -
Page-batch-runner test: relax timing assertion slop (PR #489). A 30 ms timing assertion on the page-batch-runner internals flaked under CI's variable load. Relaxed to 25 ms slop (10/10 runs clean on a 6-vCPU runner). 3391 tests. No behaviour change.
-
CI: Gate 1 PR-time status check (PR #487).
.github/workflows/pr-ci.ymlruns the full Five-Gate (lint + tsc + build + test + css-lint) on every PR tomain; status checkGate 1 / Five-Gateis now a branch-protection requirement. Order is non-negotiable: build before test —openai-codex-loopback-flow.test.ts:39readsmain.jsto verify esbuild bundle shape, so a test-before-build run on a fresh clone fails ENOENT. CLAUDE.md §"Gate 1" was corrected to match. Localpnpm lintremainssrc/-only (the Obsidian Bot scans the whole repo.tstree, not justsrc/, andpnpm lint:tools-botis the local pre-check). Lockfile-pinned install (pnpm install --frozen-lockfile) preventseslint-plugin-obsidianmddrift between local Gate 1 and CI. CI is defense-in-depth — explicit "merge it" still required per CLAUDE.md §"Git Safety Protocol". -
Final lint analysis sent an uncapped full-wiki context, 400-ing on 49K-context local models (Issue #473, PR #494).
runAnalysisPhase(src/wiki/lint/llm-phases/analysis-phase.ts) read the entirewiki/index.mdand injected it into the LLM prompt. On a 1,690-page vault that is ~152K input tokens against an LM Studio 49K context window → HTTP 400. Decision (established with the maintainer from Karpathy first principles): instead of capping the prompt or adding a token-budget estimator, remove the LLM analysis section entirely. The prompt asks the LLM to judge a whole wiki it only sees 8 pages of; schema-suggest keeps the LLM-advice path on the "Analyze Schema" button. This deletes the section, its 5 accumulated regressions (duplicate## LLM 分析heading — root cause:report-builder.ts'scleanedLLM.startsWith('##')guard fires on empty string — leaked chain-of-thought, nested<ul><ul>, repeated headings, JSON parse failures), and one LLM call per lint. Side-effect fix in same PR: #474'sprependReasoningForParsechange dropped Query reasoning — query stream paths (openai-compat 4 sites, openai-codex added) now usewrapReasoningContent; anthropic stream uses the shared helper. Net -563 LOC; 3391 → 3401 tests. Closes #473. -
Startup quick-fix completion Notice was 6 lines of "everything is fine" on a healthy vault. The three per-check detail lines (structure / sources / incomplete) are now emitted only when that check actually needed fixing; a routine morning startup shows just title + page-count summary + disable hint.
-
Wire-up:
cacheBreakpoint→cache_controlon the user-message prefix, not the system block (Issue #449, PR #464).cacheBreakpointis a UTF-16 code-unit offset (String.length, not bytes) into the FIRST user message's text content (set bysource-analyzer.ts:404asstaticPrefix.length). Anthropic prompt caching is a prefix match on render ordertools → system → messages; a marker on the system block cachestools + system(a few KB, below Anthropic's minimum cacheable size on every current model — the floor is model-dependent and not monotonic across generations, 512 to 4096 tokens, so it is not a constant) and leaves the 75K-char user-message prefix uncached — the silent no-op class of regression that pre-fix had been shipping. Fix: replacebuildSystemWithCacheControl(emitsSystemModelMessage[]withcacheControlon the system block) withbuildMessagesWithCacheControl(emits the first user message as twoTextParts cut at the offset, withcacheControlon the prefix part). AI SDK v6's Anthropic adapter (@ai-sdk/anthropic/dist/index.mjs:2316-2340) readscacheControlfromTextPart.providerOptionsand emitscache_control: { type: 'ephemeral' }on the matching wire block. system stays a plain string. Branch D non-blocking fix (DocTpoint): call-site spread is now...(system ? { system } : {})(truthy-check), droppingsystem: ''from the wire. 5 tests rewritten to inspectcall.messages[0].contentasTextPart[](the previous 5 pinned the wrong wire shape —call.systemasSystemModelMessage[]); 1 new test covers Branch D. Wire-shape net: pre-fix hadcache_controlon system block (below cache floor → no cache hit); post-fix on user prefix →cache_creation_input_tokenson first call,cached_input_tokenson subsequent calls (all-prefix reuse). On a 2,838-page vault (75,876-char prefix) the saving is ~22-25K input tokens per post-first batch. 6 tests. Closes #449. -
Contradictions: clamp the page in sections and restore what was withheld (#287 follow-on, PR #492).
ContradictionManager.resolveContradictionsent the affected page asexistingContent.substring(0, 6000). Three facts on that path composed badly: the prompt asks the model to preserve every existing fact and output the complete repaired page; the answer is written back over the file withcreateOrUpdateFile; nothing preserves what the model was not shown. For pages above the budget the model never saw the tail, answered with what it believed was the complete page, and the write replaced the original — silent data loss on 55 of 2,416 knowledge pages (2.3%) on a typical vault, with the largest page dropping four-fifths. Same family as #292 and #287, one path further along. Fix:src/core/clamp-page-sections.tsclamps in whole##sections (drops from the end, never mid-sentence), names the omitted sections in the prompt text, returns the withheld blocks verbatim sorestoreWithheldSectionscan put them back after the rewrite. Below the budget the return is byte-identical (97.7% of pages). A page over budget with no##boundary (preamble alone busts the budget, or no heading at all) is refused withhardCut: true— the rewrite is refused rather than written back incomplete. The contradiction record is clamped through the same helper (read-only path). The merge-triage path is left alone (median 1,424 / p90 2,695 chars; the payload does not grow with the vault there). 11 + 3 tests cover byte-identity below budget, whole-section dropping, the marker, verbatim withheld blocks in document order, no-boundary hard cut, and the integration path: the prompt admits the omission and excludes the tail, the file keeps the tail anyway, and the unclamped case reaches the model unchanged. Closes #287 follow-on.
-
Per-task policy: choose output mode and thinking per pipeline step (Issue #481, PR #490).
taskPolicies?: TaskPolicyMaponLLMWikiSettingsmaps a task label to{outputMode, thinking}, resolved specific → wildcard → default. The client wrapper applies it at the one seam every call already passes through, so no call site changes. An unset policy spreads{}and the path stays byte-identical to today. The spec format isextract=text:on,merge-triage=text:on,page-generate=-:off;parseTaskPolicySpecthrows on anything it cannot read (a silently-ignored entry would mean an arm that did not run what its own manifest says it ran). Two mechanics the wire forced: a pinnedtext_promptputs noresponse_formaton the wire, so the JSON shape has to come from the prompt —forcedTextPromptSystemaddsJSON_ENFORCEMENT_SYSTEM_PREFIXup front (the 400-driven demotion adds it at retry time; a pinned mode has no retry path);low/medium/highsendreasoning_effortand the reasoning-strip retry deliberately drops the field (the backend just rejected it, so re-sending it would only earn a second 400).low/medium/highare indistinguishable on LM Studio / gemma-4-26b-a4b-qat (byte-identical output at 417 reasoning tokens);reasoning_effort: nonedoes switch reasoning off. The levels are the standard openai-compatible field and are carried for the backends that honour it. The stream path is untouched — it carries notasklabel (#469), so there is nothing to key a per-step decision on. The setting is deliberately not exposed in the UI — the settings worth offering are the ones a measurement has picked out, and this field is what makes that measurement possible. 3430 → 3434 tests. Unblocks #481. -
Extraction payload stops growing with the vault (Issue #482 stages 1+2, PR #484). Stage 1 removes the slug catalog that was the first block of
analyzeSourceand ~91% of its characters (2,843 lines / 69,355 chars on a mature vault). The prompt is now instructions plus the note, so its prefix is identical for every note — the best case for prefix caching — and per-note cost is a function of the note instead of the vault. Requirement 7 (related_pages) went with it:source-analyzer.tsoverwritesaccumulation.relatedPagesunconditionally with the programmatic match whenever anything was extracted, so the LLM output was paid for and discarded. Stage 2 stops showing the candidate list in generation/merge prompts and resolves every related link after generation against an index of every page: title first, then curated aliases — so[[E433]]lands onentities/Polysorbate(a connection no candidate window could contain, because the page's title is not the name in the prose). An alias claimed by two pages resolves to neither (#446 lesson); bare[[Name]]links are now in scope, since a prompt without a path list produces them. A name the vault does not know keeps the previous behaviour (folder from typed related lists, slug from the name) so the dead-link/stub path that requirement 3 of the generation prompt relies on is unchanged.buildPagesListForPrompthad no remaining caller and is removed with its facade and its tests; the #234 invariant it carried — sources/ is never a body-link target — now sits on the resolver, which cannot select a sources/ page at all. The closing commit removes the run-scoped catalog plumbing #483 added: stage 1 makes the block it stabilises unnecessary. Acceptance measurement (2,838-page vault): Stage 1 — 4 usable draws each, catalog arm runs round 1 then loses round 2 to the output ceiling every draw (398-421 s); stage 1 arm runs both rounds (80-110 s). Round 1 differs (mergeBatchResultsis strictly additive; a name in only one arm cannot have been dropped by a later round). Stage 2 — 8,800 related links: 7,962 → 8,191 resolving (90.48% → 93.08%); +235 newly resolving, −6 (one class: links that resolved only because a sources/ page carried the name — the #234 invariant arriving in its new home; the resulting 6 dead links convert to stubs via Fix Dead Links, tracked separately as #485). Net -288 LOC; 3434 tests. Closes #482 stages 1+2.
A surgical PATCH that closes the pre-submission blind spot exposed by the v1.26.1 Obsidian Bot pre-review. The bot scans the whole repo .ts tree while local pnpm lint only scans src/ — and v1.26.1 shipped a blocking no-unsafe-call Error in tools/llm-wiki-cli/src/obsidian.ts that local lint never saw. No behaviour change, no new settings, no migration. The headline: the CLI's obsidian.ts:117 await import() chain is now type-safe AND exempt from obsidianmd/no-nodejs-modules, and a pnpm lint:tools-bot script closes the local blind spot so the next release doesn't need a Bot trip to surface what local lint should have caught.
- CLI
obsidian.ts:117blockingunsafe-callError (#442).await import(<dynamic-arg>)leftrequestasany, cascading 6unsafe-*warnings and triggeringno-unsafe-call→ Error. Split into two literalawait import('node:https')/await import('node:http')branches with explicittypeof import('node:http').requestannotation — Error + the entire unsafe-* cascade disappear in one stroke. obsidian.ts:158requestUrl().jsoncontract (#442).JSON.parse(text) as unknown+ try/catch that re-throws with the status context, matching Obsidian host's behaviour on bad JSON.main.ts:443loadSettingsJSON.parseargument type (#442). Now typed asPartial<LLMWikiSettings> | nullto matchapplySettingsMigrations' declared parameter; eliminatesno-unsafe-argument.main.ts:647globalThis.crypto.subtle→crypto.subtle(#442). Node 18+ exposescryptoas a global; the explicitglobalThis.prefix was trippingobsidianmd/no-global-this(no-disable rule).vault.ts:291redundantas Record<string, unknown> | nullremoved (#442).parseFrontmatteralready returns aFrontmatterData | nullwhose index signature is compatible — Bot flagged as no-op assertion.node-globals.ts:28(...args: any[])→(...args: unknown[])(#442).Consoleconstructor acceptsunknown[]; eliminates a Bot-flagged bareanyand theUnexpected anywarning.
- Local
tools/blind-spot closure:pnpm lint:tools-bot(#442). Neweslint.tools-bot.config.mjs(obsidianmd recommended ruleset scoped totools/**, type context fromtools/llm-wiki-cli/tsconfig.json, Node globals declared) and a matchingpackage.jsonscript (|| true, informational — never gates CI). Developers now see the Bot's view of the CLI tree locally instead of discovering it post-submission. Platform.isDesktopAST guards on the three runtime-loadednode:*imports (#442).obsidian.ts:requestUrl()andnode-globals.ts:plainConsole()each carry a function-startif (!Platform.isDesktop) throw new Error(...). The CLI's own Platform shim hardcodesisDesktop: true, so the guards never throw at runtime — they declare the desktop-only invariant theobsidianmd/no-nodejs-modulesrule requires (verified against the rule source; bare dynamic imports are not exempt, contrary to the assumption baked into PR #418/#433's patterns). Mirrorssrc/llm-sdk/openai-codex/loopback-flow.ts:156-160.
- Release skill v1.7.0 now mandates an Obsidian Bot pre-review (Step 6b.5, HARD STOP ②) between tag and publish. This is the gate that should have caught v1.26.1's pre-publish — making it explicit instead of relying on the maintainer remembering to submit. See [[feedback_obsidianmd_no_nodejs_guard_detection]] for the rule-detection mechanism.
- All 7 remaining
tools/warnings are accepted-structural (staticnode:fs/path/fs-promises/utilimports,.obsidianliteral,console.logoutput interface,globalThisshim). Dynamic form would break the 14 parser-contract tests that pinparseCliOptionsas sync. The honest long-term fix is the CLI split into a separate repo ([[project_v1_27_0_cli_split_planning]]).
A surgical PATCH that closes five UX blind spots the maintainer discovered while validating the v1.26.1 / v1.26.2 release on the production vault. None of these touch the LLM pipeline (PR #447's v1.26.3 PATCH owns that work — Phase A 3-tier state machine, Path 2 fix, Phase B 11 caller migrations, per-model placeholder demotion, pushed 2026-08-12, awaiting DocTpoint re-review); this entry covers the settings/lint/UI bugs the maintainer surfaced during the same E2E round and shipped on a separate branch (fix/ux-b1-b2-b3-provider-statusbar-dedup): Fetch-Models error classification (B1), status-bar cancel affordance (B2), lint dedup cross-type filter (B3), full status-bar progress i18n (B2.5), and the remaining hardcoded-English Toasts.
-
Settings: Fetch Models misclassified auth/endpoint/server failures as "Network" (
B1).fetchOneUrlinmodel-section.tssilently returned[]on non-2xx and the outer catch rewrote every error to the status-lessAll URL candidates failed—classifyFetchError's regexes (\b(401|403)\b/\b(404|405|...)\b/\b5\d\d\b) had nothing to match, so every auth/wrong-URL/server failure surfaced asfetchErrorNetwork. Verified across all 7 cloud providers (OpenAI / Anthropic / DeepSeek / Kimi / MiniMax / GLM all return 401 for invalid keys; LM Studio / Ollama have no auth).fetchOneUrlnow throwsHTTP {status}: {body-snippet}on non-2xx;fetchModelsWithFallbacktrackslastHttpErroracross all candidates and re-throws it; the outer catch passes the original error through unchanged. The classifier's existing regexes now hit on the first try. DocTpoint CR follow-up (2026-08-12): two unknown-case regressions the original fix introduced were closed — a 2xx with an empty/absentdataarray is now a valid[]return (theHTTP 200throw had no classifier branch → misreported Network), a true network failure is tracked aslastNetworkErrorand re-thrown as Network (previously it collapsed into[]→empty model list→ the misleadingfetchErrorEmptyfor a disconnected user), andclassifyFetchErrornow matches the leading^HTTP (\d+)BEFORE the keyword regexes so a 5xx whose body containsunauthorizedis Server, not Auth. -
Settings: every locale's
fetchErrorNetworknow mentions the API Key as a fallback hint (B1). True network failures (DNS / connection refused / timeout) still fall through tofetchErrorNetworkand cannot be disambiguated from auth by status alone; the fallback message now suggests checking the Key alongside network settings. 10 locales updated (en,zh,zh-Hant,ja,ko,de,fr,es,pt,it,ru) with an i18n-parity test that asserts every locale's message matches a Key-mention regex. -
UI: status-bar update path dropped the "click to cancel" label (B2).
setStatusBarUpdateCallbackincommand-registry.tscalledsetText(text)directly with the raw progress text, dropping the always-visible base label for the entire duration of long ingest/lint batches. Users sawAnalyzing batch 2/3 (0 entities, 5 concepts so far)...with no indication the bar was clickable to abort. The click handler itself was already wired (line 156) — only the affordance text was missing. NewcomposeStatusBarUpdatehelper insrc/core/status-bar.tsselects the active label (ingest > lint, mutex in practice) and appends it as a stage segment viabuildIngestStatusBarText, restoring the docblock contract. The callback now hides the bar (returns null) when neither is running, instead of leaving stale text on screen. DocTpoint CR follow-up (2026-08-12): the PDF emitter (ingestPdfSource'ssetPdfStage) passed an already-composedbuildIngestStatusBarTextstring — which already ended in the base label — intoupdateStatusBar, socomposeStatusBarUpdateappended the label a second time on every PDF stage (My Note.pdf · Reading PDF… · Ingesting… · Ingesting…).setPdfStagenow emits raw segments ([filename, stage].join(' · ')) so label composition happens in exactly one place. -
Lint dedup: cross-type pair filter (B3). The dedup candidate generator emitted entity↔source and concept↔source pairs that shared a wiki subfolder bucket (
tp:/ic:/lh:), polluting the LLM verify batch with nonsense questions. Per the #358 complementary memory model, a source mentioning an entity by name is NOT a duplicate of the entity — they live in different cognitive registers. Per the maintainer's 2026-08-12 direction, the dedup now considers only: entity↔entity, concept↔concept, entity↔concept, source↔source. Forbidden: entity↔source and concept↔source (in any order). NewpageTypeOf(path)helper infers page type fromWIKI_SUBFOLDERSsegments; newisCrossTypePairAllowedguard rejects forbidden pairs ataddCandidateinjection time using canonicalizedsmaller|largerstring keys. DocTpoint CR follow-up (2026-08-12): two B3 refinements — the anti-regression comment now states the true root cause (the canonical keyconcept|entityMUST be present inALLOWED_PAIR_KEYS; thea < bcomparison was never the miss — it produces an identical key either way, so a future editor must not 'simplify' it), and the rejected-pair count is surfaced viahooks.onCrossTypeRejectedin the dedup-phase candidate debug line so the filter's effect is measurable. -
Status-bar progress text fully localized (B2.5). 16 hardcoded English status-bar strings in
wiki-engine.ts/conversation-ingest.ts/source-analyzer.ts(e.g.Analyzing batch 1/3...,[7/10] Concept: <name>) produced mixed-language bars on non-English vaults. 18 new i18n keys (status-bar stage +Entity/Concepttype labels) across 10 locales; everyonProgressstring now flows throughgetText(). 22 i18n-parity tests pin the placeholder contract ({current}{total}{filename}etc.) so a translator dropping a placeholder fails loudly. -
Remaining hardcoded English Toasts localized (B2.5 follow-up). User E2E found
Ingesting: <file>Toasts still English while the status bar was fully localized. Full sweep of everynew Notice()/showProgressFor()call site found exactly three hardcoded strings —Ingesting: {filename}(single-file manual ingest),Checking for already-ingested files...(batch pre-scan), and<N> findings(auto-lint completion). 3 new i18n keys + 10 locales;lintFindingsSummaryis a full phrase ({total} findings) so each locale can order/pluralize freely. 22 i18n-parity tests. -
repetitionPenaltyuser setting was a silent no-op on every shipped provider (Issue #414, PR #453). Since the v1.23.0 AI SDK migration dropped the pre-AI-SDKunsupportedFieldsblocklist, the setting flowed through to wire on no path: LM Studio / Ollama / llama.cpp received the wrong spelling (repetition_penaltywith-ion; llama.cpp recognizesrepeat_penaltyper DocTpoint #414 type-error test on gemma-4-12b); Kimi / OpenRouter / vLLM saw the field placed underproviderOptions.openaiCompatiblewhile the AI SDK's openai-compat passthrough at@ai-sdk/openai-compatible@2.0.62/dist/index.mjs:525-540readsproviderOptions[this.providerOptionsName](the provider id) — the key mismatch meant the lookup missed for every provider; Anthropic received the field but its Messages API has norepetition_penalty(onlytemperature/top_p/top_k); DeepSeek / OpenAI / OpenAI Codex / Ollama (OpenAI-compat) / Gemini / MiniMax / GLM / Bedrock-OpenAI do not list the field at all. Per-provider dialect dispatch inOpenAICompatSdkClient.buildProviderOptions:lmstudio/ollama→ wirerepeat_penalty(no-ion);kimi/openrouter/custom→ wirerepetition_penalty(snake_case, OpenAI-spec);deepseek/gemini/minimax/glm/bedrock-openai/ unknown → field dropped silently. The return key flips from{ openaiCompatible: openaiOpts }to{ [this.provider]: openaiOpts }so the SDK's per-id-key passthrough delivers the field. The Anthropic client drops the field entirely instead of placing an unrecognized key on the wire — matches the 10-locale i18n text "cloud providers will silently ignore it". OpenAI / Codex unchanged. The dialect table +repetitionPenaltyWireField(provider)helper lives insrc/core/repetition-penalty-dialect.ts(re-exported fromopenai-compat-sdk-client.tsfor test parity; placed in core to avoid a circularcore↔llm-sdkimport — see [[project_v1_26_3_pr454]]). No 400-strip retry forrepetitionPenalty: the setting is user-opt-in (not a plugin default), so a backend rejection should surface to the user rather than be silently swallowed (dead-code-as-docs policy + half-life rule). One-lineconsole.debug([REPETITION-PENALTY-EMIT], mirrors[REASONING-STRIP-DEBUG]) so users with developer-mode debugging can verify the wire contract on their backend. Known limitation:wrapWithAdvancedSettings(src/llm-client-wrapper.ts) usesObject.create(client)to inheritcreateMessageStreamwithout settings injection —repetitionPenalty(and all other settings) is silently dropped on the stream path (Query Wiki, streaming UI). Tracked as #451 for v1.27.0; this fix lands only on the non-stream path. Closes #414. 3212 tests / 230 files (+3 are the new dialect dispatch tests). -
Frontmatter writer silently emptied the
sources:field on re-ingest (Issue #438, PR #450).enforceFrontmatterConstraintsran AFTERpreserveExistingSourceswas already merged, so the final write dropped any pre-existingsourcesvalue that didn't satisfy the new constraint — silently. DocTpoint Finding 1 fix (commit2560ab4): filter empty-string entries frompreservedSourcesbefore merging, so a baresources:header (no values) no longer re-emits assources:\n - "". Finding 2 (whole-class passthrough viaextractPassthroughLines) tracked as a follow-up PR — same PR series, separate commit. Per-vault impact: 321 affected pages in the maintainer's vault. CHANGELOG-side note: previously this regression was hidden by the v1.25.11 frontmatter-writer audit (which validated YAML shape, not value preservation); the fix lives at the writer-merge layer. -
Placeholder detector missed
{"": {}}/{"": []}empty-object/array variants (Issue #443 follow-up, PR #454). The grammar-constrained JSON-repair gate (isPlaceholderObjectinsrc/core/json.ts) only caught{"": ""}(empty string under the empty key). User E2E on LM Studio / qwen3.5-9b (2026-08-13) showed the model emits an empty OBJECT or ARRAY under the empty key when the grammar token is pluralised — both bypassed the existing gate and returned the broken JSON to the caller, triggering the source-analyzer's empty-result path. The detector now uses a single-passObject.entries(...).every(isEmptyJsonValue)over all keys (not just the first), so empty object/array/string/number/null variants are caught uniformly. The throw-wiring test inwiki-engine-repetition-penalty-hint.test.tsconfirms the placeholder gate still routes failures to the "Source analysis failed" path with the localized repetitionPenalty hint attached (gated bycore/repetition-penalty-dialect.tsso providers that never putrepetitionPenaltyon the wire don't get a misleading hint — see the #414 entry above). -
RepetitionPenalty UX hint on
Source analysis failed(PR #454). User feedback 2026-08-13 (LM Studio / gemma-4-12b / qwen3.5-9b): when a customrepetitionPenaltyvalue silently broke grammar-constrained extraction (qwen3.5-9b), the user-facing error was a genericSource analysis failedwith no mention of the setting. NewbuildRepetitionPenaltyHint(language, value, provider)helper appends a localized hint to the throw site (wiki-engine.ts:905-908) ONLY when (a) the user opted into a custom value AND (b) the active provider actually puts the field on the wire (repetitionPenaltyWireField(provider) !== null). Hint suppressed on anthropic/deepseek/gemini/minimax/glm — never put the field on the wire, so a "reduce or clear" hint would be misinformation. Settings description extended in 10 locales (the existingrepetitionPenaltyDesci18n text already warned about silent-drop on cloud providers — the new failure-path hint is a complementary UX surface for the wire-supporting subset).
- Five PRs land in this PATCH. #447 (LLM pipeline, 39 commits, Phase A 3-tier + Path 2 + Phase B 11 caller migrations + per-model placeholder demotion) + #448 (UX fixes, 10 commits, B1-B3 + B2.5 + Toast i18n) + #453 (Issue #414 dialect, 4 commits: client + wire-shape tests + Anthropic drop + 400-strip documentation) + #450 (Issue #438 sources-loss, 1 commit for Finding 1; Finding 2 follows separately) + #454 (placeholder detector widening + repetitionPenalty UX, 4 commits: detector + UX + simplify/code-review cleanup + provider gate). #450 + #454 still awaiting DocTpoint re-review at tag time; release is held until both clear.
- No settings-schema change, no migration, no key rename. 21 new i18n keys (18 B2.5 status-bar + 3 Toast) added across all 10 locales + the
fetchErrorNetworkvalue change + 1repetitionPenaltyErrorHintkey + the existingrepetitionPenaltyDescextension. i18n-parity guards (bidirectional + placeholder-drift + non-empty) pin all new content; existing fetch-flow regression guards pin the HTTP-status re-throw behaviour; new dialect-dispatch tests pin the per-id-key passthrough at the wire boundary; newwiki-engine-repetition-penalty-hint.test.tspins the throw-site wiring. - Test count growth. v1.26.2 → v1.26.3: 2992 → 3305 tests (+313 / +12 net files after 5 PRs land). Per-PR deltas: #447 +135 / #448 +58 / #453 +3 / #450 +4 / #454 +5. Composition and full regression-guard list in ROADMAP v1.26.3 PATCH track.
- CLI repo split is now live in v1.27.0 scope (see ROADMAP §v1.27.0). The in-tree
tools/llm-wiki-cli/remains the canonical CLI source until v1.27.0 ships; the README §Headless CLI was rewritten to point at the publishedkarpathywiki-clinpm package + the standalone sibling repogreen-dalii/obsidian-llm-wiki-cli. No code change; only user-facing docs.
-
parseJsonResultdiscriminated union for LLM JSON parse outcomes (Issue #407 Stage 0, PR #436, commitad02b0e). No behaviour change; the union gives the failure a name:{ok: true, value}/{ok: false, reason: 'empty' | 'malformed' | 'exception'}, so a parse failure can no longer be read as a negative answer, and under the unionparsed?.field || fallbackstops compiling at a call site that ignores the distinction. Every current call site keeps the oldparseJsonResponseand is untouched — identity shown over 776 input combinations across return value, thrown error, and the full console call sequence includingdebug. Call-site migration follows in Stages 1+2 as one PR per site, starting with the two highest-blast sites,path-resolution.ts:220andconversation-ingest.ts:337. -
Per-step LLM timing ledger (PR #409, eucher). Each
createMessagecall now carries atasklabel naming the pipeline step; a process-global ledger (src/core/llm-task-usage.ts) accumulates call count + wall-millis per label at the single seam every call passes through (wrapWithAdvancedSettings). Callers snapshot before the work and diff after — deliberate, since a reset would be wrong the moment two ingests overlap. A phase as large as page generation (one interval covering path resolution, dedup call, page writes, merge routing) now decomposes into per-step timings, so a slow ingest says which step to look at. The ledger is cumulative for the process; an unlabelled call lands in'untagged'rather than being dropped, so the table never under-reports the run it exists to explain.
- Duplicate
sources:frontmatter key on stub-created concept pages (Issue #399, PR #405, commit4c43cdfb). v1.25.11 regression inappendSourceSlugToFrontmatter; produced two top-levelsources:keys (invalid YAML) on post-stub ingests, breaking Obsidian Properties render. Two-sided fix inbuildStubContent+appendSourceSlugToFrontmatter. Corpus: 321 affected pages in @borthwick's vault. 5 new unit tests + 3 parser-shape guards (realyamlpackage assertion thatsourcesisstring[]). - CLI: per-run bundle isolation prevents concurrent-run corruption (PR #408, commit
7f864f1). Two concurrentingest --helpruns raced esbuild's in-place bundle write (14 of 60 failed withSyntaxError: Unexpected end of inputonmain2a42241). Fix: per-process bundle name +process.kill(pid, 0)liveness sweep at startup + post-importrm(Node keeps loaded module + inline sourcemap). 20 of 20 clean after fix. No config / API / env-var change. - Dedup-phase in-scan concurrency halving was inert (CR-1, post-v1.26.0 code review).
consecutiveThrottleChunks+HALVE_AFTER_CONSECUTIVE_CHUNKSwere declared inside the chunk-iteration for-loop body insrc/wiki/lint/llm-phases/dedup-phase.ts, so the counter reset to 0 at every chunk and could never reach the halving threshold of 2. Result: the v1.26.0 Batch 2 attribution "979s→365s e2e gain on the 2141-page vault came from force-disable + halving + 500ms backoff" was wrong on the halving factor — only the retry/backoff mechanism delivered the gain; halving was dead code in practice. Fix: hoist the two declarations above the loop (alongsidecurrentConcurrency) so the counter accumulates across chunks. New regression-guard test insrc/__tests__/wiki/lint/llm-phases/dedup-phase.test.ts("runDedupPhase — in-scan concurrency halving (CR-1 regression guard)") with 4-pair caseVariant fixture +systemPrompt.length = 7000+to force chunkSize=1 +pageGenerationConcurrency = 2+ per-prompt mock (1st call → '', 2nd → valid JSON). Asserts the"temporarily reducing in-scan concurrency 2 → 1"warn line fires. Attribution correction: see [[feedback_dedup_phase_halving_dead_code]] + [[feedback_force_disable_thinking_openai_compat_noop]] for the full 979s→365s→151s chain. - Six reasoning-budget-sensitive
TOKENS_*caps raised to 3000 (Issue #403, PR #429). Short-JSON output call sites ({strategy, path},{keywords: []},{kind: "entity"}) were sized for non-reasoning models; on reasoning-capable models the deliberation is billed against the samemax_tokensbudget as the answer, so the cap was burned before content. DocTpoint measurement ongemma-4-12b / LM Studio / 2.4 KB source × 45 calls: 14 truncated, 13 of those empty;complementaryAppendwas 3/3 = 100% miss at its 600 cap. Bumped to 3000 uniformly:TOKENS_DEDUP_RESOLUTION1000 → 3000,TOKENS_MERGE_TRIAGE2000 → 3000,TOKENS_COMPLEMENTARY_APPEND600 → 3000 (the three #403 primary sites), plus three same-pattern sites surfaced by the post-#403 audit pass:TOKENS_LINT_ALIAS_BATCH500 → 3000,TOKENS_LINT_ORPHAN_FIX800 → 3000,TOKENS_QUERY_KEYWORDS1000 → 3000. ~50–80% reasoning headroom while keeping the cap well below the call's context window. Per-call reasoning-aware multiplier is deferred to v1.27.0's per-callthinkingPolicyenum (scope item 6). - CHANGELOG v1.26.0 entry:
thinking/chat_template_kwargsnever reached the wire correction (Issue #420, PR #420). Replaced the "SDK'sfilter()silently drops them" framing with the actual mechanism verified by DocTpoint: the SDK'sfilter()at@ai-sdk/openai-compatible@2.0.62/dist/index.mjs:531-540is a passthrough for undeclared keys (copies them verbatim), but it reads fromproviderOptions[<provider id>](e.g.lmstudio/deepseek) whilebuildProviderOptionsreturns them under the hardcodedopenaiCompatiblekey, which no shipped provider id matches — the fields were misaddressed, not filtered.reasoningEffort: 'none'is the only verified-working disable (it IS declared in the schema and emits asreasoning_effort: 'none'on the wire at:541). - CHANGELOG + ROADMAP: Bedrock Stage 2 (SSO/Profile auth) planning entry recorded (Issue #425, PR #426). Cancels the prior "≥3 user requests" gate. Implementation window v1.26.x PATCH / v1.27.0 via a zero-AWS-SDK path: hand-rolled IAM Identity Center OIDC (reusing the Codex OAuth skeleton) →
GetRoleCredentials→ temp IAM creds → hand-written SigV4 signer → existingbedrock-mantleendpoint. ~+10 KB bundle, zero new npm deps (vs the rejected PR #263's +1.2 MB). Issue #425 milestone: v1.27.0+ research. PR #263 author notified with the new decision (comment 5218259440). --seedis no longer documented as honoured by every local server (Issue #423, PR #434, commit8826710). Docs-only. Three sites promised strict seed honouring: the flag table intools/llm-wiki-cli/README.md, the--helptext intools/llm-wiki-cli/src/main.ts, and thesamplingSeeddoc comment insrc/types.ts. Measured on LM Studio /google/gemma-4-12b(MLX, 4bit) the field is accepted and type-validated (seed: "abc"answers HTTP 400 namingllm.prediction.seed) and then ignored: five requests atseed: 42,temperature: 1.0returned five distinct outputs, as did five requests with no seed at all — onlytemperature: 0returned a single output. Nothing in the exchange tells the caller the run is not reproducible, which is what made the sentence costly rather than merely optimistic. Theopenai/ Anthropic / Codex exceptions are accurate and stay verbatim.- A page keeps its own H1 through an LLM body rewrite (Issue #419, PR #422, commit
cddd460; hardened for Issue #435, PR #437, commit8eb3948).reassertH1restored the title withrewrite.replace(current, previous), wrong twice over: the replacement string processes$escapes, so a title the function exists to keep verbatim was mutated (# Kosten $$500 und $& im Titelcame back as# Kosten $500 und # Kosten im Titel), andreplacesubstitutes the first occurrence anywhere in the body rather than the matched line, so a preceding line quoting the title took the restore while the real H1 kept the model's version. Both are repaired by splicing atexec().index. #435 then removed the remaining assumption behind that match — H1 re-assertion hardened against frontmatter and code-fence comment lines:findH1walks the lines once and skips a---block in frontmatter position (further down it is a thematic break) and any fenced block, closed by its own opening marker. The read side is the mass-mutation case the file-name approach was rejected for in #419 — a#shell comment inside a bash example could be adopted as the page's previous title and mint a title for a page that never had one. The same repair applies tomergeDuplicatePages, which adoptsparsed.bodyas the merged body. 2 + 6 tests; the thematic-break test pins the boundary rather than fixing it. yamldeclared indevDependencies(Issue #424, PR #431, commit64601ee). Clean-install regression:yaml@2.8.3was only transitive via vite peer in vitest → eslint-plugin-obsidianmd → yaml-eslint-parser, so pnpm's strict isolation kept it nested undervite/node_modules/yaml. Theappend-source-slug.test.tsimport (import { parse as parseYaml } from 'yaml') failed to collect on a freshpnpm installfrommain. Two things had hidden the regression:release.ymlusesnpm install --legacy-peer-deps(flat layout hoists), and the release workflow runsnpm run buildonly — no test step, so a file that fails to collect never turns CI red. Fix: declare^2.4.2indevDependencies; lockfile regenerated perpre-release-gate§2f.2.- Query wiki: silent-success defect on Save (Issue #398, PR #432, commit
e2071af). User clicks "Save to Wiki" on a conversation; the notice said "Conversation saved to Wiki!" but no file was written. Three notices flashed: "Checking for existing knowledge" → "Konversation im Wiki gespeichert!" → "0 entities, 0 concepts, 0 pages" (or "0实体, 0概念, 0页" in zh-CN locale). Root cause:src/wiki/conversation-ingest.ts:78-93returns early withsuccess: true,createdPages: [],entitiesCreated: 0,conceptsCreated: 0,errorMessage: 'Knowledge already exists in Wiki'whencheckDedupreturnsstatus: 'fully_redundant'— butQueryView.saveToWikidisplayed an unconditional "saved!" notice without surfacingreport.errorMessage. Silent-success defect (UI lied about what actually happened). Two-layer fix: (a) UX layer —noticeTailconditional append whenreport.errorMessageis set; i18n keyquerySaveAlreadyExistsadded to all 10 locales. (b) Diagnostic layer —console.debugfor the dedup verdict +console.warnwhen save is skipped, so the user can inspect DevTools to see the LLM's actual verdict. 3 regression tests pin the silent-success contract.
Issue tracker had drifted from the v1.26.0 merge history (no Closes #N in PRs #401 / #406 / #410 / #411 commit messages, so auto-close-on-merge never fired). Closed administratively on 2026-08-07:
- #382 [v1.26.0 hardening] — all 5 P0+P1 batches shipped in v1.26.0 via PRs #401 / #406 / #410 / #411.
- #328 [schema layer rethink] — Phase 1 closed by PR #331 (2026-07-22). Phase 2/3 deferred to v1.27.0+.
- #402 [providerOptions stripped] —
response_formatclosed byca4a24d(2026-07-29);repetitionPenaltysplit to #414. - #399 — see Fixed section above.
-
24 Dependabot alerts closed via transitive devDep upgrade (Dependabot batch 2026-08-08). All 24 alerts were in transitive devDependencies only — production runtime (
@ai-sdk/*,openai,anthropic, etc.) was untouched. Bumped 4 root devDeps so the 4 vulnerable transitives resolve to safe versions:fast-uri3.1.4→3.1.5(added as direct devDep so pnpm hoists it; override updated) — closes 3 alerts (#1, #30, #31)undici7.27.2→8.10.0viajsdom@^30.0.1— closes 16 alerts (#5-#11, #18-#27)postcss8.5.15→8.5.26viavite@^8.2.1(vitest peer re-resolved) — closes 3 alerts (#12, #34, #35)vite8.0.13→8.2.1via direct devDep — closes 2 alerts (#3, #4)- Plus
ajv@^8.20.0added to devDeps (pulls the safefast-uri) - Plus
eslint-plugin-obsidianmd@^0.4.1+vitest@^4.1.10range-widened to latest
All 24 alerts now have
first_patched_version ≤ installed versionper Dependabot's metadata; GitHub auto-closes on next lockfile re-scan post-merge. Zero runtime impact (fast-uri/undici/postcss/vitenever appear inmain.js— verified bygrep -cagainst the built bundle, 0 hits each). Gate 1 green on the new lockfile: lint 0/0, tsc 0, 2980 tests passing (217 files), build clean, css-lint 0 violations.Known CI-only carry-over: npm-audit (registry-local advisory DB) flags an additional
brace-expansion@1.1.16/2.1.2transitive reachability througheslint-plugin-import/eslint-plugin-n/eslint-plugin-react/eslint-plugin-json-schema-validator. GitHub Dependabot does NOT flag these (no open alert forbrace-expansion). The npmoverridesfield syntax to cascade (eslint-plugin-import > brace-expansion: 5.0.9) is incompatible with pnpm'sparseCatalogProtocol(bareSpecifier.startsWith is not a function) — flat overrides work in both, but flat overrides do not cascade into grand-children transitive deps on npm's side. pnpm's hoisting deduplicates everything to a single5.0.9via flat override. Resolving the npm-side carry-over requires either (a) migrating CI to pnpm, (b) using pnpm-only overrides viapnpm.overrides(subtly different field), or (c) replacingeslint-plugin-import/eslint-plugin-n/eslint-plugin-reactwith non-vulnerable alternatives. Out of scope for v1.26.1 — the runtime bundle is unaffected, Dependabot considers it resolved.
- #407 —
parseJsonResponseparse failures indistinguishable from negative answers at 7-12 sites (high-blast:path-resolution.ts:220+conversation-ingest.ts:337). Stage 0 shipped as PR #436 (see Added above); the call sites still read a failure as a negative answer until Stages 1+2 port them, one PR per site. - #414 —
repetitionPenaltysetting inert (split from #402). DocTpoint's per-backend measurement 2026-08-07 on LM Studio / gemma-4-12b confirmed:repetition_penaltyis silently discarded on this backend; the correct spelling isrepeat_penalty(llama.cpp style) for LM Studio / llama.cpp andrepetition_penaltyfor vLLM / OpenRouter. Path = per-backend spelling transform. Gap: DeepSeek / Kimi / GLM / Ollama / vLLM unmeasured.
Scope. Adds AWS SSO / Profile login to the existing bedrock-anthropic / bedrock-openai providers via a zero-AWS-SDK path (cancels the prior "≥3 user requests" gate). Mechanism: hand-rolled IAM Identity Center OIDC device-code flow (reusing the Codex OAuth skeleton at src/llm-sdk/openai-codex/) → GetRoleCredentials → temp IAM creds → hand-written SigV4 signer → existing bedrock-mantle endpoint (bedrockMantleMessagesUrl / bedrockMantleChatCompletionsUrl). ~+10 KB bundle, zero new npm deps.
Why this replaces the rejected PR #263 approach. #263 shipped @ai-sdk/amazon-bedrock + @aws-sdk/credential-providers for the same feature at +1.2 MB (bearer users pay it too — esbuild single-file CJS cannot lazy-load). The bedrock-mantle endpoint accepts AWS credentials (SigV4) per AWS docs and speaks standard OpenAI/Anthropic protocols over plain SSE — so SSO needs only the OIDC login flow + a hand-writable SigV4 signer (~300 LOC, crypto.subtle, AWS test vectors). No AWS SDK required.
Design record: ~/.claude/.../memory/project_bedrock_stage2_codex_style_sigv4.md (implementation checklist). Target window: v1.26.x PATCH or v1.27.0.
MINOR. Anchored at #358 (complementary memory model). User-visible surface from this release: the headless ingest CLI, #383 boundary follow-up, dual-key bucketed dedup, cross-type dedup candidate expansion, dedup threshold advanced tunables, real wire-level force-disable thinking (4-layer fallback), parse-failure routing into dedupFailures, dead-code-as-docs governance, Russian i18n, and three DocTpoint PRs (#357 source-lemma, #386 vault-wide link retarget, #388 created: provenance). The complementary-memory design items (per-type registration, typed edges, bidirectional frontmatter, identity ambiguity, Preview-Confirm, stable mutation interface) are scoped but not implemented in this release — they remain v1.26.x follow-on work and are tracked in docs/v1.26.0-design.md plus the issues listed there.
Composition. 115 commits on top of v1.25.11, 110 files changed, +10,604 / −994 LOC, 2928 tests / 213 files passing. The Batches 1-4 P0+P1 hardening was added on top of the originally-shipped CLI surface; v1.26.0 is the first MINOR in the project that carries P0+P1 hardening into the same tag.
- Headless ingest CLI is now discoverable (PR #372 + #387). The engine under
tools/llm-wiki-cli/previously shipped with nobin, no pnpm script, and no mention in any README — a fresh clone could not find it. Exposes it asllm-wiki(bin) pluspnpm llm-wiki(script) and sets the executable bit onrun-llm-wiki.mjssonpm installproducesnode_modules/.bin/llm-wiki. The tool is namedllm-wikirather thanwiki-ingestso it can grow beyond ingest into a general wiki-management CLI (lint, query, mutation) without a later rename. Note: pnpm 10 does not link the root package's bin tonode_modules/.bin/, so the pnpm-user entry point ispnpm llm-wiki(notpnpm exec llm-wiki); npm users see./node_modules/.bin/llm-wiki. - 🛠️ Tools H2 section in all 10 READMEs. One paragraph pointing at the CLI's flag reference, environment requirements, and shim caveats — links out to
tools/llm-wiki-cli/README.md(absolute GitHub URL so the readme-links test stays green). - Source-slug deterministic merge (PR #357, DocTpoint). Replaces the LLM-only merge judgement with a deterministic "source-slug = page-lemma" path so a re-ingest of the source that produced a page cannot fail to merge that page into its own subject on the second pass.
- Real wire-level force-disable thinking, 4-layer fallback (PR #411, Batch 6). Prior versions shipped
thinking.type = 'disabled'as a wire disable, but they never reached the request body. The AI SDK'sopenaiCompatibleLanguageModelChatOptionszod schema (@ai-sdk/openai-compatible@2.0.62/dist/index.mjs:322-344) does not declarethinking/chat_template_kwargs— on its own that would not have stopped them, because thefilter()at:531-540is the passthrough for undeclared keys: it copies them into the body verbatim and skips only the keys the schema already handles. It reads them fromproviderOptions[<provider id>](lmstudio/deepseek/ …), whilebuildProviderOptionsreturns them under the hardcodedopenaiCompatiblekey, which no shipped provider id matches — so the fields were misaddressed, not filtered. Layer 1:reasoningEffort: 'none'is declared in the schema and the SDK emits it asreasoning_effort: 'none'on the wire (:541) — the only verified-working disable. Layer 2: co-emitthinking: { type: 'disabled' }+chat_template_kwargs.enable_thinking = falsefor the Anthropic path (uses a different field that the Anthropic SDK accepts). Layer 3: on HTTP 400 mentioningreasoning_effort/thinking/chat_template, retry once withreasoningEffortstripped. Per-baseURL cache prevents infinite loops. Layer 4: "Do not reason step by step" line in the dedup prompt. User-facing impact on a 2141-page vault: wall-time 979s → 365s (Batch 2 retry only) → 151s after Layers 1-3 went live (−85% vs baseline, −59% vs Batch 2). Per-callthinkingPolicy(the JSON-repair path atsource-analyzer.ts:417always allows reasoning — DocTpoint measurement showed disabling it produces structurally valid JSON with wrong content) is deferred to v1.26.x PATCH. - Dual-key bucketed dedup, Batch 1 rev 2 (PR #401).
partitionPagesMultiBucketpartitions pages intotp-prefix(title-prefix) +lh-link-hash(link-hash) buckets before the O(n²)generateDuplicateCandidatesscan. Bucket boundary emitscheckCancelled()so the user can interrupt mid-scan. Pure refactor on legacy vaults (≥95% recall on synthetic N=200 pages vs 80-90% on the previous single-bucket baseline); memory peak O(N² candidates) → O(B² per bucket). - Cross-type dedup candidate expansion, Batch 2 (PR #410).
generateDuplicateCandidatesnow surfaces candidates across entity / concept / file types when the shared-link signal crosses type boundaries, and the dedup phase ships an inline empty-response retry + backoff + concurrency halving on transient burst load (LLMs that return 200 + 0-byte body under burst). User-facing impact: 2141-page vault dedup 979s → 365s after retry/backoff live. Threshold inputs (see Changed) are user-tunable. The companionlintDedupIncludeSourcestoggle (per-source-file dedup scope filter) relocates to the bottom Advanced settings panel — it was originally misplaced in the LLM Advanced section. - Russian i18n, full UI + wiki-output + README (PR #397). 667 new keys in
src/texts/ru.ts; fulldocs/README_RU.mdtranslation; 11-way language switcher across all READMEs; system-prompt section labels for wiki output. 10→11 locales. i18n-parity test enforces bidirectional coverage. - Vault-wide link retarget for
mergeDuplicates(#386, PR #392, DocTpoint).merge-duplicates.tspreviously retargeted links only inside the surviving page's body; links from sibling pages pointing at either the surviving page or the deleted page now also get rewritten vault-wide and across every alias form before the delete commits. Closes #386. - Frontmatter
created:provenance (#388, PR #396, DocTpoint).create-page.tsandfill-empty-page.tsnow takecreated:from the caller (new Date().toISOString()); never read from the LLM-generated content. Closes #388 — the previous shape could echo an LLM-hallucinated old date into freshly-created pages. - Dead-code-as-docs governance (Batch 4, policy only — no functional change). CLAUDE.md §"Dead-code-as-docs policy" +
pre-release-gatePhase 2g audit. Exported helpers with zero production importers now have a one-release half-life: either wire into production before the next MINOR or delete before the next MINOR. Two prior instances on record (v1.25.10 PATCHlint-analysis-cache.ts+lint-smart-skip.tsshipped as dead code and survived until v1.26.0 Batch 3 deletion; v1.25.0 PDF cache-only helpers followed the same pattern). Two is a pattern; three would be a culture. P1-1/P1-2 from v1.25.10 PATCH #367 deleted as part of this governance (PR #406).
--thinking on|off→--thinking-mode data-json | plugin-off | server-default. The old flag was a two-state surface that hid the three outcomes the plugin can actually produce: leavedata.jsonalone, force-disable reasoning, or defer to the server's preset.onlooked like "enable reasoning" but actually meant "defer to server default" — a footgun. The new flag makes all three states explicit. The legacy--thinkingthrows a deprecation error pointing at the new flag and the v1.27.0 removal target; it does NOT silently translate, so a--thinking ontypo can't keep working long after the deprecation is forgotten.--max-rounds→--round-base. The old name was actively misleading: it set the granularity'smaxBatchesBasefield, not a ceiling. The actual ceiling ismin(base * 3, ceil(source_chars / 2000) + 2), so--round-base 6allows up to 18 rounds, and on a short source the length term wins regardless. Renaming to--round-basedescribes what the flag actually sets. The internalGRANULARITY_CONFIG.maxBatchesBasefield is unchanged (engine contract, not CLI surface). Legacy--max-roundsthrows a deprecation error.- Picker exclusion rule centralised (PR #389 follow-up to #383).
FileSuggestModal,FolderSuggestModal, andMultiFileSuggestModalpreviously each open-coded the wiki + configDir filter. They now share a singleisExcludedFromSourcePickerprimitive insrc/core/folder-scope.ts. The originalFolderSuggestModalalso held the same unanchored-prefix leak classfolder-scope.tswas created to eliminate (.obsidian-backup/next to.obsidian/) — closed as part of the centralisation. - Dedup threshold constants extracted + user-tunable (PR #395, Batch 2 surface area).
LINT_DEDUP_JACCARD_LINK_THRESHOLD,LINT_DEDUP_JACCARD_BODY_GATE,LINT_DEDUP_BIGRAM_THRESHOLDextracted fromsrc/wiki/lint/duplicate-detection.tstosrc/constants.ts(Lint Performance Knobs block).generateDuplicateCandidatesnow accepts aDuplicateDetectionThresholdsoptions-object; defaults preserve legacy behavior. Three user-settable inputs in Settings → LLM Configuration → Advanced → Custom ("Duplicate detection thresholds" subsection): shared-link duplicate threshold, body-similarity floor, title/alias similarity threshold. Leave blank = constant default. Tier-1 cutoff (LINT_DEDUP_BIGRAM_TIER1_CUTOFF = 0.6) is NOT user-settable — controls LLM budget allocation; user-tunable would let users silently flood or drop LLM candidates. New bottom "Advanced settings" panel (separate from the LLM Advanced section) hosts the three threshold inputs +lintDedupIncludeSources.
- FolderSuggestModal leaked the wiki folder itself as a pickable source/watched folder (#383 PR #384 follow-up, PR #389).
isInFolderScope(folder, wikiFolder, false)is false for the folder itself (a folder is not a descendant of itself); without an explicit identity check, the wiki folder re-entered the folder picker after PR #384 landed. NewisAtOrInFolderScopeprimitive insrc/core/folder-scope.tsmakes the "folder itself OR anything inside it" semantics a single tested rule. - Three of PR #384's per-site regression tests pinned nothing (#383 PR #384 follow-up, PR #389). The
delete-empty-stubs,merge-duplicates(deleted — see below), andauto-maintaintests rebuilt the filter expression inside the test file and asserted against the copy — reverting the source line left them green. Rewritten as real production-function tests:deleteEmptyStubs(5 cases: leak direction + substantive content + reviewed:true + protected paths + deleteFile throw),normalizeSourcesInFolder(3 cases: leak direction + clean file + read throw). Themerge-duplicatessite is intentionally NOT covered by this release — #386 (assigned to DocTpoint) replaces that filter and owns its own coverage. auto-maintain.tsPhase 2 extracted to a module function (normalizeSourcesInFolderinsrc/core/sources-normalizer.ts). Previously inline inrunStartupCheck, which sleeps 3 seconds and depends on the wikiEngine/plugin surfaces — neither is testable from the startup surface. The new module function mirrors the Phase 3 shape (findIncompletePages) so every startup phase follows the same module-function pattern.- Dedup-phase parse-failures were silently merged with legitimate-empty results (Batch 7, PR #411).
dedup-phase.tscollapsednull(parse-fail / truncated) and{"duplicates": []}(LLM said "no duplicates") into the same[]outcome; neither was routed todedupFailures. Fix: structuredtype: 'parse-failure'discriminator on eachdedupFailuresentry;isRateLimitFailurepredicate now accepts a structured item and bails early ontype: 'parse-failure'. BothdetectRateLimitFailuresand the dedup-phase consumer now pass the full item (CR-3 wiring fix — the structured branch was previously unreachable in production because consumers passedf.reasonstring only; a regression test inrate-limit.test.tspins the discriminator throughdetectRateLimitFailures). - Two-marker (verb + field) classifier for reasoning-field 400 errors (Batch 6 CR-2). Prior single-substring classifier included the bare word
thinking, which collides with model names (kimi-k2-thinking,qwen3-235b-a22b-thinking-2507,glm-4.6-thinking). Any 400 on these models — bad model name, context-length exceeded, max_tokens mismatch — was misclassified as a reasoning-field rejection, permanently marked the baseURL as "strip" (silently disabling force-disable-thinking for the rest of the session), AND consumed the 400 so the token-key fallback never fired. Two-marker classifier (rejection verb + field marker) rejects all four false positives while still catching real rejections.
- Lint dedup-phase empty-response retry (Batch 2). Inline retry on
nullLLM response with 500ms attempt-2 backoff; concurrency-halving on consecutive throttled chunks. Deferred: the concurrency-halving counter (HALVE_AFTER_CONSECUTIVE_CHUNKS = 2) was scoped inside the per-batchforloop body and never reached the threshold — see v1.26.x PATCH CR-1 for the location-only fix. The 979s→365s e2e on the 2141-page vault came from retry/backoff only; the (dormant) halving contributed zero. The 151s additional gain over Batch 2 came from Layers 1-3 of the Batch 6 fallback going live (see Added). Map<string, true>→Set<string>onReasoningStripProber.cache(PR #411 simplify). Removed the dead=== truecheck on every read and the deadinvalidate(baseUrl?)overload (zero production callers — only tests used it).
- 2928 tests / 213 files passing. +13 net since v1.25.11:
- +7
__tests__/tools/llm-wiki-cli/main.test.ts(parseCliOptions base contract + boundary-catch USAGE contract, dispatchCli shapes,--thinking-modeenum + legacy deprecation + ambiguity,--round-base+ legacy deprecation, numeric validation (safe-integer, empty-value rejection,=-form negatives), boolean plumbing incl.--extract-only⇒--dry-run, applyOverrides (prototype-key guard, single-field patches), resolveApiKey OS guidance, applyThinkingMode, parseNumber) - +3
__tests__/root/constants.test.ts(new threshold constants) - +11
__tests__/root/i18n-parity.test.ts(Russian locale wiring + 11-way README switcher) - +11
__tests__/wiki/lint/duplicate-detection.test.ts(threshold override tests) - +3
__tests__/wiki/lint/llm-phases/dedup-phase.test.ts(classifyTiers threshold tests) - +3
__tests__/llm-sdk/reasoning-strip-probe.test.ts(two-marker classifier + Set conversion) - +1
__tests__/llm-sdk/openai-compat-request-body.test.ts(wire-body regression:reasoning_effort: 'none'IS on the body, not just on theproviderOptionsargument handed to the SDK) - +3
__tests__/core/rate-limit.test.ts(CR-3 wiring + structured-form + discriminator) - +4
__tests__/types/settings.test.ts(new threshold settings defaults) - +1
__tests__/wiki/lint/fill-empty-page-created.test.ts(#388 regression guard:created:from caller, not content) - +1
__tests__/wiki/lint/merge-duplicates-link-retarget.test.ts(#386 regression guard) - +2
__tests__/core/folder-scope.test.ts(PR #389 new primitivesisAtOrInFolderScope+isExcludedFromSourcePicker) - +1
__tests__/core/source-lemma.test.ts(PR #357 source-slug deterministic merge) - +2
__tests__/wiki/source-analyzer-thinking.test.ts(Batch 6 per-call thinkingPolicy regression guard: repair callback does NOT passenableThinking: false) - +1
__tests__/wiki/source-analyzer-lemma-guarantee.test.ts(PR #357 invariant) - −2 net deletions (removed 2 stale
lint-analysis-cache.test.ts+lint-smart-skip.test.tsfrom PR #406 dead-code cleanup; replaced 1 staleduplicate-detection.test.tsshadow file from the lint sub-package)
- +7
- Subcommand dispatch via
dispatchCli(argv)returning a tagged union{ kind: 'tool-help' | 'ingest' | 'unknown' }. Adding a futurelintorquerysubcommand is onecaseand the compiler will refuse to forget it. Flag-shaped first arguments (e.g. forgettingingest) surface a hint rather than getting swallowed. - Numeric validation collapsed into a single
parseNumber(raw, flag, predicate)helper driven by a spec table inparseCliOptions(), so errors surface from the parser (with the ingest USAGE block attached) instead of mid-run. A single boundary catch appendsINGEST_USAGEto every escaping error, detecting by a stable marker substring (the exactrun-llm-wiki.mjs ingestpath) rather than matching on message text. --helpis now a pure marker (options.help: boolean) instead of callingprocess.exit(0)inside the parser, soparseCliOptionsis testable. TherunIngestrunner handles the actual printing.- Code-review hardening (8 angles, subagents, both CLI and #383 follow-up): prototype-key guard on the settings-derived granularity path in
applyOverrides, empty/whitespace numeric rejection (Number('')coerced to 0 —--max-tokens-per-call ""silently meant "no cap"),--modeltrim,Number.isSafeIntegerfor integer flags,-halias at the ingest subcommand,--vaultENOENT → friendly message, deprecation throws before required-flag checks, folder-scope centralisation closing the same unanchored-prefix leak class on the configDir half. All backward-compatible. - Root
tsconfig.jsongainsexclude: ["src/__tests__/tools/**"]because the CLI test files importtools/source via relative paths and root tsc would otherwise follow those imports into@types/node@16territory (parseArgswas added in Node 18.3+). vitest still finds them via its own include glob. src/core/folder-scope.tsadds two new primitives alongsideisInFolderScope:isAtOrInFolderScope(path, folder, isRoot)(true for the folder itself OR any descendant — fixes the #383 picker leak in one rule) andisExcludedFromSourcePicker(path, wikiFolder, configDir)(the centralised picker rule used by all three pickers).src/core/source-lemma.tsexposesisSourceOwnPageLemma+selectSourceLemma(PR #357 source-slug = page-lemma deterministic merge);src/core/sources-normalizer.tsexposesnormalizeSourcesInFolder(PR #389 module-function pattern).src/llm-sdk/reasoning-strip-probe.ts(Batch 6, PR #411) —ReasoningStripProberper-baseURL cache +isReasoningFieldErrortwo-marker classifier (mirrorsisPdfRelatedLlmErrordesign insrc/wiki/wiki-engine.ts:587-608).src/wiki/lint/duplicate-detection.tsaddsDuplicateDetectionThresholdsoptions-object + per-calltier1Cutoffparameter onclassifyTiers(PR #395 + PR #410 thread-through).src/ui/settings-sections/advanced-settings-section.tsis the new bottom Advanced settings panel (PR #395); hosts the 3 dedup threshold inputs +lintDedupIncludeSources. Separate from the LLM Advanced section (which retainstemperature,repetitionPenalty,forcePdfSupport).src/ui/settings-sections/shared-inputs.tsaddsrenderNumberInput(consolidated from priorrenderNumericInput+renderDedupThresholdInput); regression test insettings-section-helpers.test.ts.src/constants.tsgains 5 new constants:LINT_DEDUP_BUCKET_COUNT,LINT_DEDUP_BUCKET_PREFIX_LEN,LINT_DEDUP_JACCARD_LINK_THRESHOLD,LINT_DEDUP_JACCARD_BODY_GATE,LINT_DEDUP_BIGRAM_THRESHOLD.LINT_DEDUP_BIGRAM_TIER1_CUTOFF(Batch 2) — constant-only, intentionally NOT exposed to Settings.package.jsonbinfield exposesllm-wiki;scripts.llm-wikimirrorsnode tools/llm-wiki-cli/run-llm-wiki.mjs.tools/llm-wiki-cli/tsconfig.json(PR #372) — separate tsconfig for the CLI with@types/node@22; root tsconfig excludessrc/__tests__/tools/**to keep theparseArgsimport belownode_modules/@types/node@16from poisoning root tsc.versions.jsongains1.26.0: 1.11.4entry.
Super-aggregated per Keep a Changelog spec + CLAUDE.md "ancient versions are pre-aggregated". 11 PATCH releases (v1.25.0 + 1.25.1 → 1.25.11) over 14 days. Per-PR detail preserved in git log --oneline 1.25.0..1.25.12 and memory files (project_v1.25.x_release.md series).
- v1.25.0 (2026-07-18) — MINOR — PDF Ingest Level 1 (content-hash cache + bounded growth + provider gate + Force-PDF-Support escape hatch + local OCR path on Apple Silicon + AbortSignal-cancellable). 2182 tests / 165 files.
- v1.25.1 → v1.25.11 — 10 PATCHes covering high-ROI bug-fix clusters:
- Frontmatter data-loss class: #312 part 2 (merge-triage own-source skip override), #356 (unknown-field strip on re-touch), #363 (Mentions
[[|]]empty-citation truncation), #365 (freshly-generated pages losingsources:field). - Lint pipeline hardening: #367 P0-1 (fix-runners batched by
pageGenerationConcurrency), P1-1/P1-2 helpers (LintAnalysisCache,lint-smart-skip— shipped dead per dead-code-as-docs policy; wire-up deferred to v1.26.0). - Path-resolution safety: #364 (folder-scope prefix leak — sibling files sharing name prefix matched), #446 follow-up's precursor (alias latch removal).
- Slug / tag handling: #366 phase 1 (Turkish-aware case fold on slug comparison keys), #368 (custom tag vocabulary documented as LLM hint not enforcement gate),
MIN_ALIAS_LENGTHlowered 3 → 2 chars. - README + i18n + status-bar i18n: #169 (fine-grained pipeline stage hints in status bar), #375 (relative cross-file README links broken in Obsidian marketplace — switched to absolute GitHub URLs across all 10 locales), 30 dead
lintStatus*i18n keys removed, EN banner upgrade (Obsidian Review Perfect Score + Local-first privacy), comparison table dedup 12 → 8 rows, MinerU online conversion added as first item in Ecosystem section of all 10 READMEs. - Per-step LLM accounting (Issue #99): #339 follow-up — SecretStorage migration 2-phase wipe (text survives until IO succeeds) + task-label audit on 5 call sites →
core/llm-task-usage.tsaccumulator.
- Frontmatter data-loss class: #312 part 2 (merge-triage own-source skip override), #356 (unknown-field strip on re-touch), #363 (Mentions
- 11 releases
- Test count: 2182 → 2713 / 165 → 202 files (+531 tests across the series)
- Composition + per-PR detail:
git log --oneline 1.25.0..1.25.12+ memory files (project_v1.25.x_release.mdseries)
Theme: Cache-only PDF Ingest (Level 1) with provider gate + content-hash cache + bounded growth; prompt centralization for the PDF transcriber; status-bar cancellation via Vercel AI SDK v6 AbortSignal; local model guidance with Apple Silicon OCR path (oMLX + Markitdown + Baidu Unlimited-OCR). 2182 tests passing (165 files). Recommended upgrade for everyone on v1.24.x.
- PDF Ingest (Level 1). Pick a PDF from your vault — the plugin reads it through your LLM provider's native file input (anthropic / openai / bedrock-anthropic / bedrock-openai natively; any other OpenAI/Anthropic-compatible endpoint via Force PDF Support in Settings → LLM Configuration → Advanced), converts it to Markdown via an OCR-style verbatim transcriber prompt with
[illegible]/[figure: ...]/[equation: ...]anti-hallucination markers, and re-enters the regular Markdown ingest pipeline. Every existing entity / concept / alias /[[wiki-link]]workflow applies unchanged. The result is content-hash cached in.obsidian/plugins/karpathywiki/pdf-cache/; the cache key embedsconverterVersionso prompt upgrades invalidate stale entries automatically. - Bounded cache growth. Three-defense-layer cache housekeeping: single-entry cap (10 MB) pre-write, LRU-by-mtime eviction (100 MB total / 1000 entries) post-write, and
prepareBatchIngest()(TTL purge + size enforce) wired intorunBatchIngest()viapreparePdfCacheForBatchIngest(). Cache only by default — your vault is not modified. - Optional vault sidecar. Settings → Wiki Configuration → Wiki Folder → Write PDF Markdown to Vault writes a
<basename>.pdf.mdsidecar next to the source PDF after conversion. Off by default (cache-only). This is the only user-visible opt-in that touches the vault. - Universal Force PDF Support escape hatch. Any non-native provider (custom, anthropic-compatible, ollama, lmstudio, deepseek, kimi, glm, etc.) can attempt PDF conversion when the toggle is on. The endpoint decides; failures surface as a localized
sourceRejectedPdfUnsupportedNotice guiding the user to disable the toggle or switch provider. The trust boundary is the user — your endpoint either accepts PDF or it doesn't; the toggle tells us to ask it. Switching the provider to a NATIVE one (anthropic / openai / bedrock-*) auto-resets the toggle tofalse. - Local PDF OCR path on Apple Silicon. Documented end-to-end recommended setup for fully-local PDF ingestion: oMLX + Markitdown backend + Baidu Unlimited-OCR (open-sourced 2026-06-22, 3B total / 0.5B active, end-to-end OCR that solves the "slower the longer it generates" failure mode of older OCR models on long documents). Provider: Custom OpenAI-Compatible pointing at oMLX's local server with Force PDF Support on. PDF never leaves the machine.
- Cancellable PDF ingest. Clicking the status bar mid-conversion aborts the in-flight LLM call through Vercel AI SDK v6 AbortSignal in ~200 ms. Both
.catchhandlers (selectSourceToIngestandingestActiveFile) now calldismissProgress()so the persistent "Ingesting: " Notice clears on throw. - Local model recommendations. Dedicated
### 🦙 Local Model Recommendations (Ollama / LM Studio)H3 in the Model Selection Guide, covering Qwen3.5 (27B / 35B-A3B / 122B-A10B), Qwen3.6 (27B with 256K+ context / 35B-A3B), Gemma 4 (E2B / E4B / 26B-A4B / 31B), with parameter-vs-quality tradeoff guidance, MLX-vs-GGUF quantization notes, and a context-strategy block. All 10 locales. - New Cloud Model Picks H3 in the Model Selection Guide, separating the cloud-vs-local sections explicitly. All 10 locales.
- PDF transcriber prompt centralized.
src/wiki/prompts/pdf.tshousesPDF_CONVERSION_SYSTEM_PROMPT(rewritten as OCR-style verbatim transcriber) plusunwrapFencedMarkdown()cleanup helper (strips```markdown/```/<output>wrappers that small/local models still produce despite instructions). Re-exported via the existingsrc/prompts.tsbarrel — PDF was the last LLM-call site to be folded into the project's prompt barrel. - PDF error classifier (
isPdfRelatedLlmError). Routes obvious PDF-rejection errors (rejection verb + PDF/media marker) to a localizedsourceRejectedPdfUnsupportedNotice. Tightened after the initial implementation: requires BOTH a rejection verb (reject/not support/unsupported/invalid/not allowed) AND a PDF/media marker (pdf/application/pdf/file part/mediatype). Pre-fix classifier substring-matched on'pdf'alone, causing transient 413 size-limit errors and Rust-serde "unknown variantfile" schema rejects (nopdfkeyword) to be misreported. - Three-defense-layer cache filename safety. Physical filename on disk is
sha256(logicalKey).slice(0, 16)(Git short-hash style); the logical key retainssha256:model:converterVersionsemantics; the converter hashes via newhashCacheKey()helper beforecache.get/set. Fixes WindowsERROR_INVALID_NAME+ POSIX unintended subpath when model contains/or:. - PDF cache directory auto-creation.
PdfConversionCache.ensureCacheDir()walks path segments beforemkdir. Obsidian's adapter does NOT auto-create parent directories, which left cache writes silently failing in fresh vaults.
- Default behavior preserved. No breaking changes since v1.0.0. Old
data.jsonwithout the new settings fields defaults tofalse, preserving cache-only behavior. The previously-planned sidecar-by-default approach was withdrawn in favor of cache-only before v1.25.0 ships (architecture pivot documented inproject_v1.25.0_pdf_cache_only). - PDF dispatch lives in
wiki-engine.ts. The separatepdf-ingest-orchestrator.tsfile was deleted;ingestPdfSourcenow feedsconvertPdfToMarkdownresult intoanalyzeSourceviaIngestOptions.contentOverride, reusing the existing Markdown ingest pipeline. - 5 dead i18n keys removed across all 10 locales (old "PDF orchestrator" + sidecar-default language).
LLMClient.createMessagegainedabortSignal?: AbortSignalas an optional parameter. Existing client implementations ignore unknown params (graceful degradation); the project ships a passing thread.
- ENOENT cache dir (Bug A). Obsidian adapter doesn't auto-create parent directories.
ensureCacheDir()walks segments before mkdir. - AI-SDK cause chain (Bug B). Vercel AI SDK v6 wraps provider rejections inside
error.cause.message. The pre-fixisPdfRelatedLlmErrorclassifier inspected onlyerror.messageand missed the rejection phrase.inspectCauseChain()walks the cause chain up to 4 levels with cycle protection; classifiers consult both layers. Now also extended to detect Rust-serde schema rejects ("unknown variantfile, expectedtext") which lack anypdfkeyword. - Stuck "Ingesting: " Notice (Bug H). When an interactive single-file ingest threw (network / vault IO / unexpected error), the persistent progress Notice stayed on screen until the next ingest. Both
.catchblocks (selectSourceToIngestline 645,ingestActiveFileline 671) now callthis.dismissProgress()after showing the error Notice. - Status bar didn't mirror Notice (Bug C). Clicking the status bar during PDF conversion didn't update text — fixed via double-callback pattern (Notice channel + text mirror in
onProgressclosure). - PDF mid-flow cancel ineffective (Bug D). Two-layered bug: setup block re-initialized on re-entry overwrote AbortController, AND
convertPdfToMarkdowndidn't thread AbortSignal to the LLM call. Fixed with idempotency guard inwiki-engine.ingestSource(if (this.abortController === null)) + abortSignal threading throughPdfConversionContext. - pdf-cache never written (Bug E). Same root cause as Bug A but in the cache write path.
ensureCacheDir()fix covers both directions. - Classifier false-positive guards (PR3 follow-up #3). 6 new tests pin the contract — 2 happy-path (route to skip) + 4 false-positive guards (413 / 5xx / null-deref / generic-invalid → re-throw).
- Markdown wrapper contamination in PDF output. Some local / small models (Qwen3.5-2B-MLX-4bit, Llama 3 8B Instruct, etc.) wrap their PDF-conversion response in
markdown ...fences despite the system prompt forbidding them.unwrapFencedMarkdown()heuristic cleaner strips BOM → outermost```markdown→ outermost```→<output>→ leading "Here is the converted Markdown:" preamble. Internalpython ...blocks survive (regex is single-fence, outermost-only).
- 2182 tests passing (165 files). +102 tests since v1.24.1.
- New tests cover:
- 30+ PDF ingest end-to-end tests (provider gate, cache hit/miss, settings defaults, sidecar create/update, forcePdfSupport toggle, classifier, cause chain walking, status bar, cancel-mid-PDF)
- 20 prompt invariant + unwrap helper tests (
src/__tests__/wiki/prompts/pdf.test.ts) - 6 PDF error classifier regression tests (happy-path + 413/5xx/null-deref/generic-invalid guards)
- 3 Bug D lifecycle tests (idempotency guard, AbortSignal propagation, dismiss on throw)
Super-aggregated per Keep a Changelog spec. v1.24.1 (single PATCH) over 4 days. Per-PR detail preserved in git log --oneline 1.24.0..1.24.1.
- 5-stage PPR seed-selection cascade (PR #281). Query Wiki now composes context through five complementary stages before generation: (1) lex fast path over entity/concept titles and aliases; (2) LLM keyword generation for synonyms, abbreviations, and token-overlap-resistant terms; (3) local substring scan of generated keywords across titles, aliases, and body snippets; (4) LLM KB fallback that re-seeds top-N candidates semantically when earlier stages are weak; (5) Personalized PageRank (Haveliwala 2002) over the
[[wiki-link]]graph starting from the seed set. The cascade auto-truncates at the stage that returns enough signal — no fixed 5-step cost, no LLM calls when lex suffices. Benchmark: PPR @5 = 27.1% vs pure knn baseline 24.1%, zero embedding opt-in. - Bedrock Stage 1 providers (PR #277/280). Added
bedrock-anthropicandbedrock-openaiprovider options routed through the AWSbedrock-mantle.<region>.api.awsendpoint. Region selector defaults tous-east-1. Zero new npm deps; bundle delta ~+3 KB. Stage 2/3 (bearer-only@ai-sdk/amazon-bedrock, SSO/profile) remain deferred pending demand. - Page-factory split (PR #276). Split
src/wiki/page-factory.ts(1252 LOC) into 10 focused modules (aliases.ts,complementary-appends.ts,contextualize.ts,create-page.ts,index.ts,mentions-integration.ts,merge-page.ts,merge-triage.ts,path-resolution.ts,related-page.ts) with 99 new dedicated unit-test files. - Consolidated the two "reviewed" protection mechanisms (#244 follow-up, PR #283). Removed the body-level HTML-comment marker (v1.24.0) that protected only a page's
## Mentions in Sourcesection. Protection is now driven solely by frontmatterreviewed: truevia the minimal-append path. - Non-lossy Mentions re-ingest (#267, PR #269/272).
assembleFinalContentpreviously dropped every earlier source's accumulated mentions (regression from #244). Merge now parses the existing page's mentions and unions them with the new source's (composite(quote, source_path)dedup key) before injecting; fail-safe preserves hand-edited section verbatim. - Empty-response quiet path (PR #282).
parseJsonResponsegainedsilentOnEmpty/throwOnEmptyoptions. Lint batch callers suppress noisy console errors for empty LLM bodies. Seed selector throwsEmptyResponseErroron empty body as defense-in-depth. - LM Studio no-key ingest (PR #269/272).
initializeLLMClient,llmReady,testLLMConnectiontreat LM Studio like Ollama for the API-key gate. - Settings unified↔per-task cascade (post-#281 e2e). Three edge cases where toggling Model Scope could leave
tempSettingsandsettingsout of sync — fixed. load-pages.mdsuffix defense (post-#281 e2e). Normalized path handling so wiki-page paths with or without.mdsuffix resolve consistently.- Streaming-chunk debug cleanup (post-#281 e2e). Removed stray
console.debuginopenai-compat-sdk-client.tsstreaming path. - Tier C welcome-note recreate bypass (PR #271).
recreateWelcomeNoteandensureWelcomeNoteacceptforceRecreate: true.
- 1 release
- Test count: 1825 → 2080 (+255 tests)
- Composition + per-PR detail:
git log --oneline 1.24.0..1.24.1
Theme: Per-task model routing, custom query instructions, four monolith splits, source-note aliases, frontmatter write repair. 1825 tests passing. Recommended upgrade for everyone on v1.23.x.
- Per-task Models (#208). Three independent settings (
ingestModel,lintModel,queryModel) on top of the existingmodel. Switch via Settings → Wiki → Model Scope dropdown: Unified (one model for all tasks) or Per-Task (independent choice per ingest / lint / query). Empty per-task field falls back tosettings.model, so existing v1.23.x data.json continues to work bit-identically. Newcore/model-resolver.ts(resolveModelForTask(settings, task)) is the single decision point used by all 28 LLM call sites;ui/settings-per-task-helpers.tsowns the UI-scope logic (mode resolution, displayed-model computation, preserve-on-toggle). Each picker uses a sentinel__custom__("Custom input…") — leaving the text input blank means "use unified model", matching the original picker behavior. - Test Connection multi-probe (#208). When
usePerTaskModels === true, the Test Connection button now probes each configured model sequentially (ingest → lint → query) with fail-fast — until every per-task model passes, the connection is considered unhealthy. Console logs include[testLLMConnection] probe plan: ingest=…, lint=…, query=…for verification. - Custom Query Instructions (#251,
jameses-cyber). Collapsible<details>panel inside the Query Wiki view, between the prompt and the history list. Appends user-supplied instructions to the system prompt at the three Query Wiki send sites (streaming, non-stream fallback, non-stream main). 5000-character defensive cap (centralisedCUSTOM_QUERY_INSTRUCTIONS_MAX_CHARS). Strictly scoped to Query Wiki chat — ingest, lint, page generation, save-to-wiki evaluation, duplicate merge, and seed selection are intentionally unaffected. Persisted ascustomQueryInstructions?: stringin data.json. Modes dropdown (Default / Research / Exact Facts / Commitments) + per-conversation override planned for v1.25.0+. Initial UI review used Settings → Query Wiki; shipped UI is the Query-local panel per user review. - First-query PPR warmup. Engine-level
_cachedGraph(WikiEngine.getOrBuildGraph(allPaths)) loaded once on first query, invalidates onwikiFolderchange orinvalidatePageCaches. First query now uses Personalized PageRank instead of falling back to lex-only on cold start.QueryView.invalidateGraph()delegates to the engine. fundingUrlin manifest. Adds"fundingUrl": "https://ko-fi.com/greenerdalii"tomanifest.jsonper Obsidian manifest spec. Optional field; Obsidian-side display depends on Community Plugin UI surfacing.
modals.ts1008-LOC split into directory (PR #257,4b65450).src/ui/modals.ts→src/ui/modals/with 7 focused files. External API unchanged (barrelindex.tsre-exports). Required after the v1.23.0 P2 modals feature set pushed the file past the 1000-LOC threshold.controller.tsrunLintWikigod function split into 3 phase modules (PR #248,ef44a58).src/wiki/lint/controller.ts:runLintWiki(was a monolithic 200+ LOC function) decomposed into Phases A/B/C (src/wiki/lint/llm-phases/analysis-phase.ts,src/wiki/lint/llm-phases/scoring-phase.ts,src/wiki/lint/llm-phases/synthesis-phase.ts). The orchestrator now delegates: analysis → scoring → synthesis.history-modal.ts1579-LOC single file split into directory (PR #249,fe273a4).src/ui/history-modal.ts→src/ui/history-modal/with 14 files (~250 LOC each max):types.ts,render-state.ts,HistoryModal-class.ts, 9 renderer modules undersrc/ui/history-modal/renderers/, and anindex.tsre-export shim. External API (HistoryModalclass,TEXTS-basedHistoryTexts) unchanged. Zero caller-side changes required. 1610 tests passing.query-engine.ts1373-LOC monolith split into directory (PR #250,3ff0cc6).src/wiki/query-engine.ts→ 15 focused modules undersrc/wiki/query-engine/.QueryView.buildWikiContext(was 165 LOC inline) decomposes into 4 pure pipeline phases (read-index,load-pages,assemble-context,seed-selector). External API (QueryView,VIEW_TYPE_QUERY,renderThinkingBlocksUI) unchanged via TypeScript directory resolution. 1616 tests passing.- 28 LLM call sites wired through
resolveModelForTask(#208,e96568e). Sourced via Sliced change-by-change across 11 production files: ingest (14 —source-analyzer,page-factory× 7,conversation-ingest× 4,wiki-engine.createSummaryPage,schema-manager,auto-maintain× 2), lint (9 —analysis-phase,dedup-phase,fill-empty-page,fix-dead-link× 2,fix-runners× 2,link-orphan,merge-duplicates,contradictions), query (5 —QueryView× 3 send sites +save-eval,seed-selector). Fivesettings.modeldirect reads intentionally preserved: Test Connection probe plan, 2 log metadata, console.debug, empty-model pre-flight. E2E observability: 6console.debuglines show the resolved model at each major call site. - Source-note aliases propagation (#185,
c0f0bc0). Frontmatteraliases:from source notes now propagates into generatedsources/<slug>page frontmatter, so downstream[[wiki-link]]matching and alias-aware search reach every quote. Reduces "DSA ≠ DeepSeek-Sparse-Attention" type misses on cross-language aliases. - Tier-1 + Tier-2 merge triage (#216,
b7bf5f0,DocTpoint). Classify-then-route duplicate-bypass decision: spurious Tier-1 candidates are skipped outright; Tier-2 runs only on the remainder. Reduces Lint merge batch size without sacrificing high-precision matches.
- Frontmatter write repair (4 user-reported bugs,
1d943ea).aliases:[]no longer falsely passes the alias-deficiency lint check; duplicate aliases are collapsed on write via the newreplaceFrontmatterArrayFieldhelper; block-style frontmatter is preserved (no longer flattened to inline) via the newmergeFrontmatterArrayFieldhelper; write failures are now logged with the offending field name. Affects Smart Fix and merge paths. - Empty-line / trailing-blank-line fix for
## 相关实体/## 相关概念sections (PR #260,9793efd). Tier-2 per-section append normalized to use a single blank-line separator; previously produced double-blank or zero-blank depending on the input. wikiFolderchange propagation (1d943ea,8d5baf3).saveSettingsnow invalidates the QueryView graph cache and WikiEngine pagesCache whenwikiFolderchanges;updateSettingsdrops the path-keyed caches onwikiFolderchange. Stale history migration Notice explains that pre-v1.24.0 query history keeps its old folder paths (clearing history remains the escape hatch).- Retrieval label human-readable + persistence (#221 follow-up,
b46f7b1/81813ae). Retrieval-label text now reads "Found N page(s)" instead of the internal cache key; label is persisted across view re-open.
- 1825 tests passing (132 test files). 81 tests added during the v1.24.0 cycle.
- 5 new i18n keys × 10 locales for the per-task model pickers + Model Scope dropdown + Test Connection labels.
- 8 new i18n keys × 10 locales for the Custom Query Instructions collapsible panel.
Super-aggregated per Keep a Changelog spec. v1.23.2 (single PATCH) over 3 days. Per-PR detail preserved in git log --oneline 1.23.0..1.23.2.
- Semantic progress notification module (#219). New
core/progress-notification.tswithdecideProgressDisplay(scope, isLong, hasUserAction). Manual operations show Notice + status bar; background operations (watch-mode auto-ingest, periodic lint, startup QuickFixes) show status bar only. Channel selection is derived from operation semantics — no user-facing setting. - Query turn indicator (#221). Right-edge vertical dots, one per conversation turn. IntersectionObserver highlights the currently visible turn; clicking scrolls that turn's question to the top via
scrollIntoView({ block: 'start' }). Hover reveals the original question text in a tooltip. - Retrieval label click-to-expand. The
🔍 N page(s) · …label below each assistant response is clickable — clicking toggles an inline panel listing the retrieved pages. - Section header canonicalizer (DocTpoint, PR #241).
core/section-header-canonicalizer.tsuses bounded Levenshtein distance to snap LLM-garbled section headers (e.g.Erwägungen…→Erwähnungen in der Quelle) back to canonical labels on write. Eliminates silent drop from Tier-B retrieval inwikiLanguage: declean re-ingest runs. - Dynamic lint/fix status bar.
wikiEngine.updateStatusBar()is wired to the real Obsidian status bar element. Fix-runners' per-file progress messages (e.g.[3/10] fixing: file.md) reach the status bar during manual lint, watch-mode auto-ingest, and Smart Fix All. wrapWithAdvancedSettingsrefactor. Replaced.bind()+ in-place mutation with composition (Object.create(client)+ explicitcreateMessageoverride). Preserves prototype chain — class-based SDK clients no longer fall back to non-streaming because spread{ ...client }droppedcreateMessageStream.buildPagesListForPromptsources-filter (#234). Adds{ excludeSources: true }default option. The LLM candidate list no longer includeswiki/sources/pages.- Frontmatter serializer consolidation (DocTpoint, PR #238).
mergeFrontmatter/enforceFrontmatterConstraints/mergeDuplicatePagesdelegate to a singleserializeFrontmatterwriter. - Lint completion Notices respect TTLs. All
run*Fixescompletion Notices andlintWikiFaileduseNOTICE_NORMAL(5s) /NOTICE_ERROR(8s) instead ofnew Notice(msg, 0). - License upgrade to Apache 2.0 + DCO. NOTICE file lists all 6 human code contributors alphabetically. CONTRIBUTING.md includes a License & DCO section. Future commits require
Signed-off-by:.
- 1 release (v1.23.2)
- Test count: 1378 → 1431 (+53 tests)
- Composition + per-PR detail:
git log --oneline 1.23.0..1.23.2
Theme: Replace the brittle hand-rolled LLM client (v1.22.x 1625-LOC llm-client.ts with 30+ provider-version workarounds accumulated since v1.20.0) with Vercel AI-SDK v6, then ship the Graph Engine PPR primitive on top. Biggest architectural change since 1.0.
Branch state: refactor/v1.23.0-ai-sdk-migration (38 commits ahead of main, 1376 tests passing, 3.17 MB bundle). Folds in the v1.22.6 hotfix series and P2-4 PPR tuning.
- Vercel AI-SDK v6 migration (P1-7). Replaced hand-rolled
OpenAICompatibleClient/AnthropicClient/AnthropicCompatibleClient(1625 LOC) with@ai-sdk/openai@3/@ai-sdk/anthropic@3/@ai-sdk/openai-compatible@2/ai@6. Newsrc/llm-sdk/(5 files, 1421 LOC:openai-sdk-client.ts455 LOC,anthropic-sdk-client.ts300 LOC,openai-compat-sdk-client.ts449 LOC,token-key-probe.ts70 LOC,create-llm-client.ts151 LOC).src/core/obsidian-fetch-bridge.ts(326 LOC) provides activeDocument-aware fetch for jsdom. Deleted 8 old test files (2609 LOC). Eliminates the entire class of provider-version regressions (#137 / #141 / #143 / #147 / #207). - Graph Engine (Issue #198). Personalized PageRank over
[[wiki-link]]graph — closes #117 (Query Wiki relevance), #157 (hub detection), #175 (link distinctiveness) with one primitive.core/monte-carlo-ppr.ts(Fogaras 2005 MC-PPR, 99 LOC) performs K short random walks per query page at O(K×L) cost independent of |V|.core/ppr-cascade.ts(213 LOC) orchestrates three-tier pipeline (lex fast path → LLM seeds → PPR walks).core/section-extractor.ts(Tier B zero-LLM, 173 LOC).core/hub-detection.ts(134 LOC).core/build-graph.ts(wiki-link graph builder, 13 unit tests). - Query Wiki three-tier pipeline (P1-5). Lex fast path → LLM seed selection (only when fast path is weak) → PPR walks. Reduces 99% of LLM seed-selection cost.
- Hub-link distinctiveness scanner (P1-6, Issue #157 / #175). New lint pass that flags pages whose outgoing links mostly point to low-distinctiveness hubs. 229 LOC + 15 tests. Contributed by @DocTpoint.
- Hub-retirement crystallization signal (PR #215, @DocTpoint).
core/hub-retirement.ts(175 LOC + 12 unit tests + 136 LOC integration tests). Pure percentile-based verdict with dual absolute guards. - Unified URL fallback for custom baseURLs.
core/url-fallback.ts(395 LOC) auto-resolves missing/v1in user-entered baseURLs (Kimi Coding Plan, GLM, z.ai). Module-level static cache survivescreateLLMClientre-creation so Ingest / Lint / Query all benefit. - Token-key probe-then-retry (KISS, no regex).
src/llm-sdk/token-key-probe.ts(70 LOC) caches workingmax_tokens↔max_completion_tokenskey per baseURL on first failure. Triggered byif (statusCode === 400 && !cached) → retry. Addresses root cause of #207 for all OpenAI-compatible gateways. - Real-time streaming for all providers (P2).
result.textStreamtrue逐块 streaming now works in all threellm-sdkclients. macrotask yield between chunks forces a paint frame per chunk (no more batch-arrival UX). Resolves user Q1 feedback. - Welcome note (Phase 5.1.5). Three-tier first-run Welcome note (Tier A empty / Tier B existing / Tier C upgrade).
type: welcomefrontmatter,createWelcomeNotetoggle,Recreate Welcome Notecommand. D8 LLM dynamic translation writes the note in the user's wiki language at write time — no hardcoded i18n. - Multi-File Ingest (Issue #130). Two-pane picker: left = recursive folder tree with per-file checkboxes, right = live ingest queue with status. "Add to queue" two-step flow, per-file cancel, "Cancel all" for pending/running jobs. Reuses
runBatchIngestso the per-file loop, dedup, and report modal are shared with folder ingest. NewIngestQueuepub/sub store is the single source of truth for in-session ingest lifecycle. - LM Studio API-key gate (Issue #223).
main.ts:962now excludes bothollamaandlmstudiofrom API-key validation. Local providers can test connection without an API key. - knn baseline analysis (P2-3 eval acceptance gate). DocTpoint ran a knn baseline (bge-m3, no graph) on the same
sample-50pagefixture per #198 follow-up: cascade R@5 27.1% vs knn 24.1% (3pp gap). Reinforces 2026-06-22 #175 rejection — embeddings permanently rejected. - i18n settings rewrite (10 locales). User-first language throughout ("disable thinking") instead of implementation details ("3-tier dialect fallback chain"). 14 new keys per locale for Welcome note + Ingest modal UI.
- Sponsor section. Ko-fi button + 💖 Support the Project section in all 10 READMEs. https://ko-fi.com/greenerdalii.
- P2-4 PPR tuning. Real vault (2142 pages) tuning across 6 iterations. Recommended parameters
damping=0.05, numWalks=3000, walkLength=20improve R@5 from 21.5% → 23.8% (+11% relative). Seesrc/__tests__/fixtures/wikis/sample-50page/REAL_VAULT_EVAL.md.
- Provider error body now reaches Test Connection UI.
window.fetchre-fetch with 5s timeout captures the provider's diagnostic into the Notice. Replaces genericstatus 400with e.g."status 429: You exceeded your current quota". - Lint performance knobs centralised in
src/constants.ts. Single-file tuning instead of 4-file drift acrosscontroller.ts/duplicate-detection.ts/preparation.ts/batch-limits.ts. - 429/5xx exponential backoff on Responses API path. Both Chat Completions and Responses API paths now share the same
withRetry(3 attempts, 1s/2s/4s + jitter). thinkingControlCachedeprecated. Removed the 3-tier dialect probe; AI-SDK handles thinking internally. Cache retained on disk for backward-compat (will be removed in v1.24.0 if no use case surfaces).- Real-time streaming UX. Cascade + LLM seed retrieval improvements: reduced tokens per cascade round, tightened seed-selection prompt.
- Welcome note refactor. Moved LLM config status from in-body text to frontmatter (hidden metadata). Local-check in Welcome note orchestrator (no LLM if config already valid).
- #207 — GPT-5.x models no longer fail Test Connection with 400. Full coverage including
-provariants (v1.22.5 / v1.22.6 hotfixes). - #204 — Auto Ingest no longer opens blocking modal.
trigger='auto'|'manual'field onIngestReport/IngestOptionsroutes auto-ingest completion toonAutoIngestDone(Notice) instead ofIngestReportModal. - #204 — Auto Smart Fix completion is context-aware. Same
triggerpattern routesAutoMaintainManager.schedulePeriodicLintcompletion differently based onautoSmartFixsetting. - #223 — LM Studio Test Connection no longer requires API key. Local providers excluded from the API-key gate.
generation_completeno longer stamped ontolog.md/index.md/schema/(v1.22.3, carried forward).isInWikiContentFolder()guard restricts the stamp towiki/{entities,concepts,sources}/....- Real-time streaming was batched. Fixed via macrotask yield +
result.textStream-only consumption (notfullStreamthentextStream, which buffered all events).
- 1376 tests passing across 100 files (+272 since v1.22.0).
- Bundle size 1.24 MB → 3.17 MB (user accepted 2026-06-29). Obsidian manifest has no size limit; lazy
await import()for AI-SDK packages didn't reduce bundle (esbuild CJS inline); future ESM bundle / dynamic chunk can revisit. - #207 close decision: user will close manually after real-world testing — separate commit
Closes #207, not part of v1.23.0. - #213 (configurable page categories): Discussion-only, NOT confirmed for any minor release per user instruction 2026-06-30. Requires broader community/architectural discussion.
Super-aggregated per Keep a Changelog spec + CLAUDE.md "ancient versions are pre-aggregated". 6 PATCH releases (v1.22.1 → v1.22.6) over 7 days. Per-PR detail preserved in git log --oneline 1.22.0..1.22.6 and memory files.
- #204 — Auto-ingest modal suppression: v1.22.2 split
onIngestDone→onAutoIngestDonewithautoIngestNotificationLevel: 'notice' | 'modal'setting; v1.22.6 wirestrigger: 'auto' | 'manual'throughIngestReportso watch-mode ingests skip the blockingIngestReportModal. Sametriggerpattern applied torunLintWikifor auto vs manual completion dispatch. log.mdheader language-agnosticism: v1.22.3 replaced text-based detection with structural<!-- llm-wiki-log-header-start -->marker + moved all 10 locale header strings intosrc/texts/<lang>.ts; auto-migration viaisOldFormatLogHeader()/migrateLogHeader().generation_completestamp scope narrowed: v1.22.3 addedisInWikiContentFolder()guard so the stamp no longer polluteslog.md/index.md/schema/files on every QuickFix run.periodicLintcadence refined: v1.22.2 removed "Hourly" (unrealistic for LLM-based lint), added "Monthly"; auto-migratehourlysaves todailyon next plugin load.- #207 — GPT-5.x OpenAI Responses API routing: v1.22.4 introduced
max_tokens↔max_completion_tokensruntime probe-then-cache + provider error body enrichment; v1.22.5 addedisResponsesApiModel()forgpt-5.1+ / o1-o4reasoning family routing to/v1/responseswithreasoning: { effort: 'low' }; v1.22.6 broadened the regex to covergpt-5.x-provariants. Test Connection Notice now surfaces provider's actual error body (not bare status code). 429/5xx exponential-backoff retry extended to Responses API path. - GPT-5.x-pro path correctness (v1.22.6 follow-up). Regex broadened to
^(gpt-5\.[1-9]\d*(?:-pro)?|o1(?:-mini|-preview)?|o3(?:-mini|-pro)?|o4-mini)$;gpt-5-chat-latestexclusion preserved. - Lint performance knobs centralised in
src/constants.ts: v1.22.4 unified yield cadences (LINT_YIELD_EVERY_OUTER/_PHASE1/_COMPARISON), candidate batch sizing, prep batch read, and source-analyzer batch sizing — eliminated drift acrosscontroller.ts/duplicate-detection.ts/preparation.ts/batch-limits.ts.
- 6 releases
- Test count: 1054 → 1118 (+64 tests across the series)
- Composition + per-PR detail:
git log --oneline 1.22.0..1.22.6+ memory files
- #97 — One-click schema apply with IDE-style diff Modal + auto-backup.
SchemaDiffModalclass (dual-pane IDE-style diff, Apply/Cancel/Open file buttons, Regenerate hidden for v1.22).applySchemaSuggestion()with auto-backup to.llm-wiki-backups/schema/(rotation MAX_BACKUPS=3 viacore/backup-rotation.ts).lineDiff()LCS algorithm incore/diff.ts. Lint "Update Schema" button removed from command palette — schema updates flow through Lint Modal only (single entry point). - Schema dynamic tag sync. Schema vocabulary is now the single source of truth; tag vocab injected into generation prompts via
SchemaContext+buildSchemaSectionTemplate.parse-suggestion.tsfor structured LLM response parsing. - Traditional Chinese (zh-TW) locale. 10th language (zh-Hant). Parity guard extended to all 10 locales (bidirectional). 8 new i18n keys per language for schema diff modal.
- #189 — Ingest status bar shows document name + batch progress (PR by @YounianC). Single-file ingest displays
<doc> · Ingesting... click to cancelinstead of the bare label. Folder batch ingest shows[current/total] <doc> · Ingesting... click to cancel. New pure-functioncore/status-bar.ts(buildIngestStatusBarText) composes from the existing localizedingestionStatusBarlabel — no new i18n keys, all 10 locales covered automatically.WikiEngineingestion-start callback now passes the source basename (optional param, backward-compatible).batchProgressfield inmain.tstracks loop position.
merge.tshardcoded English section headers (#188). BothmergeEntityPageandmergeConceptPageprompt templates used hardcoded## Related Entities/## Related Concepts/## Basic Information/## Description/## Mentions in Sourceheaders, ignoring the configuredwikiLanguage. Replaced with{{section_*}}placeholders soapplySectionLabels()localizes them consistently across create and merge paths. Non-English vaults no longer get mixed-language section headers.appendAliasesblock-replace regex left stale items (#186).page-factory.ts:70regex/^aliases:[\s\S]*?(?=\n\S|\n*$)/m— themflag caused$to match end-of-line, so the lookahead succeeded immediately and the lazy quantifier matched zero characters. Only the barealiases:line was replaced; existing list items survived, producing duplicate entries on every subsequent append. Fixed with/^aliases:[^\n]*(?:\n[ \t]+[^\n]*)*/mwhich consumes continuation lines by indentation.- Lint:
apply-suggestion.tsusedvault.delete()fallback. Simplified to directapp.fileManager.trashFilecall — respects user's file deletion preference per Obsidian review ruleobsidianmd/prefer-file-manager-trash-file. Test mock updated accordingly. - Lint:
parse-suggestion.tsunnecessary type assertion.as LLMSchemaResponsecast removed (receiver already accepts the original type).
- 1006 tests passing (was 948 in v1.21.1; +58: schema suite 48 tests + status-bar suite 7 tests + #186/#188 regression tests 3 tests).
Super-aggregated per Keep a Changelog spec. v1.21.1 (single PATCH) over 1 day. Per-PR detail preserved in git log --oneline 1.21.0..1.21.1.
- #173 Symptom A — createOrUpdateFile create-retry loop. When
getAbstractFileByPathreturned null (e.g. macOS NFC/NFD normalization mismatch), the 3-attempt loop kept callingvault.createinstead of first resolving viaresolveFileInVault. Now resolves at the earliest attempt, eliminating 3× failed retry overhead. Contributed by @Indexed-Apogrypha (reporting). - esbuild 0.28.0 → 0.28.1. Patches GHSA-g7r4-m6w7-qqqr (low severity, dev-only arbitrary file read on Windows).
- 1 release (v1.21.1)
- Composition + per-PR detail:
git log --oneline 1.21.0..1.21.1
- Pre-ingest requirements gate (#164). Every source file is now validated before any LLM call — non-empty, compatible file type, and unique — and files that fail are logged and skipped instead of reaching the model. New
core/source-requirements.tsholds an extensible, orderedCONTENT_CHECKSregistry so future checks (e.g. prompt-injection) can be added as a single entry. Contributed by @Indexed-Apogrypha.- Non-empty (
isBlankSource): empty, whitespace-only, and frontmatter-only notes are skipped — closing the #164 root cause where small/local models (e.g. Ollama) hallucinated entities/concepts from blank content interpolated into the extraction prompt. - Compatible file type: case-insensitive allowlist
['md', 'markdown', 'txt', 'text']. Folder and active-file ingest now accept.txt/.text(was.md-only). - Uniqueness (
hashBody): content-hash de-duplication (length-prefixed FNV-1a over the normalized body) catches duplicate content even across different file paths, plus within-batch dedup for folder and watcher ingests (both share onecreateBatchContext()); the hash is stamped into the source page frontmatter ascontentHash.
- Non-empty (
- Re-ingest confirmation prompt. Interactive ingests (file picker / active file) prompt before re-ingesting a duplicate (new
ConfirmModal); folder/watcher ingests auto-skip duplicates. The ingest report now lists skipped files with a localized reason (empty / unsupported type / duplicate content). New i18n keys across all 9 locales. Contributed by @Indexed-Apogrypha. - Operation History Panel (#122). Pure-function
parseLogEntries+HistoryModalwith date grouping, search, filter, clickable page links, and insight-driven visualization. Command palette entry + settings entry. - Schema Coherence Phase 1 (#124).
SchemaContextshared parsed representation ofschema/config.md, used by both system prompts and generation prompts.buildSchemaSectionTemplateextracts user-defined sections. Tag vocabulary injection into system prompt. - Incomplete-page cleaner (#170). Wiki pages left in a partial state (interrupted ingest, plugin reload mid-write, LLM error) are automatically cleaned on startup via
generation_completefrontmatter flag + QuickFixes Phase 3 self-scan. Pages without the field are treated as legacy (preserved). - Italian locale (#159). 9th language added to UI and wiki output. Contributed by @FrancoTampieri.
- Empty notes made small/local LLMs fabricate wiki pages (#164, CRITICAL). Ingesting an empty / whitespace-only / frontmatter-only note no longer produces fabricated entity/concept pages (large models refused the blank input, so it never surfaced in dev). A defense-in-depth
isBlankSourceguard was also added insource-analyzer.tsbefore the extraction prompt is built. Contributed by @Indexed-Apogrypha. - Hardcoded Chinese error string leaked into non-Chinese UI (#172).
wiki-engine.tscreateOrUpdateFilefinal-fallback throw now usesgetText('fileWriteFailed')with 9-locale i18n coverage. - Duplicate entry in
createdPagesinflated report count (#173).dedupPages()pure-function helper prevents duplicated surface-forms from inflating the ingest report "Created" listing.
- New coverage for the gate:
core/source-requirements,isBlankSource/upsertFrontmatterFieldincore/frontmatter, the #164 reproduction inwiki/source-analyzer, and a new in-memoryWikiEngineingest-gate harness (wiki/wiki-engine-ingest). - Watcher batch-context wiring (
schema/auto-maintain) and thebuildIngestedHashesTTL-cache + write-invalidation paths (wiki/wiki-engine-ingest). - Incomplete-page cleaner tests (
core/incomplete-page-cleaner):isIncomplete,findIncompletePages,cleanIncompletePages. - i18n error message assertion (
wiki/wiki-engine-i18n-error). dedupPagesordering/edge-case tests (wiki/wiki-engine-dedup).- 939 tests passing (was 791 in v1.20.3). +148 tests, 67 test files.
Super-aggregated per Keep a Changelog spec. 3 PATCH releases (v1.20.1 → v1.20.3) over 2 days. Per-PR detail preserved in git log --oneline 1.20.0..1.20.3.
- #141 / #147 — Anthropic prefill rejection on newer Claude models: v1.20.1 detected the 400 "Prefilling assistant messages is not supported for this model" rejection (Claude Opus 4.8 / 4.7 / 4.6, Sonnet 4.6, Claude Fable 5, Claude Mythos 5 / Preview), cached per-client, and auto-retried without prefill. v1.20.2 also fixed the fallback path: Anthropic's Messages API only accepts
user/assistantroles inmessages— the no-prefill retry had been puttingsystemintomessagescausing a second 400 that masked the real fix. All 4 Anthropic fallback paths now use top-levelbody.systeminstead. - #154 —
mergeFrontmatteralias dedup on re-ingest: v1.20.3 fixed unbounded alias array growth on repeated re-ingests (one real-world page had ~15× duplicate alias block / 86 duplicate lines). Mirrors the dedup contract inenforceFrontmatterConstraints(first occurrence wins, empty strings dropped). Contributed by @DocTpoint. - #155 / PR #156 — Source provenance slug collision: v1.20.3 added
<basename>_<6hex FNV-1a of full path>slug derivation. Two source files sharing a basename across folders (e.g. 11×About this course.mdacross Academy courses) no longer silently overwrite each other;[[sources/<slug>]]backlinks resolve to the correct source. Purecore/source-slug.tsmodule. Contributed by @Indexed-Apogrypha. - PR #158 —
updateRelatedPageignoredreviewed: truelock on Stage-4: v1.20.3 routedreviewed: truepages toappendToReviewedPage(parity withcreateOrUpdatePage) so re-ingesting an unrelated note cannot LLM-rewrite a curated reviewed page body. Contributed by @DocTpoint. - PR #156 follow-up — tsconfig housekeeping:
libbumped to ES2021 (sotrimEndresolves cleanly under newer TS language servers); vestigialbaseUrldropped (clears TS 6/7 deprecation warning).
- 3 releases
- Test count: 771 → 791 (+20 tests across the series)
- Composition + per-PR detail:
git log --oneline 1.20.0..1.20.3
- Collapsible thinking UI in Query Wiki. When thinking-capable models (DeepSeek, etc.) return reasoning content, it's displayed in a collapsed
💭 Thinking processpanel above the answer (ChatGPT/Claude.ai style). Fully localized in 8 languages. extractThinkingBlocks()pure function incore/markdown.ts— extracts<think>and<thinking>blocks from LLM responses.wrapReasoningContent()pure function — encodes reasoning_content into<think>tags with escaping for nested closing tags.renderThinkingBlocksUI()— DOM construction for collapsible thinking panel with localized labels.- DeepSeek
reasoning_contentextraction. SSE parser extractsreasoning_contentfrom OpenAI-format deltas. Both streaming and non-streaming paths prepend reasoning as<think>tags for the thinking UI. PROTECTED_FIELDSwhitelist inOpenAICompatibleClient— preventsmodel,messages,streamfrom being stripped byunsupportedFieldseven if a 400 error mentions them.
- Provider-first thinking control (default
disableThinking: false). The plugin no longer sends any thinking-control field by default — the provider decides its own reasoning behavior. Old default wastrue(sentthinking.type='disabled'). Users who explicitly want to suppress thinking can enable "Disable thinking" in Custom Advanced Settings, which triggers the 3-tier dialect fallback. enableThinkingspread consistency. All 22 LLM call sites now use...(ctx.settings.disableThinking ? { enableThinking: false } : {})— page-factory, contradictions, conversation-ingest were missing the spread (had comment-only placeholders).AnthropicClientbaseUrl normalization. Constructor now strips trailing/v1and re-appends it, preventing double-path/v1/v1(fixes #141, #134).listModels()usesthis.baseUrl. AnthropiclistModels()no longer hardcodeshttps://api.anthropic.com/v1/models.isGpt5prefix check tightened.startsWith('gpt-5')→=== 'gpt-5' || startsWith('gpt-5-')to avoid matching future unrelated models..includes('<think')guard is now case-insensitive. Uses.toLowerCase()to catch<Thinking>variants.- v1.20.0 migration in
loadSettings(). ResetsdisableThinkingfromtruetofalseandadvancedSettingsModeto'default'for existing users.
- gpt-5
max_completion_tokens(Issue #143). GPT-5 series models now usemax_completion_tokensinstead ofmax_tokens. Truncation retry also preserves the correct token key. - Truncation retry loses reasoning_content.
extractTextcallback now wraps retry response'sreasoning_contentviawrapReasoningContent. - Streaming path missing final render. After
createMessageStreamreturns, the full response (including<think>tags) is now rendered viarenderMarkdownContent— thinking content was previously only available during non-streaming path. - Non-streaming fallback missing
chatTemperature. The fallback path when streaming fails now includes the user's configured temperature. if (fullResponse)dropped empty responses. Changed to!== undefined/nullguard to handle empty-string responses.- Query Wiki respects
wikiFolder. Prompt templates and defense-in-depth normalization replace hardcodedwiki/paths. - Query Wiki auto-scroll. Chat scrolls to bottom on open.
- User message right-align. User bubbles use
flex-endalignment with accent background.
Super-aggregated per Keep a Changelog spec. v1.19.1 (single PATCH) over 1 day. Per-PR detail preserved in git log --oneline 1.19.0..1.19.1.
- Gemini HTTP 400 on ingestion (Issue #137). Added a 3-tier thinking-control dialect fallback chain (anthropic → openai → none) so
OpenAICompatibleClientauto-discovers the correct field name (thinking.type='disabled'vsreasoning_effort='none'vs none) per baseUrl. Result is cached on the client + indata.jsonso subsequent requests skip the 400 probe round-trip.thinkingControlCacheschema toggles frombooleanto dialect string ('anthropic' | 'openai' | 'none'). - Settings tab auto-save wiped
thinkingControlCacheon every close.LLMWikiSettingTab.hide()and the explicit Save button used shallow{ ...tempSettings }spread that droppedthinkingControlCache. Fix: extractcommitTempSettings()helper that preserves untracked probe-mutated fields. - Generic 400-field rejection retry (temperature, repetition_penalty, etc.).
parseUnknownFields()extracts rejected field names from Gemini-style 400 bodies;unsupportedFieldsSet pre-strips them on subsequent requests.retryBodyWithStrippedFields()helper deduplicates strip-and-retry logic across non-stream and stream paths. - Stream path field-strip retry was dead code.
createMessageStream'sdoRequestlacked an inner 400 catch block; added the same catch+populate pattern that the non-stream path uses. [DEBUG-400]firing on 429 quota errors. Limited to 400-class errors only; 429/5xx go through standardwithRetrybackoff without the re-fetch overhead.- Fallback notices always in English.
queueFallbackNotice()hard-codedTEXTS.en; the 3 newly-added fallback notice keys were present in all 8 locale files but never used. Fixed:OpenAICompatibleClientnow has alanguagefield wired bycreateLLMClient. - Advanced LLM Settings moved above Test Connection in the settings panel.
- 400-path diagnostic output silenced from
console.errortoconsole.debug. - Simplify cleanup:
IS_400regex extracted as module-level constant;retryBodyWithStrippedFieldsdeduplicates strip+change-detect;applyThinkingDialectFallbackreusesbuildRequestBody;commitTempSettings()extracted; probe success/failure cache write clarified.
- 1 release (v1.19.1)
- Test count: 728 → 744 (+16 tests, 0 regressions)
- Composition + per-PR detail:
git log --oneline 1.19.0..1.19.1
- Compact slug list in analyzeSource prompt (Issue #116). New
buildCompactSlugList()injects a sorted slug-only list of existing wiki pages into the prompt so the LLM uses exact paths when creating[[links]], reducing dead-link slug mismatches caused by the verbose 40K-char index cap. Previously, only the first ~50 pages fit. Contributed by @DocTpoint. - Quote-grounding lint scanner (Issue #126). New
scanQuoteGrounding()pure function verifies that every quote under## Mentions in Sourcecan be found in the linked source file. Supports both current"quote" — [[sources/slug]]format and historical bare quotes (scans all source files if no link is present). Tier 1 = exact substring match; Tier 2 = normalized (case-fold, punctuation stripped, whitespace collapsed). Report-only, zero token cost. Contributed by @DocTpoint. - Advanced LLM parameter settings (Issue #128). Collapsible "Advanced parameter settings" section in LLM Configuration with a Default/Custom mode selector. Default mode keeps all advanced parameters hidden and "disable thinking" on — the right choice for most users. Custom mode reveals the thinking toggle, extraction temperature (range 0–2), query temperature (range 0–2), and repetition penalty (range 0–2). Only sent to the LLM when the user sets a value — cloud providers that ignore the field fall back to their own defaults. The
disableThinkingfield name is preserved indata.jsonfor backward compatibility; production code passes the affirmativeenableThinkingform internally. - Reasoning-only response detection (Issue #99).
OpenAICompatibleClient.createMessagenow detects when the model returns an empty response with high reasoning tokens (content == '' && finish_reason == 'length' && reasoning_tokens >= 50% of completion_tokens) and throws an actionable error prompting the user to check the disable-thinking toggle or switch models. Also adds automatic 400 fallback: when the provider rejectsthinking.type='disabled', the client retries withchat_template_kwargs: {enable_thinking: false}(auto-fallback, no separate user toggle). - Status bar mirrors popup during ingest and lint (Issue #110). All ingestion progress messages and lint checkpoints now update both the popup Notice and the Obsidian status bar simultaneously.
makeMirroredNotice.hide()clears the status bar text. Fix-runner Notices mirror everysetMessage()call to the status bar. Contributed by @dmarchevsky. - Auto Smart Fix setting (PR #109). When enabled, lint automatically runs all Smart Fix phases after analysis completes without showing the report modal. Default: off — existing users see no behaviour change.
- Sources normalization in write path (PR #127).
fixPollutedSources()is called from the centralized write chokepoint (WikiEngine.createOrUpdateFile()), so every generated/merged page gets a normalizedsources:field. Contributed by @DocTpoint.
- Startup quick-fixes Notice simplified. Removed heavy emoji icons and
━━━━━━━━━━━━━━━━separators; cleaner layout with plain text prefixes. Logs now use English consistently. - Lint report summary now includes ungroundedQuotes and tagViolations counts. The report header line shows all current dimensions.
- Ungrounded quotes section in Lint report. When scanQuoteGrounding finds issues, a new "Ungrounded quotes" section appears in the programmatic findings report.
- lintTagViolationSection i18n completed. Previously 7 non-English locales showed English placeholder — now fully translated (de/es/fr/ja/ko/pt/zh).
- Language dropdown labels simplified. Labels now use each language's native name only (e.g.
中文,日本語,Deutsch) without English sub-labels.
- Advanced settings mode dropdown did not render controls. The
onChangehandler was missingthis.display()(contrast with Tag Vocabulary dropdown which called it). Fixed: choosing "Custom" now properly reveals thinking toggle, temperature, and penalty inputs. - Misleading watchedFolders debug logs removed.
loadSettings/saveSettingsno longer printwatchedFolderscontent, preventing confusion whenautoWatchSourcesis off. - Previously-merged PR #110 "click to cancel" status bar affordance. UX fix by @dmarchevsky in PR #110: status bar now shows locale-specific "click to cancel" throughout ingest/lint/fix operations.
- Stage 4 no-op skip (PR #131 Tier 1).
PageFactory.updateRelatedPageskips the LLM call whennew_inforesolves to the'No directly relevant information'fallback string. Removes ~33% of Stage 4 LLM calls. Still updates frontmattersources+updatedprogrammatically. Contributed by @DocTpoint.
- lint-controller modularization. Extracted
phases/preparation.ts,phases/programmatic.ts,report-builder.ts,types.tsfrom the monolithic controller. lint-controller.ts went from 1069 → 897 lines. 17 new unit tests (728 total). - schema-analyze moved to schema/ directory.
src/wiki/schema-analyze.ts→src/schema/analyze.ts. - LintContext extracted to lint/types.ts. Breaks the latent import cycle between
fix-runners.tsandlint-controller.ts;fix-runnersnow imports from./types. - lint-controller + lint-fixes moved into lint/ directory.
src/wiki/lint/controller.ts(was lint-controller.ts),src/wiki/lint/fixer.ts(was lint-fixes.ts). All internal imports updated.
Super-aggregated per Keep a Changelog spec. 2 PATCH releases (v1.18.1 + v1.18.2) over 1 day. Per-PR detail preserved in git log --oneline 1.18.0..1.18.2.
- Custom extraction limits not hard-enforced (Issue #120, v1.18.2). When
extractionGranularitywas set tocustom, thecustomEntityLimit/customConceptLimitsettings were only enforced as soft prompt hints — the LLM routinely returned 12-25 items for a configured cap of 8. After all batches are accumulated and immediately beforebuildSourceAnalysis(), slice bothaccumulation.entitiesandaccumulation.conceptsto the configured limits. The first N items in extraction order are preserved. No behavior change fordefault/1-5granularity modes. - Obsidian Community Plugin review compliance (v1.18.1). Removed
documentfallback andeslint-disablecomments referencingobsidianmd/prefer-active-active-docfrom production code.activeDocumentstub centralized in test setup file. No user-visible behavior change.
- 2 releases
- Composition + per-PR detail:
git log --oneline 1.18.0..1.18.2
- User-Controlled Tag Vocabulary (Issue #85) — chip input UX + end-to-end pipeline (v6). Wiki admins in medical, legal, R&D, and other professional domains can now define a controlled vocabulary for entity/concept frontmatter tags and the LLM actually uses it. The new "Tag Vocabulary" sub-block (embedded in Wiki Configuration — no separate heading) has a Vocabulary Mode dropdown:
- Default — preserves the original hardcoded subtype tags (
person/organization/… for entities,theory/method/… for concepts). The dropdown description now shows the concrete default list inline:Default uses built-in tags: person, organization, project, … (entities) / theory, method, … (concepts). - Custom — two chip inputs (Custom Entity Tags + Custom Concept Tags). Add via Enter /
,/;, remove via × click or Backspace on empty input. Nested tags with/(e.g.Arzneimittel/Neurologie) are preserved. Whitespace is trimmed, empty entries filtered, duplicates (case-insensitive) are silently skipped with a brief shake animation. CJK IME composition is respected (event.isComposingguard). Defaults are editable baseline (not preview) — when the persisted custom CSV is empty, the chip input materializes the default vocabulary as fully-editable chips.
- Default — preserves the original hardcoded subtype tags (
- 🔴 v6: End-to-end prompt injection. New
buildActiveTagVocabularySection()+appendTagVocabularyToPrompt()helpers inject the active vocabulary into ingestion (source-analyzer), page generation (page-factory × 3 sites: new page, merge, rebuild), and lint analyze (lint-controller). The LLM now knows exactly which entity/concept types are valid and stops inventing its own. Before v6, the user-defined vocabulary was only used for post-hoc validation; the LLM kept inventing subtype names that got silently dropped at write time. - 🔴 v6: Preserve LLM intent on write.
enforceFrontmatterConstraintsno longer silently drops out-of-vocab tags. It retains all LLM-emitted tags (with aconsole.debugnote when the vocabulary diverges) so the user can see exactly what the model produced and can decide whether to expand their custom vocabulary. Fallback toDEFAULT_ENTITY_TAG/DEFAULT_CONCEPT_TAGonly when the tags array is genuinely empty. - v1 → v2 migration runs on
onload(). NewcleanupVocabularyTags()readscustomEntityTags/customConceptTags, normalizes them vianormalizeVocabularyCsv(trim, dedupe case-insensitively, drop empty), and writes back todata.jsonso existing users see clean chips on first reload. getActiveEntityTags/getActiveConceptTagspure helpers inutils.ts— the single source of truth for "which tags are valid right now". All call-sites (page-factory, lint-fixes × 2) passthis.ctx.settings.- 🔴 v7: Programmatic tag audit + LLM-assisted retag. New
scanTagViolations()(pure function insrc/wiki/lint/scanners.ts) walks every entity/concept/source page in the wiki at Lint time and reports any page whosefrontmatter.tagsarray contains at least one value not in the active vocabulary. Zero token cost, <50ms on 2000-page vaults. The Lint Report Modal gets a new "🏷️ Retag N page(s) with LLM" button that callsrunRetagViolations()(insrc/wiki/lint/fix-runners.ts): the LLM is given the page's first-paragraph summary + the active vocabulary section (viaappendTagVocabularyToPrompt()from v6), and returns a newtags: string[]. The runner re-validates every returned tag against the active vocabulary (defensive), and only thetags:line of the frontmatter is rewritten — the body is byte-identical. Source pages get a staticVALID_SOURCE_TAGSvocabulary (paper / article / book / transcript / clippings / notes / other) — NOT user-configurable per Issue #85 v7 design decision. Smart Fix All now runs retag as Phase 5 (after duplicates / orphans / empty pages). enforceFrontmatterConstraintssource-page branch now validates againstVALID_SOURCE_TAGS(previously:[]= no validation). Page writes still succeed even with out-of-vocab tags thanks to v6's preserve-LLM-intent behavior (only aconsole.debugnote when divergence is detected).- Default vocabulary cross-discipline optimization (v8). Entity
location→placefor more natural semantics; Concept+field,+phenomenon,+standard,-technologyfor better distinction; Source-document(overlapped with article),notesretained. Full backward compatibility via v6 preserve-LLM-intent — removed tags survive in existing frontmatter, flagged by Lint audit for optional LLM-assisted retag. - Reviewed-guard (D4 design).
enforceFrontmatterConstraintsnow respectsfm.reviewed: true: when a user has marked a page as reviewed, their tag intent (including intentionally emptytags: []) is preserved — the function does NOT auto-filltags: [other]. Only LLM-hallucinated dates are still stripped (date fields are strictly programmatic). Aligns with existing reviewed-aware code paths (lint-fixes.ts:439, page-factory.ts:288/308, prompts/generation.ts:206-241). - 🔴 Layer A complete: disableThinking propagation (Issue #99 v2). The v1.16.2 three-layer defense added
disableThinkingparameter to the LLM client interface but ZERO of ~22 productioncreateMessagecalls passed it. This release completes the wiring:disableThinkingis declared inLLMWikiSettings(defaulttrue), and all 22createMessage/createMessageStreamcalls across 7 engine files now passdisableThinking: settings.disableThinking. Thinking-capable models (Gemma 4, DeepSeek-R1, QwQ) receivethinking: { type: 'disabled' }on every call, preventing mid-response CoT and duplicated body output at the source. - AnthropicClient fallback for thinking-mandatory models. Unlike OpenAICompatibleClient which already had try/catch fallback from v1.16.2, AnthropicCompatibleClient and AnthropicClient would throw unconditionally when a provider rejects
thinking.type='disabled'(e.g. Claude Fable 5 / Mythos 5). Both clients now wrap the request in try/catch: on 400 +disableThinking=true+isThinkingControlError(), they cachethinkingControlSupported=falseand retry the request WITHOUT the thinking field. The redundant ~70-line duplicated request/parse/withTruncationRetry block was refactored into a sharedanthropicDoRequesthelper.
- Long-document ingestion now works end-to-end. Previously, sources over ~200KB were unprocessable due to a hardcoded batch size of 15 items in custom granularity and a
max_tokenscap that truncated large responses. The same 619KB Chinese source (史记 / Shiji) that previously failed after 3 minutes and 15 items now completes fully, extracting hundreds of entities and concepts. Key enablers:- Custom granularity now dynamically scales
initialBatchSizeandmaxBatchesBasefrom the user'scustomEntityLimit+customConceptLimit(was hardcoded to 5/1, capped at 15 items). For caps of 300+300: batchSize=50, maxBatchesBase=12, up to 36 effective batches. max_tokensnow scales with batch size (base: 16K → 20K for 50-item batches; retry cap: 60K), avoiding the silent truncation that previously caused later batches to fail with malformed JSON.- Truncation retry: if a non-first batch's response is truncated, the system halves the batch size and retries once instead of aborting the whole ingestion.
- Custom granularity now dynamically scales
- Source pages inherit tags from source note frontmatter (Issue #90). The LLM used to inject arbitrary concept names (e.g.
Alzheimer-Demenz,Neuroprotektion) into source pages, polluting the user's tag vocabulary. NewextractSourceTags()pure helper reads the source note's frontmatter tags and passes them directly to the summary-page template, falling back to LLM-derived names only when the source has no tags. - Default Schema documents the new contracts. Three new sections were added to the default
wiki-folder/schema/config.md:## Source Page Template— mandates tag inheritance from source note, no LLM-derived tags.## Date Fields— documents thatcreated/updatedare filled programmatically (the LLM may produce wrong dates; the system overrides them).## Mentions Format— academic-footnote style:- "verbatim quote (optional translation)" — [[source-path|display-name]]. Existing user schema files are NOT overwritten; onlyregenerateDefaultSchema()writes the new template.
- Lint report persistence with minute-precision timestamps. Lint now writes the full report to
wiki-folder/log.mdbefore showing the modal, with a📋 Full report saved to log.mdhint. Log entries have minute-precision timestamps (e.g.[2026-06-08 14:35]) so multiple Lint runs on the same day are distinguishable. The Lint Report Modal also points to the persisted log. - Custom granularity upper bound raised from 300 to 500 to support professional knowledge bases (legal, medical, deep research). 8-language i18n text updated accordingly.
- Mentions are now footnote-style with explicit source attribution. The "Mentions in Source" section in entity/concept pages now renders each verbatim quote as
- "quote" — [[source-path|display-name]], replacing the previous free-form block of untraced quotes. The source link makes every quote traceable to its origin, so future page merges can never mix up which quote came from which source. - Setting description for custom entity/concept limit now reads "1-500" (was "1-300") in all 8 languages to match the new hard cap.
- Test connection no longer persists broken config on failure. When "Test Connection" fails, the previously-saved settings are restored and a 2nd saveData() call re-persists the original. Prevents the user from accidentally saving settings that the test proved broken.
- Provider settings no longer fail to propagate. Switching Provider/API Key/Model in Settings used to fail to reach the wiki engine, so the next Ingest/Lint/Query would silently use the old provider. Root cause:
settings.tswas replacingplugin.settingswith a NEW object (from tempSettings spread), but theEngineContextpassed to all submodules captured the OLD reference at construction time. Fix:WikiEngine.updateSettings()now keeps the EngineContext.settings reference in sync, and all settings paths (saveSettings, test connection, language switch) call it. - LLM-hallucinated dates in frontmatter are now stripped. The LLM sometimes invents wrong dates (e.g. a 2025 date on a 2026-06-08 ingestion).
enforceFrontmatterConstraintsnow strips LLM-generatedcreated/updatedlines and replaces them with programmatic values:createdis preserved on merge (older value kept),updatedis always set to today. 3 new TDD tests cover: preserves created, forces updated, adds when missing. - Long-source Notice no longer blocks the UI. Was
new Notice(..., 0)(persistent, never auto-hides). NowNOTICE_NORMAL(5-second auto-hide) so the user isn't stuck with a forever-visible notice. - Lint dedup progress "1/1/1" display bug. The progress template was
批次 {current}/{total}butprogressLabelwas already passed1/1(with the total), causing duplication. Removed the extra/{total}substitution. - Folder ingest
setDoneCallbacknot restored on early return. IfingestCount === 0(no new files), the method returned early without restoring the callback, so subsequent folder ingests used a wrong callback. Now restored before the early return. - 5 audit-discovered issues (test settings pollution on connection failure; custom-scaling edge cases; repair-call max_tokens insufficient; constant duplication; comment misleading). All resolved with explicit Gate-4 performance verification.
Closes: #90 — Source pages now inherit tags from the source note frontmatter instead of LLM-generated concept names.
- Small Schema / prompt / i18n cleanups (new
lintLogReferencei18n key in 8 languages; prompt updates for the new mentions format; pure helper extractions:extractSourceTags,truncateMentionswithsourcePathparameter).
- 38 new tests added (549 → 587): 7 in
batch-limits.test.ts, 6 intruncateMentionsblock ofutils.test.ts, 3 inenforceFrontmatterConstraintsblock, 6 inextractSourceTagsblock, 1 indefault-schema.test.ts, plus updates across reorganized test folders. Test suite: 28 files, 587 tests, 0 regressions.
Super-aggregated per Keep a Changelog spec. 3 PATCH releases (v1.16.1 + v1.16.2 + v1.16.3) over 3 days. Per-PR detail preserved in git log --oneline 1.16.0..1.16.3.
- #95 (Anthropic CORS, v1.16.1). Removed
@anthropic-ai/sdk(1.3MB) and rewroteAnthropicClienton Obsidian'srequestUrl. SDK's internalfetchfromapp://obsidian.mdorigin was intermittently blocked by CORS. Prompt caching (cache_control: ephemeral) preserved by emitting the same JSON structure in the raw request body. Streaming is now post-hoc SSE (parseSSEEvents). - PR #87 (lowercase slugs, v1.16.1).
computeSlug()now lowercases output, preventing case-variant duplicate page creation on case-sensitive filesystems. Removed redundant.toLowerCase()calls inmatchExtractedToExistingandconflict-resolver.ts:slugMatchKeys(centralized incomputeSlug). - PR #87 (case-variant detection, v1.16.1). New
caseVariantsignal ingenerateDuplicateCandidatescatches pages with case-colliding titles (e.g.,Unixvsunix). Wired as Tier 1 inlint-controller.ts. - PR #88 (lint false positives, v1.16.1). New
bodyWordSet()withBODY_STOPWORDS(45 English function words) gates sharedLinks duplicate candidates by body-text similarity (threshold ≥ 0.2). 20+ unit tests cover English + CJK edge cases. - PR #88 (dead links slug norm, v1.16.1).
scanDeadLinksnormalizes space→hyphen in target basename before lookup.[[entities/Claude Code]]matchesentities/Claude-Code.md. - Settings UX: drop hardcoded model fallback (v1.16.1). Removed
defaultModelfrom all 12PREDEFINED_PROVIDERSconfigs. Switching providers clearsmodel/availableModels/useCustomModel. - Settings UX: friendly fetch error classification (v1.16.1). New
classifyFetchError()categorizes failures intoAuth/Endpoint/Server/Empty/Networkwith specific Notice per category. - #94 (Lint cancellation, v1.16.2).
AbortSignalnow propagates through all 5 fix-runner functions. All persistent Notices wrapped intry/finallyso they dismiss on cancellation. - #96 (Lint granularity, v1.16.2). LLM analysis step in lint respects the user's
extractionGranularitysetting viaappendGranularityToPrompt. - #99 (Thinking token bleeding, v1.16.2). Three-layer defense: (1) API-level
disableThinkingsendsthinking.type='disabled'uniformly with 400 fallback; (2)parseJsonResponsestrips<think>/<think>before JSON extraction; (3)cleanMarkdownResponsediscards preamble before\n---\nor\n#markers. - #86 (Frontmatter dates, v1.16.2). Root cause was preamble before frontmatter (shared with #99). Fixed by
cleanMarkdownResponseLayer B2 preamble detection. - #103 (Delete empty stubs, v1.16.2). New "Delete empty stubs" button in Lint report modal. Skips
reviewed: truepages. disableThinking?: booleanadded (v1.16.2).OpenAICompatibleClientusesthinking.type='disabled'uniformly. Provider 400 errors trigger automatic fallback retry.- #94 (Lint cancel status bar regression fix, v1.16.3). v1.16.2 wired AbortSignal but LintReportModal still called
this.close()on every fix-button click. Fix gives each fix phase its own lint-operation lifecycle so the status bar persists across fix phases. - #94 (batch count display, v1.16.3). Duplicates-check progress Notice now shows actual inner-batch range (1-4/16) instead of outer round counter (X/4).
- #243 thinkingControlCache key mismatch (v1.16.3). Extracted
getThinkingControlCacheKey()helper so read and write paths inmain.tsuse the same cache key. - #244 deleteEmptyStubs error handling (v1.16.3). Now returns
{deleted, failed, errors}instead of throwing on first failure. - #245 thinkingControlSupported cache after fallback (v1.16.3).
OpenAICompatibleClientsetsthis.thinkingControlSupported = falseafter successful 400-fallback. - #248 isThinkingControlError tightening (v1.16.3). Now requires both HTTP 400 status AND rejected-field/parameter keyword in the message.
- Batch count display in i18n strings (v1.16.3). Replaced 3 hardcoded English progress strings with i18n keys in 8 locales.
- de.ts trailing-comma syntax error (v1.16.3). 6 other language files had same issue — all fixed in lockstep.
- endLintOperation made idempotent (v1.16.3).
- Test rename (v1.16.3, #246): "omits thinking for Gemini" → "sends thinking.type=disabled for Gemini baseUrl".
- 3 releases
- Test count: 488 → 549 (+61 tests, 0 regressions)
- Composition + per-PR detail:
git log --oneline 1.16.0..1.16.3
- LM Studio provider: New dedicated provider option (
PREDEFINED_PROVIDERS.lmstudio). API key is optional — LM Studio runs locally but supports key-based auth. Base URL defaults tohttp://localhost:1234/v1. - Context Window setting: Configurable cap on LLM output tokens to protect local models with limited context (LM Studio 8K, Ollama 4K, etc.). Dropdown options from 4K to 1M. Shown only for local/custom providers (Ollama, LM Studio, custom OpenAI/Anthropic). Sets
maxCapon truncation retry for safety. - Startup quick fixes: Low-level format repairs run automatically on plugin load: sources field normalization, wiki folder structure verification. Default ON. Detailed 10s Notice with cleanup stats + disable hint.
- Sources field normalization (Issue #81): 4 new pure functions in
src/core/sources-normalizer.tshandle 6 real-world pollution patterns reported by DocTpoint (external paths,.mdsuffixes, alias pipes, duplicates, inline arrays, empty[[]]links). 22 tests covering both inline and multi-line formats. - Lint integration:
fixPollutedSourcesruns in lint section 0.5, normalizes all wiki files before LLM-dependent phases. Reports "Sources normalized" section in lint output. - TDD shell test documentation: Mandatory test quality rules added to CLAUDE.md — cover all production paths, assert content mutation (not just return values), re-scan for idempotency verification.
- Issue #81: YAML
sources:field generated 3+ inconsistent formats (external paths,.md,\|alias) from different code paths. Root cause:wiki-engine.ts:646passedfile.pathto{{source_file}}, andutils.ts:518normalizeSourcePathonly stripped[[]]. Fix: unifiednormalizeSourcePathwith external-path remapping + full frontmatter rewrite. - Issue #75: LM Studio HTTP 400 on batch 2+ —
source-analyzer.ts:113had local shadowMAX_TOKENS = 16000that bypassed centralizedMAX_TOKENS_BATCH. Replaced withMAX_TOKENS_BATCH. Plus newcapMaxTokens()pure function andmaxTokensPerCallsetting to cap output explicitly. - Issue #76:
TOKENS_DEDUP_RESOLUTION=300caused "empty JSON" with thinking models where reasoning consumed the budget, thenstripThinkingTokensremoved it leaving zero JSON. Fixed: 300→1000 (insurance). AlsoTOKENS_QUERY_SAVE_DEDUP: 150→300. - Dead code: Removed
TOKENS_PAGE_MERGEandTOKENS_RELATED_UPDATE(zero callers). RemovedpromptIncludesConstraints. - Alias language: Replaced hardcoded Chinese↔English translation rules with English-as-linker-language + "do NOT invent established technical translations" rule. 4 examples (Transformer/Vitamin B2/RoPE/Neural Network) prevent LLM outputting non-existent translations like "变换器" for Transformer.
- withTruncationRetry retry cap: Previously used
MAX_TOKENS_BATCH(16000) unconditionally, causing retry HTTP 400 on local 8K models. Now respectsmaxTokensPerCallsetting asmaxCap.
- Settings UX redesign: New "LLM-Wiki Status" section with inline status indicators. "LLM Provider Configuration" → "LLM Configuration". "Wiki Folder Configuration" → "Wiki Configuration". LLM Concurrency and Batch Delay moved to LLM Configuration section. Startup Quick Fixes toggle moved to first item in Auto Maintenance. Status prefix "LLM Client Status:" removed.
- Provider dropdown i18n: Non-Chinese languages now display English provider names (international technical convention) instead of falling back to Chinese.
- CLAUDE.md: TDD section evolved with mandatory test quality rules, TDD shell failure example, and debug template for "stuck counter" symptoms.
- Dead constants:
TOKENS_PAGE_MERGE,TOKENS_RELATED_UPDATE - Dead function:
promptIncludesConstraints - Shadow constant:
source-analyzer.ts:113localMAX_TOKENS = 16000 - Redundant "LLM Client Status:" prefix from status indicator
- Wiki auto-initialization UX (Issue #80): Wiki structure auto-creates on first successful LLM connection — no more "Generate Default Schema" button doing nothing on empty vaults. Settings panel shows real-time wiki init status (✅/
⚠️ ). saveSummaryi18n: Query-to-Wiki save dialog now uses localized summary strings across all 8 languages instead of hardcoded English/Chinese.
- Issue #80: Empty vault → "Generate Default Schema" button silently failed because
schema/folder didn't exist. Now auto-creates via defensivecreateFolder(). - withRetry nesting: Removed nested
withRetryin truncation retry paths — reduced from max 9 calls to max 3 per client. OuterwithRetryhandles all network errors.
- Core architecture: Extracted 2 new pure function modules to
src/core/:sse-parser.ts— shared SSE event parser for streaming responses (Anthropic + OpenAI formats)truncation-retry.ts— shared token truncation retry policy (3 clients → 1 helper)
- DRY fix: Extracted
isWikiInitialized()from duplicate code insettings.ts. - Dead code cleanup:
promptIncludesConstraints(zero callers) removed;foundAliasesArray.isArray check simplified. - Constants:
PAGES_CACHE_TTL_MScentralized. - Test infrastructure: +37 tests (446 total across 21 files), covering SSE parsing, AnthropicClient truncation, wiki initialization.
- Model compatibility expansion: DeepSeek-R1, QwQ (reasoning models), and LM Studio now fully supported. Think token stripping (Issue #64) removes ` Schweizer
/blocks from reasoning model outputs. LM Studio compatibility fix (Issue #65) removes unsupportedresponse_format: json_object` parameter.
- Test infrastructure expansion: Mock infrastructure (
createMockContext,createMockFile) enables unit testing of core engine modules without Obsidian runtime. Total tests increased from ~200 to 400 (+200 tests), covering previously untestable core logic.
-
TypeScript type safety complete: Fixed 8 type errors in
page-factory-core.test.ts(interface completeness, null checks, parameter types). Project achieves TypeScript strict mode compliance. -
Query engine stability: Page content loading capped at 3000 tokens (MAX_PAGE_CONTENT_CHARS) to prevent token overflow in
loadRelevantPages. -
Dual Gate Verification Mechanism: Upgraded quality gates to require both ESLint and TypeScript passing (0 errors + 0 warnings each). ESLint alone is insufficient for type safety.
-
Core architecture refactoring: Extracted 4 pure function modules to
src/core/directory:conflict-resolver.ts— zero-IO conflict detectiondead-link-detector.ts— dead link identificationorphan-matcher.ts— orphan page matchingprompt-builders.ts— prompt template builders
-
Constants centralization: Centralized 30+ scattered magic numbers into
src/constants.ts(192 lines). Activated semantic constants: WIKI_SUBFOLDERS, notice durations, token budgets, retry parameters. -
lint-fixes.ts refactoring: Extracted pure logic to core modules, reduced file complexity (~180 lines removed).
-
Documentation upgrades:
- TDD Standard: "write failing test first, then implementation"
- Development Protocol: "plan first, then execute"
- ROADMAP architecture quality upgrade plan
- Dual Gate Verification documentation (ESLint + TypeScript both required)
-
Code quality: 2576 lines added, 503 lines removed across 44 files. Zero side effects, zero breaking changes, backward-compatible refactorings.
- Extraction aliases seeding: Entity and concept extraction now supports
aliasesfield (optional). Pre-generated aliases serve as seeds for page generation and act as signals in multi-round extraction to prevent duplicate extractions. Contributed by @Indexed-Apogrypha (PR #61) and @green-dalii (PR #67). - Multi-round extraction context: Non-first extraction rounds now receive a list of already-extracted names and aliases, enabling the LLM to reliably avoid duplicates even on small/local models that struggle to maintain session state.
- Source analysis false abort (#61): First batch gate changed from
||to&&— only aborts when BOTH entities and concepts are absent. Previously a glossary source (entities only, no concepts) would incorrectly abort. Contributed by @Indexed-Apogrypha (Matthew Harper). - Hidden TypeError on non-array LLM output:
normalizeBatchResponseuses typedcoerceToArrayto handle models returningentities: true(or similar non-array truthy values), preventingTypeErrorin downstream.filter()calls. - Alias self-pointing duplication:
appendAliasesnow skips aliases that equal the page's own filename, preventing redundant self-pointing frontmatter entries on cross-type collisions.
- NormalizeBatchResponse pure function: Extracted 8 scattered
|| []fallbacks into a centralized pure function withBatchValidityenum (unusable/empty/valid), improving testability and fixing edge case handling. - Prompt task 0 clarified: Separated "field round restrictions" from "content requirements" — each is now an independent task item with front-loaded scope markers.
- Generation prompt receives aliases seeds: Page creation template now includes
{{extraction_aliases}}field, enabling the LLM to build on pre-extracted alias suggestions. - Three-No Principle structured: Replaced abstract manual-check descriptions with actionable evaluation procedures (call-site audit, data flow trace, state mutation analysis, breaking-change matrix).
- Official blog links added: All 8 READMEs now include links to the official blog (CHN:
/zh/blog/, others:/blog/).
Super-aggregated per Keep a Changelog spec. 3 PATCH releases (v1.12.1 + v1.12.5 + v1.12.6) over 3 days. Per-PR detail preserved in git log --oneline 1.12.0..1.12.6.
- Query modal auto-save prompt disabled (v1.12.1). Closing the Query window no longer triggers LLM evaluation and SuggestSaveModal prompt.
- Lint status bar text corrected (v1.12.1). Status bar now shows "Linting... click to cancel" instead of "Ingesting... click to cancel" during lint operations.
- Notice toast i18n completed (v1.12.1). All remaining hardcoded English notices converted to i18n (
mdOnlyFile,lintPollutedFixed,regenerateIndexCompleted,operationFailed). 8-language coverage. packageManagerfield added (v1.12.1). Added topackage.jsonfor unambiguous pnpm usage.- 4 lint scanner functions extracted & tested (v1.12.1).
buildKnownTargets,scanDeadLinks,scanOrphans,detectAliasDeficiencyextracted tosrc/wiki/lint/scanners.tswith zero Obsidian dependencies. 15 unit tests. - PageFactory error context (v1.12.1).
createNewPage,mergePage,appendToReviewedPagenow wrap errors with entity name and operation type. - Privacy & Transparency sections added (v1.12.1). All 8 READMEs gained localized Privacy & Security + Transparency & Compliance sections. Obsidian score updated to 95/100.
- Branch protection workflow documented (v1.12.1). In CLAUDE.md and memory.
- #54 Cross-folder entity/concept duplicates prevented (v1.12.5).
resolvePagePath()now checks opposite folder (entities ↔ concepts) when same-type matching fails. Cross-type collision merges new content into existing opposite-type page and appends name as alias. No more duplicate pages for same topic. Contributed by @dmarchevsky. - Historical cross-type duplicate detection in Fast path 1 (v1.12.5). When same-type exact slug match hits, opposite folder is also checked. Existing historical duplicates get alias-bridged + warning logged.
- IngestReportModal displays collisions (v1.12.5). Cross-type collisions section added to batch report.
- Redundant I/O eliminated (v1.12.5). Cross-type collision detection uses in-memory path matching from
allPagesinstead of additionaltryReadFile()call. - Type-safe i18n access (v1.12.5). Added
getText()helper — replaces 13 instances ofas unknown as Record<string, string>across 6 files. 8 unit tests added. - README Usage section (v1.12.5). Added sidebar button ingestion method to all 8 language variants.
- Build verification failure fixed (v1.12.6). CI workflow switched from
pnpm install + pnpm buildtonpm install --legacy-peer-deps + npm run buildto match Obsidian's verification system exactly. - Dependency version pinning (v1.12.6). All deps use exact versions (no
^orlatest) to prevent lockfile drift betweenpnpm-lock.yamlandpackage-lock.json. - CI Node version (v1.12.6). Updated from
24.xto22.xfor stability.
- 3 releases
- Test count: 148 → 173 (+25 tests)
- Composition + per-PR detail:
git log --oneline 1.12.0..1.12.6
- Extraction prompt rearchitected: Full page list removed from prompt. Extraction speed independent of wiki size (~80% faster).
- Dynamic batch limits + convergence detection: Short content finishes in 1–2 batches. Low-yield batches terminate early.
- Short-content auto-downgrade: Sources <20K chars cap maxTotalItems proportionally.
- Deterministic related_pages matching:
matchExtractedToExisting()uses slug + alias matching — zero LLM cost. - build:dev command: One-shot dev build with debug output preserved.
- Silent slug operations: Eliminates ~30K lines of debug output per ingestion.
- esbuild upgraded: 0.17.3 → 0.28.0 (dev-server vulnerability fixed).
- Production build suppresses console.debug: Clean logs in production.
- Granularity ≤ notation: 8 languages synchronized.
- 140 tests across 3 test files (+27 since v1.11.0).
- Hexagonal Architecture — over-engineering for Obsidian plugin
- Vector search (Ollama embeddings) — <1% of users have this
- Hash-bucket dedup optimization — no user-reported perf issue
- page-factory try/catch completion — exceptions handled at wiki-engine level
- API URL validation — Obsidian's requestUrl already validates
- llmReady gating (#42): New users must complete Provider → API Key → Fetch Models → Test Connection before core features unlock.
- Cancel ingestion mid-run (#43):
AbortControllerwith batch boundary checkpoints. - Ribbon icon + ingest current file (#44): One-click ingest of active editor tab.
- Lint double-nested link auto-fix: Programmatic detection across all wiki files, zero LLM cost.
- Opposite-directory stubs (#40): Slug-equivalence matching in stub safety nets.
- Extraction prompt rewrite (#34): Graph-centric "wiki-link test". Bibliographic references excluded.
mentions_in_sourcefiltering (#39): Capped at 500 chars.- 529 Overload retry (#41): All clients cover overload keyword.
- PageFactory refactoring: 8 methods → 4 generic (563→424 lines, -25%).
- LLM client retry extraction: Shared
withRetry<T>helper. - 113 unit tests via vitest.
- #37 Double-nested wiki-links: Three-layer defense.
- #38 Anthropic prompt caching: Evaluated & rejected — system prompts too small for cache threshold.
- Aliases support (#30/#31): EntityInfo/ConceptInfo.aliases? for cross-language dedup.
- Minimal + Custom granularity: 5 levels (Minimal/Coarse/Standard/Fine/Custom).
- Slug normalization in resolvePagePath (#32): Fast path 2 checks title + aliases.
- Custom granularity per-type limits ignored (v1.10.2): In custom mode, entity and concept limits now enforced separately.
- Numeric inputs accepting text (v1.10.0): Custom limit and conversation history inputs now restricted to numbers.
- Aliases omitted in duplicate detection (#30): analyzeSource and resolveEntityDedup now include aliases.
- Pollution defense system (4-layer): Write gate → index purification → stub sanitization → detection & repair.
- "Fix polluted pages" in Lint report: One-click repair.
- Missing aliases section in Lint report: Lists each page individually.
- Long source ingestion notice: Files >1000 lines trigger persistent Notice.
renderComponentmemory leak in QueryModal: Fixed dangling component.createMessageStreamlanguage type: 3 client implementations now accept 8 languages.- Missing i18n keys in zh.ts: Added
lintNoIssuesFoundandlintContradictionOpen. - Batch delay default: 300ms → 500ms.
- Full i18n for 8 languages: 269+ UI fields. English, Chinese, Japanese, Korean, German, French, Spanish, Portuguese.
- Dynamic download badge: Real-time counts from Obsidian's community-plugin-stats.json.
- Complete badge suite: 8 standardized badges across all READMEs.
- Rate limit detection: HTTP 429 errors trigger actionable suggestions.
- Smart Fix All completion modal: Per-phase results report.
- Single-value aliases crash: YAML frontmatter with
aliases: single-valuenow normalized. - README command accuracy: Usage table corrected across all 8 language READMEs.
- Quality Milestone (v1.7.0): Content truncation protection, lint/command i18n, batch reports.
- Multi-source merge (v1.7.2): Programmatic frontmatter + LLM intelligent fusion.
- Ingestion acceleration (v1.7.3): Configurable 1–5 concurrent page generation.
- Parallelization + path fixes (v1.7.6): Related page update parallelization.
- Save-to-wiki quality (v1.7.7): Smart batch skip, plugin ID rename
llm-wiki→karpathywiki. - Supply chain security (v1.7.9): GitHub artifact attestations.
- Knowledge dedup + error resilience (v1.7.10): 5xx retry, persistent notices.
- Mandatory page aliases (v1.7.11): Alias deficiency detection, "Complete aliases" button.
- README i18n (8 languages) (v1.7.13): Provider-aware model filtering, alias-aware index.
- Query modal overhaul (v1.7.14): Cmd+Enter to send, Stop button, Copy button, auto-scroll.
- Lint UI freeze fix (v1.7.15/17): Async yield points every 50 pages and 500 comparisons.
- Pollution fix (v1.7.18/20): Folder name leakage defense layer, alias convergence.
- Lint modular refactoring (v1.7.19): Split monolithic files into 4 focused modules.
- #37 Double-nested wiki-links: Three-layer defense.
- #40 Opposite-directory stubs: Slug-equivalence matching.
- #43 Cancel ingestion mid-run:
AbortController+ batch checkpoints. - #14 OpenRouter/Ollama model filtering: Provider-aware smart filter.
- Wiki Output Language (8 languages): English LLM prompts with language directive.
- Iterative batch extraction: Adaptive batch sizing, JSON output enforcement.
- Dual-layer JSON parsing: Robust error recovery.
- Query-to-Wiki feedback: Contradiction state machine, conversational ingest.
- Schema layer: Auto-maintenance, modular architecture.
- v1.4.0 (2026-04-29): Schema layer, auto-maintenance, ESLint compliance
- v1.3.0 (2026-04-28): Modular architecture refactor
- v1.2.0 (2026-04-27): Bidirectional links, entity/concept extraction
- v1.0.0 (2026-04-26): Multi-page generation, foundational architecture
- Initial plugin development and concept validation.