Commit Graph
724 Commits
Author SHA1 Message Date
Sergey KozyrenkoandClaude Opus 4.8 0cd37effed refactor: apply prompt/provider code-review findings + unify perf-scope comments
From .cursor/rules/CODE-REVIEW-prompt-provider-commits.md (settings-prompt, code-editor):
- rename abbreviations currentIdx->currentIndex, pos->position
- drop the cycle-ternary restatement comment
- gate the green "go to occurrence" badge affordance to plain view (code view only inserts)
- move code-editor's byte-faithful note into an actionable JSDoc warning on the component

Cross-component uniformity: collapse the four scoped-useWatch wrappers (DiffContent,
VariablesPanelContainer, KnowledgeFormHeader, DeleteProviderDialog) to one consistent
"Don't hoist this useWatch..." warning, dropping the duplicated narration + provenance.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 15:42:08 +07:00
Sergey KozyrenkoandClaude Opus 4.8 62e5afa04d perf(settings/provider): scope provider-name watch out of the main form
The provider `name` field was watched at the top of SettingsProvider but only
fed the delete dialog's itemName, so every keystroke re-rendered the whole form
tree (Accordion + 12 agent configs + resizable panels). Move the watch into a
small DeleteProviderDialog wrapper that subscribes on its own. Measured: typing
10 chars now re-renders SettingsProvider 2x (the one-time isDirty flip) instead
of once per keystroke; the per-keystroke re-renders land on the tiny dialog.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 15:29:12 +07:00
Sergey KozyrenkoandClaude Opus 4.8 50a272501b feat(templates): add byte-faithful code editor view for template content
A Code/Plain toggle in the ⋯ menu swaps the autosize textarea for the
lazy-loaded CodeMirror CodeEditor, mirroring the prompt-template editor, so
flow templates with {{VAR}} placeholders edit byte-faithfully (no markdown
normalization). The toggle is available for new templates too; the editor
nests in the existing InputGroup shell so the in-input save button is shared
across both modes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 15:19:19 +07:00
Sergey KozyrenkoandClaude Opus 4.8 01fa02d3b4 perf(knowledges): scope content subscription off the form + untangle save/guard cycle
Typing in the knowledge markdown editor re-rendered the whole KnowledgeForm on
every keystroke: form.watch('content') — used only to toggle the anonymize
button's disabled state — subscribed the top-level component, dragging the form
body, layout, and metadata fields through a re-render per character. Measured on
a multi-KB document: max input processing 58ms -> 21ms, keystrokes over 25ms
20 -> 0, long tasks 1 -> 0.

- Move the content subscription into a small module-scope KnowledgeFormHeader
  wrapper (scoped useWatch), so only the header reacts to typing; the form body,
  layout, and editor stay put. Mirrors the existing pattern in
  knowledge-form-controls.tsx.
- Removing the watch let the React Compiler optimize KnowledgeForm, which then
  flagged the skipNextBlockRef latest-ref (react-hooks/immutability). Rather than
  suppress it, untangle the performSave<->guard cycle the ref existed to break:
  performSave now returns the result and the *caller* owns navigation. The form
  Save button (defined after the guard) does the CREATE redirect via the stable
  skipNextBlock; the unsaved-changes dialog's "Save and leave" saves and lets the
  guard proceed the navigation the user initiated. No eslint-disable, no memo.

Behavior note: on CREATE, the Save button lands on the new document; the
"Save and leave" dialog now lands on the destination the user was navigating to
(it previously raced between the two). Verified live on the local stack:
create/update via button and dialog, delete on a dirty form, anonymize toggle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 12:58:51 +07:00
Sergey KozyrenkoandClaude Opus 4.8 bb8739072b perf(prompts): scope template subscription to stop per-keystroke full-page re-render
SettingsPrompt called form.watch('template') on both forms at the top level, which
subscribes the whole ~1000-line component to re-render on every keystroke (header,
tabs, panels, meta, all four dialogs). Move the live-template reads into small
useWatch children so only they re-render:

- VariablesPanelContainer wraps the variables panel (needs the live ×N counts).
- DiffContent wraps the diff viewer — live while the dialog is open, and Radix
  unmounts it (and its subscription) when closed.
- handleValidate and the variable-insert callback read form.getValues() at call
  time instead of the watched vars; the diff dialog's derived currentTemplate is
  dropped.

Mirrors the existing knowledge-form-controls.tsx pattern. Verified with render-count
instrumentation that the parent no longer renders per keystroke (only the scoped
children do), and that the live count, variable insert/cycle, validate, diff (live
while open), reset, code-editor insert, and dual-tab dirty flows are unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 21:15:46 +07:00
Sergey KozyrenkoandClaude Opus 4.8 65a037b91a feat(prompts): add byte-faithful code editor view for prompt templates
Prompt templates are Go templates with markdown tables, <xml-tags>, and
significant whitespace — content a markdown (Tiptap) editor normalizes and
corrupts on round-trip. Add a CodeMirror-based view instead, toggled from the
prompt page actions menu, that edits the raw template verbatim.

- New shared CodeEditor (CodeMirror 6 + markdown highlighting, line wrapping,
  theme-aware), lazy-loaded so its chunk only loads when code view is opened.
- "Code editor" / "Plain text" toggle in the prompt actions menu.
- Available-variables click inserts {{.Var}} into the code editor at the caret.
- Fix a pre-existing tsc error in the variable-cycling code
  (noUncheckedIndexedAccess on the matched-occurrence lookup).

Round-trip verified byte-exact end to end: original -> code edit -> plain, and
save -> DB -> reload, all identical to source (tables, tags, whitespace intact).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 19:55:31 +07:00
Sergey KozyrenkoandClaude Opus 4.8 3114d13fcd feat(settings/prompt): cycle through all variable occurrences + show use count
Clicking an in-use variable in "Available variables" jumped only to the first
occurrence. Now it cycles: from the occurrence the caret is on it advances to
the next (wrapping past the last), else jumps to the first at/after the caret.
Each badge also shows a ×N count when the variable is used more than once.

Verified live (vite dev → docker backend) on the adviser prompt: a ×7 variable
steps through all 7 occurrences in order and wraps.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 15:21:02 +07:00
Sergey KozyrenkoandClaude Opus 4.8 a47caa4ff9 refactor(settings): use InputGroup for the model combobox input+trigger
FormModelComboboxItem manually glued an Input and a dropdown Button by
stripping adjacent borders/corners and juggling z-index on focus/hover.
Replace that with the project's InputGroup primitive (single border, ghost
trigger, native group focus-ring) — matching InputPassword and the ~20 other
input-group usages. Also gives the icon-only trigger an accessible name.

Verified live (vite dev against the docker backend): input editable, dropdown
opens via the asChild trigger, option/price autofill intact.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 14:46:22 +07:00
Sergey KozyrenkoandClaude Opus 4.8 a1908536b7 test(providers): extend the price-consistency guard to all 9 providers
anthropic, bedrock, and gemini also carry per-agent prices in config.yml but
were not covered. Adding them confirms no current drift and guards them going
forward. bedrock's loaders take a *config.Config, so they're wrapped with an
empty config to read the embedded catalog.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 13:55:26 +07:00
Sergey KozyrenkoandClaude Opus 4.8 4ecf2bbac1 fix(providers): correct model prices from a full official-source audit
Audited all 133 catalog models across 9 providers against official pricing
pages, each delta independently re-verified. openai/anthropic/gemini/glm/kimi
were already correct (0 changes). Corrections:

- deepseek-v4-pro: 1.74/3.48/0.0145 -> 0.435/0.87/0.003625 (catalog was 4x the
  official DeepSeek rate).
- bedrock mistral-large-3-675b: 4.0/12.0 -> 0.5/1.5 (8x overstatement; old
  Mistral Large 24.07 rate).
- minimax M3 0.6/2.4 -> 0.3/1.2, M2.7 0.4/1.1 -> 0.3/1.2, M2.7-highspeed
  0.4/1.1 -> 0.6/2.4, all +0.06 cache (permanent-50%-off effective rates).
- qwen3.6-35b-a3b: 0.248/1.485 -> 0.375/2.25 (catalog had the China-mainland
  price, not the International endpoint the rest of the catalog uses).
- qwen: add cache_read = 0.20 x input (implicit-cache rule) to 16 open-source
  models that were missing it.

config.yml agent prices synced for deepseek-v4-pro and MiniMax-M3 (guard test
keeps them aligned).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 13:44:59 +07:00
Sergey KozyrenkoandClaude Opus 4.8 dad1be429e feat(providers): declare reasoning efforts for gpt-5.2-codex and gpt-5-pro
Verified per the official OpenAI model pages:
- gpt-5.2-codex: [low, medium, high, xhigh]
- gpt-5-pro: [high] (the page states it "only supports reasoning.effort: high")

Other openai reasoning models (gpt-5, gpt-5.1 = no xhigh; gpt-5-mini/nano,
codex variants, o-series) keep the default — their official pages do not
enumerate an accepted effort set.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 13:44:58 +07:00
Sergey KozyrenkoandClaude Opus 4.8 ebeddf4e76 fix(providers): set qwen cache_read to the documented 20% implicit-cache rate
Alibaba Model Studio's context-cache doc bills cached input tokens at 20% of the
input price (implicit hits, International endpoint). Most of the qwen catalog
already used 20%; align the outliers and guard against future drift.

- qwen3.7-max 1.25 -> 0.5, qwen3.7-plus 0.2 -> 0.08: corrects an earlier change
  that read a "50% discount" off the model-pricing page; the authoritative rate
  is 20%, so the original 0.5 for qwen3.7-max was already correct.
- qwen3.6-{max-preview,plus,flash}, qwen3.5-{plus,flash}: 10% -> 20% stale
  outliers, with matching config.yml agent-price updates.

Add TestAgentConfigPricesMatchCatalog: GetPriceInfoForType returns the agent
price with no catalog fallback, so config.yml drift silently mis-prices cost.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 05:04:13 +07:00
Sergey KozyrenkoandClaude Opus 4.8 2f9e86391e feat(providers): declare reasoning efforts for the remaining xhigh openai models
Verified per the OpenAI model pages (reasoning.effort support):
- gpt-5.4-mini, gpt-5.4-nano, gpt-5.2: [low, medium, high, xhigh]
- gpt-5.2-pro: [medium, high, xhigh] (no low)

gpt-5 / gpt-5.1 keep the default [low, medium, high] (no xhigh; gpt-5 uses
"minimal", not representable in the UI enum). Codex variants are unverified
and left for a follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 05:04:12 +07:00
Sergey KozyrenkoandClaude Opus 4.8 de7db21448 feat(providers): declare reasoning efforts so xhigh/max surface in the UI
The settings effort dropdown is driven by a model's reasoning.efforts and
falls back to [low, medium, high] when absent, so xhigh/max were never
selectable for OpenAI-compatible models even though the backend accepts them.

Declare the verified accepted levels:
- glm-5.2: [high, max] (Z.AI: GLM-5.2 reasoning_effort accepts only high/max,
  so the default low/medium were also wrong for it)
- gpt-5.4: [low, medium, high, xhigh] (OpenAI GPT-5.4/5.5 effort enum)

Other openai/gpt-5.x entries still need per-model effort verification.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 04:39:31 +07:00
Sergey KozyrenkoandClaude Opus 4.8 8bbbdbf7c5 fix(providers): correct qwen3.7-max cache_read to the official rate
cache_read was 0.5; the Alibaba Model Studio official price (International)
is 1.25 per 1M tokens (a 50% discount on the 2.5 input rate). Pre-existing
inaccuracy, unrelated to the qwen3.7-plus addition.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 03:56:04 +07:00
Sergey KozyrenkoandClaude Opus 4.8 b40091d68f fix(providers): correct kimi-k2.7-code and qwen3.7-plus prices to official rates
Replace the placeholder estimates with verified list prices (USD per 1M tokens):
- kimi-k2.7-code: 0.95 / 4.00 / 0.19 cache (platform.kimi.ai official) —
  identical input/output to k2.6, only the cache rate differs.
- qwen3.7-plus: 0.4 / 1.6 / 0.2 cache (Alibaba Model Studio official,
  International base <=256K tier) — the earlier estimate conflated it with
  the qwen3.7-max tier and was ~2.5x too high.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 03:54:31 +07:00
Sergey KozyrenkoandClaude Opus 4.8 f489e13ed4 docs(providers): drop unverifiable superlative from glm-5.2 description
"the only GLM model that honors reasoning_effort" is an unenforceable
prose claim; state the capability the entry actually carries instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 03:47:13 +07:00
Sergey KozyrenkoandClaude Opus 4.8 c3504b1839 test(evidence-receipts): cover unterminated and oversized tail reads
The tail reader's no-trailing-newline branch (a torn/truncated final
append, the case M3's fsync defends) and the over-window error branch
had no coverage. Add cases for a newline-free last line, a torn final
append that must be rejected, and a single line exceeding the 64KiB
window.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 03:47:13 +07:00
Sergey KozyrenkoandClaude Opus 4.8 35c77ae5aa test(providers): scan the whole catalog for adaptive modes in the M7 pin
The M7 pin only checked models actually assigned to an agent type, so an
adaptive reasoning mode declared on an unassigned catalog model would pass
silently. Add a second loop over every catalog model so any openaicompat
provider that declares adaptive/adaptive-only fails loudly — matching the
guard's stated intent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 03:47:12 +07:00
Sergey KozyrenkoandClaude Opus 4.8 83bff62d3b refactor(evidence-receipts): drop change-narration comment on the lock pool
The comment narrated the replaced sync.Map design and defended the
choice; the fixed-stripe array plus FNV indexing is self-evident and
the rationale already lives in commit d12b984.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 03:32:02 +07:00
Sergey KozyrenkoandClaude Sonnet 4.6 2d8bce7ccb feat(providers): add glm-5.2, kimi-k2.7-code, qwen3.7-plus to model catalogs
- glm/models.yml: add glm-5.2 (only GLM model supporting reasoning_effort=high/max)
- kimi/models.yml: add kimi-k2.7-code (code-specialist above k2.6)
- qwen/models.yml: add qwen3.7-plus (cost-efficient tier below qwen3.7-max)

Prices for kimi-k2.7-code and qwen3.7-plus are estimated from adjacent
catalog entries and should be verified against official pricing pages.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 03:11:20 +07:00
Sergey KozyrenkoandClaude Sonnet 4.6 a69e19f751 feat(providers): allow xhigh/max reasoning_effort on the OpenAI-compatible path
The per-agent options builder only emitted WithReasoning for low/medium/high,
and the langchaingo fork clamped xhigh/max to high in GetEffort (treating them
as adaptive-only). OpenAI GPT-5.5 and GLM-5.2 now expose xhigh/max as real
reasoning_effort levels, so that clamp encodes a stale invariant.

Extend the effort switch to pass xhigh/max through; the fork's GetEffort clamp
is removed in the local langchaingo (propagated via vendor) so the OpenAI
transport emits the real level. Adaptive (anthropic/bedrock) and budget paths
are unaffected — GetEffort is called only by the OpenAI transport. Validity is
gated per model by ModelReasoningInfo.Efforts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 02:18:57 +07:00
Sergey KozyrenkoandClaude Sonnet 4.6 40debd150d test(providers): pin openaicompat providers off adaptive thinking (M7)
Adaptive thinking (thinking.type=adaptive + output_config.effort) is an
Anthropic-protocol mode the langchaingo OpenAI transport cannot emit, and
the openaicompat Call path never threads PrepareAdaptiveCallOptions. So an
adaptive reasoning mode declared in an openaicompat provider's models.yml
would silently no-op.

Assert that none of the openaicompat-backed providers (qwen, glm, deepseek,
kimi, minimax) resolve to adaptive thinking for any agent type. The day
someone adds an adaptive model/mode to one of their configs, this fails
loudly instead of dropping the thinking budget in silence.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 00:52:49 +07:00
Sergey KozyrenkoandClaude Sonnet 4.6 4d25cf5c51 perf(evidence-receipts): read only the file tail for the previous hash
record() called readLastEvidenceReceiptHash on every append, which re-read
and re-hashed the entire receipts.jsonl just to get the last hash and verify
the whole chain — O(N^2) over a flow's receipts, on the tool-call hot path
under the per-path lock (M2).

Read only the last line instead (windowed ReadAt from the end), failing
closed if that line is missing, wrong-schema, or hash-mismatched. This is
O(1) per append and stays correct across the multiple recorder instances
that write one path (L6): the file stays the single source of truth, so
there is no per-instance cache to go stale. Full-chain verification belongs
in a separate read-time verify tool, not on every write.

Off by default (EVIDENCE_RECEIPTS_ENABLED=false). Measured: 1024 concurrent
appends under -race dropped from ~49s to ~7.5s.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 21:43:19 +07:00
Sergey KozyrenkoandClaude Sonnet 4.6 4b9e4b4972 fix(evidence-receipts): fsync each receipt append for durability
The append wrote the receipt line and returned without flushing, so a crash
between the write and the OS flushing its page cache could lose or truncate
the last receipt, after which the fail-closed chain check halts all further
writes until the file is repaired (M3). Sync the file before returning.

Off by default (EVIDENCE_RECEIPTS_ENABLED=false), so standard deployments are
unaffected; the cost lands only when the feature is explicitly enabled.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 21:21:07 +07:00
Sergey KozyrenkoandClaude Sonnet 4.6 09383063ac test(evidence-receipts): pin sharded lock pool bounds and concurrency
Three guards for the L6 sharded mutex pool (d12b984):
- bounded: 100k distinct flows resolve to <= 256 mutexes, so reverting to a
  per-path map (the original unbounded leak) fails the test.
- high concurrency: 16 cross-executor writers on one path keep the hash chain
  intact under -race.
- stripe collision: two flows that hash to the same stripe keep their own
  chains intact, confirming shared stripes add contention but not corruption.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 21:00:27 +07:00
Sergey Kozyrenko 124a1e1084 Merge remote-tracking branch 'origin/fix/ram-consumption' into integrate/open-prs 2026-06-25 20:11:59 +07:00
Sergey KozyrenkoandClaude Sonnet 4.6 d12b984631 fix(evidence-receipts): replace per-path sync.Map with sharded mutex pool
A sync.Map holding one *sync.Mutex per flow path grew unbounded for the
whole server uptime. A fixed 256-stripe array keyed by FNV-32a hash of
the path caps memory at a constant cost while preserving per-path append
serialization required by the hash-chain invariant.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 20:10:32 +07:00
Dmitry Ng c543831205 feat(backend): enhance graphiti search tool with langfuse integration
- Added langfuse observability support to the graphiti search tool, allowing for detailed tracking of search operations.
- Introduced a mapping of search types to human-readable titles for better traceability in logs.
- Implemented a new method to build the input payload for langfuse retrievers, encapsulating relevant search parameters.
- Enhanced error handling to include retriever status updates on success and failure, improving observability and debugging capabilities.
2026-06-25 12:29:14 +03:00
Dmitry Ng e909f40b2e fix(backend): implement singleton pattern for anonymizer replacer
- Introduced a process-level singleton for the anonymizer replacer to enhance performance and ensure thread safety.
- The replacer is built once using patterns loaded at startup, allowing it to be shared across all flow executor instances.
- Updated the flow tools executor to utilize the shared replacer, simplifying the creation process and improving efficiency.
2026-06-25 12:28:49 +03:00
Dmitry Ng 9caa6a8775 fix(backend): update LRU cache implementation to use non-expirable version
- Replaced expirable LRU cache with non-expirable lru.Cache in multiple files for improved performance and reduced resource usage.
- Introduced a new function, newSummarizerCache, to create a fixed-size LRU cache for summarizer results, eliminating the need for background goroutines.
- Updated related code to ensure compatibility with the new cache implementation.
2026-06-25 12:28:10 +03:00
Sergey KozyrenkoandClaude Opus 4.8 2d2aea5785 fix(auth): reject OAuth logins with a provider-unverified email
The OAuth callback links/creates an account by the email from ResolveEmail, and
Google/GitHub only ever return a verified address — but that invariant lived
inside each provider and was invisible at the callback. A future OAuth provider
that omitted the verified check would let an attacker register a victim's email
there (unverified), "sign in", and be linked straight into the victim's account:
instant takeover.

Make verification part of the contract: ResolveEmail now returns
(email, verified, err), and the callback refuses to proceed when !verified.
Because Go's bool zero value is false and the compiler forces the new return, a
provider that forgets to report verification fails closed (its own login breaks)
rather than opening a takeover. Google reports claims.EmailVerified; GitHub only
selects verified addresses, so reports true. Added a callback test: a provider
reporting an unverified email is rejected (no link, no session) — red before the
gate, green after.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 14:43:16 +07:00
Sergey KozyrenkoandClaude Opus 4.8 497cfdd555 fix(users): require a real email on user-initiated email change
EmailChange.Mail validated with `vmail`, which accepts not just a real address
but also the literal "admin" and any UUID — escape hatches that exist so the
seeded admin row (mail "admin") passes User.Mail's Valid(). The account's
change-email form (added this branch) reaches it, so a user could save "admin"
or a UUID as their own email — a non-deliverable value (no privilege gain: roles
come from role_id, and UNIQUE(mail) blocks colliding with the real admin row).

Add a strict `realemail` validator (same address regex as vmail, minus the
hatches) and use it for EmailChange.Mail; User.Mail keeps `vmail` so the seeded
admin still validates. Tests cover the validator (admin/UUID rejected, real
address accepted) and the handler (changing to a UUID or "admin" now 400s).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 13:00:36 +07:00
Sergey KozyrenkoandClaude Opus 4.8 14e0a0ae71 fix(users): preserve email case on change to match login, OAuth and create
ChangeEmailCurrentUser was the only path that lowercased the email
(strings.ToLower), while local login, the OAuth callback and CreateUser all
store/compare it raw against a case-sensitive UNIQUE(mail). That lone
normalization let a changed address (now lowercased) miss a later raw-case
login, and let the uniqueness pre-check (run on the lowercased value) skip an
existing mixed-case row. Drop the ToLower so every path is consistently
case-sensitive again — the pre-branch invariant. The email validator already
rejects surrounding whitespace, so the paired TrimSpace was dead.

Update the test that codified the old lowercasing to assert case preservation.
A fully case-insensitive scheme (normalize everywhere, or citext /
UNIQUE(lower(mail))) is tracked separately as a follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 10:15:44 +07:00
Sergey Kozyrenko 14b3da2d6f fixup! fix(detail-navigation): re-seed the sheet search mirror from the controller on open 2026-06-25 09:56:14 +07:00
Sergey Kozyrenko ac32a8e996 fixup! fix(providers): don't crash startup when a disabled provider's external config is unreadable 2026-06-25 09:56:14 +07:00
Sergey KozyrenkoandClaude Opus 4.8 f441ec1c4d fix(providers): don't crash startup when a disabled provider's external config is unreadable
NewProviderController loaded every provider's default config (NewConfig) in an
ungated loop that treated any error as fatal, while actual provider construction
(New) is correctly gated on Enabled. A disabled provider pointed at an unreadable
BEDROCK_CONFIG_PATH (or the equivalent custom/ollama external config) therefore
aborted startup, even though that provider was never going to be used.

Extract the config-loading loop into buildDefaultConfigs and gate the failure on
Enabled: a disabled provider whose external config can't be read is logged and
skipped; an enabled provider still fails loudly. Tests cover both paths
(disabled + bad path non-fatal, enabled + bad path fatal).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 09:26:00 +07:00
Sergey KozyrenkoandClaude Opus 4.8 8ca83137a1 fix(detail-navigation): re-seed the sheet search mirror from the controller on open
DetailNavigationSheet keeps a local input mirror (localQuery) seeded once from
controller.searchQuery — a deliberate perf decouple (92a9e59) so typing in the
sheet doesn't re-render the whole detail page. But in the controller's documented
controlled mode (a page-level search box that owns searchQuery), an external
change made while the sheet was closed left the mirror stale: reopening the sheet
showed the old text.

Re-seed the mirror from the controller on each open, folded into the existing
open-transition reconciliation. The sheet is modal, so searchQuery can only
change externally while closed — on-open re-seeding suffices and never clobbers
in-progress typing. Add a controlled-mode test that reproduces the stale input
(red before, green after).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 09:16:26 +07:00
Sergey KozyrenkoandClaude Opus 4.8 47c1ea108b fix(report-pdf): drop dead CJK font ternary, document the Hangul gap, add tests
renderTextWithCJK selected the CJK font via `bold ? 'NotoSansSC' : 'NotoSansSC'`
— both branches identical, so the ternary was dead. Collapse it to the single
family. The CJK detection regex matches Han, kana, Hangul and Bopomofo, but the
only registered CJK font (NotoSansSC) lacks Hangul — verified with fontkit — so
Korean renders as missing-glyph boxes; document that limitation and the
NotoSansKR fix path.

splitByCJK and renderTextWithCJK had no tests; add a suite covering segmentation
(mixed Latin/CJK, kana, hangul, CJK punctuation, empty input) and per-segment
font assignment.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 09:03:53 +07:00
Sergey KozyrenkoandClaude Opus 4.8 d440eafea9 test(detail-navigation): cover the virtualized sheet branch (>100 items)
The DetailNavigationSheet virtualized branch (items.length > 100) was never
exercised — the fixture had 4 items, always below the threshold. Add a 150-item
suite covering the virtualized path: a windowed subset of options, the roving
tabIndex on the current option, and in-window ArrowDown navigation. The
@tanstack virtualizer can't measure a viewport under jsdom, so the hook is mocked
to a fixed window (the windowing math is @tanstack's own coverage).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 08:43:14 +07:00
Sergey KozyrenkoandClaude Opus 4.8 28377ab4a4 fix(webui): reset price/reasoning when a model name is typed, not just picked
The provider model field is an Input + dropdown combobox. Picking from the
dropdown (or "Use as custom") fires onOptionSelect, which resets price/reasoning
to the new model's catalog values — but typing the model name only updated the
field, leaving stale price/reasoning from the previously-selected model. Fire
onOptionSelect from the Input's onChange when the typed value exactly matches a
catalog option, so a typed known model behaves like a picked one.

Live-verified: typing us.anthropic.claude-sonnet-4-5-... into an agent on
gpt-oss reset Input Price 0.15->3, Output 0.6->15, cache 0->0.3/3.75.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 08:35:59 +07:00
Sergey KozyrenkoandClaude Opus 4.8 49af50d4ab refactor(webui): apply Apollo Client v4 review fixes to apollo.ts
From a doc-grounded review against @apollo/client 4.2.3 + graphql-ws 6 (all
findings low/info, adversarially verified):

- streaming link: fold the throttle timestamp into the LRU StreamingLogEntry so
  it is evicted with its log — removes the unbounded lastUpdateTimestamps Map
  that leaked for streams that never receive a closing (non-appendPart) message.
- errorLink: also dispatch auth:refresh on a ServerParseError 401/403 (the narrow
  application/graphql-response+json path that ServerError.is() does not match).
- WS retryWait: add jitter so a mass reconnect (e.g. backend restart) does not
  thundering-herd the server.
- cache: log instead of silently swallowing a storeFieldName parse failure in
  matchesCacheVariant, so a future Apollo serialization change is observable.
- v4 deprecations: onError -> new ErrorLink, createHttpLink -> new HttpLink,
  FetchResult -> ApolloLink.Result, Operation -> ApolloLink.Operation.

Verified: tsc -b + vite build, 604 vitest pass, and a live smoke on the docker
stand (queries + subscription WS connect, no auth/JS errors).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 07:48:55 +07:00
Sergey KozyrenkoandClaude Opus 4.8 ec608d7091 fix(webui): redirect to login when the GraphQL subscription WS auth fails
The subscription WebSocket retries forever on a 403 handshake (expired/invalid
cookie), but its error handler only escalated to auth:refresh when the error
*message* contained "403"/"auth required" — and browsers never expose the
handshake HTTP status on a WebSocket error event, so that detection never fired.
An idle page with active subscriptions therefore looped on 403 with no redirect
to login (the redirect only fired once a REST/axios call hit a 403). Dispatch
auth:refresh unconditionally on any WS error and let /info decide: it redirects
on a real 401/403 and is a no-op while the session holds. Live-verified both
ways: an invalid cookie now redirects to login from an idle page, and a
transient WS drop on a valid session keeps the user logged in. Pre-existing (the
WS handler is unchanged from main); surfaced by a tester.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 06:45:06 +07:00
Sergey KozyrenkoandClaude Opus 4.8 3d447c582f ci: gate frontend install on frozen-lockfile drift
The "Frontend - Install dependencies" step ran pnpm install --frozen-lockfile
with continue-on-error: true, swallowing the ERR_PNPM_OUTDATED_LOCKFILE that the
flag exists to raise. The rest of the frontend pipeline (prettier, lint,
type-check, test) already gates strictly, so a stale pnpm-lock.yaml would only
surface later as a confusing downstream "command not found". Drop the flag so
lockfile drift fails loudly at the install step. Backend steps left as-is.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 10:44:31 +07:00
Sergey KozyrenkoandClaude Opus 4.8 7867a73a6a test(providers): guard providerRegistry against AllProviderTypes drift
A type present in only one of the two hand-maintained lists fails silently:
accepted by the API then erroring "unknown provider type" at construction, or
rejected 422 despite working. Assert set-equality and canonical names so drift
fails in CI instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 09:49:00 +07:00
Sergey KozyrenkoandClaude Opus 4.8 2f3df495fa test(pconfig): cover the adaptive-thinking backstop (refs #288)
UsesAdaptiveThinking/PrepareAdaptiveCallOptions had no test coverage after the
native-langchaingo migration removed the old transport-layer tests. Add table
tests: an adaptive-only model forces adaptive over an agent's budget choice or an
absent reasoning block, while budget-only models stay untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 09:49:00 +07:00
Sergey KozyrenkoandClaude Opus 4.8 9e8ee58786 fix(resources,flowfiles): propagate zip Close() error instead of swallowing it
The four Zip* builders deferred zw.Close() with an unnamed return, dropping the
error from the central-directory flush. After the resources download moved to
direct socket streaming, a dropped connection during that final flush could
return a truncated archive under HTTP 200. Capture Close() via a named return so
streamZipArchive logs and aborts the partial response. Regression tests in both
packages drive the Close() error path with a failing writer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 09:48:45 +07:00
Sergey KozyrenkoandClaude Opus 4.8 b8151c95ba fix(frontend): resolve review nits in settings (toast re-fire, chip a11y, dead export)
- settings-provider: handleTest/handleTestAgent/onSaveAndLeave now reset
  submitError before re-validating. The error toast is driven by a
  submitError-change effect, so a repeated identical validation failure
  previously set the same string and never re-fired; the await between the
  reset and the re-set splits the React batch so the toast shows every time.
- settings-prompt: the variables palette marks "used" chips with a leading
  check icon, giving a non-color signal alongside the green variant (the
  used/available distinction was conveyed by hue alone for sighted users).
- settings-sidebar: drop the unused `export` on the MenuItem interface — it has
  no importers; the extraction from settings-layout was the moment to clean it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 20:44:28 +07:00
Sergey KozyrenkoandClaude Opus 4.8 4214f3b26d refactor(frontend): unify provider/prompt unsaved-changes onto the shared guard
The provider and prompt edit pages each carried a bespoke unsaved-changes
mechanism: a synthetic history.pushState + popstate listener that only caught
the browser back gesture, a "Discard changes?" ConfirmationDialog, and (after
the in-page Cancel button was dropped in the redesign) a chunk of unreachable
leave-handler code. In-app sidebar navigation while dirty silently discarded
edits, and the discard dialog rendered a Trash icon via confirmIcon={undefined}.

Both pages now use the same useUnsavedChangesGuard + UnsavedChangesDialog the
knowledge editor uses:

- useBlocker intercepts in-app router navigation (the sidebar links), and
  beforeunload covers reload/tab-close — not just the back gesture.
- The dialog offers Cancel / Discard / Save & leave (the old one was Stay /
  Leave with no save path).
- Forms switch to mode: 'onTouched' so isValid drives the dialog's Save button.
- Provider keeps "Save navigates to the list" via the guard's skipNextBlock;
  prompt's Save & leave saves every dirty tab (snapshotting both before the
  refetch-driven reset can clobber the other tab).

Removes the popstate/pushState refs, the dead handleConfirmLeave/-OpenChange
branches, and the misleading Trash icon. Resolves review findings on the
removed Cancel affordance, the dead leave-guard code, and the discard icon.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 10:37:13 +07:00
Sergey KozyrenkoandClaude Opus 4.8 916961d68d fix(frontend): harden the prompt variables palette (used-detection, dirty, a11y, scroll)
A code review of the "Available variables" block surfaced several real issues:

- Used-detection only matched bare {{.X}} interpolations, so variables used
  only in conditionals/ranges/nested access ({{if .X}}, {{range .X}}, {{.X.Y}})
  showed as "available" and clicking them inserted a stray bare {{.X}} instead
  of jumping to the existing usage. Detection and the click-to-locate logic now
  share one regex matching a top-level field in any {{ }} action, mirroring the
  backend's variable extraction; clicking a used variable selects its real
  occurrence.
- Clicking a used variable scrolled to it by counting logical newlines, which
  undershot badly once the textarea soft-wraps long lines (the default
  templates lay out ~40% more visual lines than logical), so the jumped-to
  variable landed off-screen. Measure the true pixel offset through a hidden
  mirror element and center it, so the selection is always scrolled into view.
- Inserting a variable via a chip called setValue without { shouldDirty: true },
  so the change never marked the form dirty and the unsaved-changes guard stayed
  silent. Both insert paths now mark the form dirty, matching keyboard typing.
- The chips were click-only <div>s (no keyboard/screen-reader access). They are
  now role="button" with tabIndex, Enter/Space handling, and a state-aware
  aria-label/title ("Insert …" / "Go to …"), which also activates the Badge's
  existing focus ring.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 07:47:26 +07:00