perplexed

Maintenance pass: dependency refresh + streaming-citations bug fix

We are maintained. Updated all packages, solved for dependabot flags, and fixed a streaming-citations bug.

Why Care?

This is the first changelog the Perplexed plugin has ever had. The project went from init (7e6831c) through 47 commits without one, and the last touched commit before today (a13b4e2 Update styles.css, 2025-05-01) left the working tree in a state where:

  • pnpm-lock.yaml was out of sync with what node_modules had been installed by; running pnpm outdated reported every package as "missing".

  • Every direct dependency had drifted at least one minor behind current latest, and three were a full major behind: ESLint (9 → 10), TypeScript (5 → 6), and @types/node (24 → 25).

  • A latent bug in handleStreamingResponse was silently dropping Perplexity's search_results / citations metadata on the final SSE chunk, so users saw plenty of streamed answer text but no reference list at the end.

The plugin was "working as is" for the user's day-to-day usage — they weren't blocked. The maintenance window opened because the toolchain drift had reached the point where any future bug fix would have to fight an outdated lockfile and a yellow-triangle-littered npm advisory list before it could even start. So: refresh the toolchain, lock in working configs against the new majors, and fix anything the refresh exposed.

The strategic point: the two tracks are coupled by intent but independent in scope. Track 1 (deps) has to be safe — every step verified against pnpm run build so the production bundle survives. Track 2 (citations bug) was discovered in the same session because the user reported it after the refresh; the bug pre-dates the refresh and is unrelated to any version change. Both ship together so future maintenance has a single, coherent baseline.

What Was Built

Two coordinated tracks: (1) refresh every dependency to current latest, including the two majors that required config migration — ESLint 9 → 10 (flat config) and TypeScript 5 → 6 (deprecated tsconfig options); (2) fix a streaming-response bug where a misnamed 'stale data' cleanup was wiping captured Perplexity citations on the final SSE chunk, so the rendered note never got a Citations reference section. Build verified clean (tsc + esbuild production). One known-flaky behavior — streaming responses occasionally truncate mid-answer with no error in the console — is documented under Open Items but not diagnosed; the user explicitly deferred ('something is funky I don't want to diagnose, or it's an api or credit thing')."

Dependency refresh (Track 1)

PackageWasNowMajor?
@modelcontextprotocol/sdk1.15.01.29.0minor
dotenv17.2.017.4.2patch
fastify5.4.05.8.5minor
zod4.0.04.4.2minor
@types/node24.0.1225.6.0major
@typescript-eslint/eslint-plugin8.36.08.59.1minor
@typescript-eslint/parser8.36.08.59.1minor
builtin-modules5.0.05.1.0minor
esbuild0.25.60.28.0minor (0.x)
eslint9.30.110.3.0major
obsidian1.8.71.12.3minor
tslib2.8.12.8.1unchanged
typescript5.8.36.0.3major
typescript-eslint (added)8.59.1new
@eslint/js (added)10.0.1new
globals (added)17.6.0new

Method: install the existing lockfile baseline first (pnpm install), confirm pnpm run build is green, then bump in three bands so any breakage is isolated:

  1. All minor / patch bumps in one shot.

  2. @types/node 24 → 25 (types-only, lowest blast radius).

  3. eslint 9 → 10 (config migration required).

  4. typescript 5 → 6 (tsconfig migration required).

Build re-verified after each band.

ESLint v10 — flat config migration (Track 1, fix-out)

ESLint 10 dropped legacy .eslintrc and .eslintignore support entirely. Running eslint with the old config produced:

ESLint couldn't find an eslint.config.(js|mjs|cjs) file.
From ESLint v9.0.0, the default configuration file is now eslint.config.js.

Migration:

  • Created eslint.config.mjs — the new flat-config equivalent of the old .eslintrc. Same parserOptions (ecmaVersion: latest, sourceType: module, project: ./tsconfig.json), same rule set (@typescript-eslint/no-unused-vars, consistent-type-imports, no-explicit-any as warn, etc.), same ignores (node_modules/, main.js).

  • Adopted the unified typescript-eslint meta-package (v8.59.1) and added @eslint/js + globals because the flat-config style composes configs as imported objects rather than string extends.

  • Deleted .eslintrc and .eslintignore.

Important constraint: the build script does not run ESLint (tsc -noEmit -skipLibCheck && node esbuild.config.mjs production), so pnpm run build would have stayed green even if lint were broken. Verified the new config separately by running npx eslint main.ts src/ and confirming it parses + emits results. The 42 errors / 33 warnings it emits are pre-existing code-quality nits (mostly type-only-import refactors and any warnings); not part of this maintenance pass.

TypeScript 6 — tsconfig modernization (Track 1, fix-out)

TypeScript 6 promoted two tsconfig.json deprecation warnings to hard errors:

tsconfig.json(4,5): error TS5101: Option 'baseUrl' is deprecated and will stop functioning in TypeScript 7.0.
tsconfig.json(7,25): error TS5107: Option 'moduleResolution=node10' is deprecated and will stop functioning in TypeScript 7.0.

Resolution: modernize rather than silence with ignoreDeprecations: "6.0".

FieldWasNowWhy
baseUrl"."(removed)Only existed to anchor the paths map below; paths was a no-op (see next row) so neither is needed
paths{ "*": ["node_modules/*"] }(removed)This pattern is a no-op restatement of default Node module resolution; deleting both clears the deprecation cleanly
moduleResolution"node" (= node10)"bundler"Correct semantic match for esbuild-as-bundler — preserves the same import behavior the project relies on without the deprecation

Build clean after the change. No source code modifications were required — none of the imports were depending on the legacy baseUrl + paths indirection.

Streaming citations bug fix (Track 2)

Bug: in src/services/perplexityService.ts, the handleStreamingResponse SSE-loop captured Perplexity's response metadata into a local finalResponseData variable as it streamed, but then immediately re-checked the same chunk and cleared the captured value if the chunk was the final one without metadata. Code as it was:

JS
if (parsed.citations || parsed.images || parsed.search_results) {
    finalResponseData = parsed;
}

if (parsed.choices?.[0]?.finish_reason === 'stop' &&
    !parsed.citations && !parsed.images && !parsed.search_results) {
    if (finalResponseData && !finalResponseData.choices?.[0]?.finish_reason) {
        console.log('🧹 Clearing potentially stale finalResponseData');
        finalResponseData = null;
    }
}

Why this is wrong:

  • finalResponseData is a local variable created at the top of handleStreamingResponse (line 609). Its scope is one request. It cannot ever be "stale from a previous request" — there is no previous request to be stale from.

  • Perplexity's normal SSE flow puts search_results / citations in earlier chunks. The final chunk usually carries only choices[0].finish_reason: 'stop' with no metadata. That is exactly the condition the cleanup fires on.

  • The cleanup also requires that the captured chunk doesn't have its own finish_reason — which is normal, because metadata chunks aren't the terminal ones. So the gate is "if metadata came in earlier, clear it now," which is precisely backwards.

Result: every streaming Perplexity request that completed normally discarded its citations before processStreamingMetadata could write them to the editor. Symptom the user reported: "I don't get the citations reference section."

Fix: removed the cleanup branch. Replaced the simple "last-write- wins" capture with a merge so partial metadata across chunks is preserved:

JS
if (parsed.citations || parsed.images || parsed.search_results) {
    finalResponseData = {
        ...(finalResponseData || {}),
        ...parsed,
    };
}

Build clean. Bundle written to main.js for testing in the user's vault.

Verification

  • pnpm run build (production: tsc -noEmit -skipLibCheck && node esbuild.config.mjs production) green after each of the four dependency bands and after the citations fix.

  • ESLint flat config verified runnable (npx eslint main.ts src/) — emits warnings on pre-existing code, but loads and parses the config without error.

  • Production bundle dropped from ~492 KB to ~84 KB. This is not a regression: the previous artifact on disk was a dev build with inline source maps (!isProduction ? 'inline' : false in esbuild.config.mjs:50), and the new one is a true production minified build with sourcemaps disabled. Bundle contents spot-checked (grep getReader|TextDecoder|reader.read main.js) to confirm streaming primitives are intact.

What Changed in Approach (the meta-lesson)

Pattern this rejectsPattern this adopts
Bump every dependency in one go and hope the build survivesBump in bands by blast radius (minors → types-only majors → config-affecting majors), verify build after each band, isolate any breakage to one band
Silence TypeScript deprecation warnings with ignoreDeprecationsModernize the offending config — TS deprecations are early-warning for TS7 hard removals, not noise to mute
Treat .eslintrc as the canonical config until it physically stops workingMigrate to flat config the moment the major bump requires it; flat config is now the only forward-compatible shape
Pre-emptive "stale data" cleanup gated on heuristics about chunk shapeTrust local-scope variables to be local; if a guard isn't justified by an actual cross-request risk, it's just a foot-gun — delete it
No changelog because the project has never had oneFirst changelog establishes the habit; even a small maintenance pass gets one so the next pass has a baseline to diff against

The generalizable point: a maintenance pass is not just pnpm update --latest. The two majors here both required real config work (ESLint flat config, TS 6 tsconfig modernization), and the session also surfaced one runtime bug that was unrelated to the refresh but worth fixing in the same window. Bundling all of that into one changelog establishes the baseline; the next maintenance pass can diff cleanly against this one.

Open Items

  • Streaming truncates mid-answer, intermittently, with no console error. User reported it during this session; first prompt truncated, second prompt succeeded. No Streaming Error: notice was raised, no red console output, the loop just stops writing. Working hypothesis (unverified): either Perplexity's server is closing the SSE connection early under some condition (rate limiting, account / credit state, request shape) or Obsidian's Electron fetch is closing the underlying socket on idle. The streaming code in handleStreamingResponse has no AbortController and no timeout, so the truncation is not client-initiated. User explicitly deferred diagnosis: "something is funky I don't want to diagnose, or it's an api or credit thing." Defensive fix to consider next pass: add an AbortController with a generous timeout (5+ min for sonar-deep-research) so the failure mode at least surfaces a clear error instead of silent truncation.

  • TextDecoder is recreated every chunk in handleStreamingResponse. Each iteration runs new TextDecoder().decode(value, { stream: true }). The stream: true flag is meant to carry partial multi-byte UTF-8 state across chunks, but a fresh decoder per chunk discards that state every time — so multi-byte characters split across a chunk boundary will be replaced with U+FFFD and the resulting JSON may fail to parse (caught silently in the per-line try/catch and logged as a warning). Pre-existing; not surfaced by this pass. Fix: hoist the decoder outside the while (true) loop. Held for a separate pass so this changelog stays scoped.

  • Three runtime dependencies are declared but never imported. fastify, @modelcontextprotocol/sdk, and zod are all in dependencies in package.json but not referenced by main.ts or any file in src/. Likely legacy from earlier scaffolding. Removing them would shrink the install footprint and the npm advisory surface area; not done here to keep the maintenance pass scoped to "refresh + fix the bug the user noticed." Worth a follow-up pass.

  • Forty-two pre-existing ESLint errors / 33 warnings. Surfaced for the first time because the flat config migration meant we actually ran lint. Mostly consistent-type-imports (auto- fixable), unused destructured imports in src/types/obsidian.d.ts, one let → const in perplexityService.ts:608, and @typescript-eslint/no-explicit-any warnings throughout. None block the build (tsc + esbuild doesn't run lint). npx eslint . --fix would clear roughly half of them mechanically. Held — separate intent from the maintenance pass.

  • Version not bumped. manifest.json and package.json both still read 0.0.0.1. The plugin's versioning scheme is pre-release / non-semver-conformant; the user did not request a bump in this pass. If the citations fix gets shipped to users, consider bumping then.

  • Commit not created. All changes (config files, package.json, pnpm-lock.yaml, tsconfig.json, perplexityService.ts, eslint.config.mjs, this changelog) are staged in the working tree only. User has not requested a commit yet.

Files Touched

perplexed/
├── package.json                                    (deps refreshed across 13 packages; added typescript-eslint, @eslint/js, globals)
├── pnpm-lock.yaml                                  (regenerated against the new package.json)
├── tsconfig.json                                   (removed baseUrl + paths; moduleResolution node → bundler)
├── eslint.config.mjs                               (created — flat-config equivalent of the deleted .eslintrc)
├── .eslintrc                                       (deleted — superseded by eslint.config.mjs)
├── .eslintignore                                   (deleted — superseded by ignores in eslint.config.mjs)
├── src/
│   └── services/
│       └── perplexityService.ts                    (handleStreamingResponse: removed citations-eating "stale data" cleanup; replaced last-write-wins capture with merge across metadata chunks)
└── changelog/
    └── 2026-05-02_01.md                            (created — this file, first changelog for this plugin)

main.js and styles.css are build artifacts; rebuilt by pnpm run build and not part of the source-of-truth diff.

Reference

  • Predecessor changelogs: None — this is the first.

  • Pattern reference for changelog format: /Users/mpstaton/code/lossless-monorepo/cite-wide/context-v/changelogs/2026-05-02_01.md (cite-wide v0.2.0 release notes — frontmatter shape, section layout, "What Changed in Approach" / "Open Items" / "Files Touched" conventions).

  • The bug-bearing function: src/services/perplexityService.tshandleStreamingResponse (line 594) and processStreamingMetadata (line 713).

  • Build pipeline: package.json scripts.build (tsc -noEmit -skipLibCheck && node esbuild.config.mjs production); esbuild.config.mjs for bundle config (target es2022, format cjs, externals include obsidian, electron, all @codemirror/*, all @lezer/*, plus builtin-modules).

  • ESLint flat-config docs: https://eslint.org/docs/latest/use/configure/migration-guide (referenced during the v10 migration).

  • TypeScript 6 deprecations notice: https://aka.ms/ts6 (referenced during the tsconfig modernization).

  • Recent commits on development (pre-this-session): a13b4e2 Update styles.css, f75afbf Add WARP.md documentation for development workflow, 153894f improve: Deep Research Streaming — this maintenance pass starts from a13b4e2.