Commit Graph
540 Commits
Author SHA1 Message Date
Sergey KozyrenkoandClaude Opus 4.8 bb695df721 fix(editor): map token end so a hard break inside {{.Var}}/<tag> spans fully
collectInlineMatches derived `to = from + match.length`, which undershoots
when a non-text inline node (a hard break from Shift+Enter) sits inside a
{{...}} or <tag> token: the highlight decoration and the cycle/select then
land one char short (off by the node's size). Read `to` from the per-character
position map instead. View-only — getMarkdown() was already byte-identical.

The module comment asserted the opposite (false) invariant; corrected. Test
inserts a hard break inside a token.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 12:58:04 +07:00
Sergey KozyrenkoandClaude Opus 4.8 69f86ae566 fix(editor): mark-boundary-safe inline scan shared by highlights + cycle
A {{.Var}} (or <tag>) split across text nodes by a mark — e.g. a user
styles one brace — was missed by the per-text-node scan: the cycle then
inserted a duplicate while the panel still counted it used, and the
highlight silently dropped on the fragment.

- new collectInlineMatches (editor-inline-scan.ts) scans each textblock's
  concatenated inline text and maps offsets back to doc positions, so a
  split token reunites in one block string.
- VariableHighlight, TagHighlight and findVariableOccurrences all use it.
- share one variableUseRegex + VARIABLE_RE between the editor and
  settings-prompt (countVariableUses + the plain-mode cycle), replacing the
  hand-synced duplicate regexes.
- test: a brace-styled {{.Foo}} is now found (was 0 before).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 12:17:33 +07:00
Sergey KozyrenkoandClaude Opus 4.8 55f105ffa1 fix(editor): focus before scroll in insertAtCursor + drop dead handle methods
insertAtCursor dispatched scrollIntoView before view.focus() — the reverse
of cycleToVariable — so the first post-load variable insert (fired from a
button outside the editor, before it is focused) would not scroll the
inserted text into view. Reorder to match cycleToVariable.

Also drop the unused getEditor() and focus() from MarkdownEditorHandle:
the only callers are cycleToVariable + insertAtCursor (verified by grep).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 06:25:59 +07:00
Sergey KozyrenkoandClaude Opus 4.8 e29582958d feat(editor): cycle through variable uses in the tiptap variable panel
The "Available variables" panel could insert {{.X}} but, in the rich
(tiptap) editor, could not jump to an existing use the way the plain
textarea already did — the used-highlight + go-to-next were plain-only,
which read as a regression.

- cycleToVariable on MarkdownEditorHandle: finds {{.var}} occurrences in
  the doc and selects + scrolls to the next one; returns false when there
  are none so the caller inserts instead (cycle-or-insert contract).
- findVariableOccurrences (editor-variable-highlight.ts): the doc scan,
  position-mapped to match the VariableHighlight decoration.
- panel isUsed = count > 0 in BOTH modes; handleVariableClick cycles when
  the variable is used, inserts when not.
- focus before scrollIntoView: ProseMirror no-ops scrollToSelection on an
  unfocused view, so the first post-load click would otherwise not scroll
  (looked like a lost/double click).
- perf: the value-sync effect re-serialized the whole ~24KB doc twice per
  keystroke only to no-op on self-echo; early-return when value is our own
  last-emitted output (mount + real external form.reset still sync).
- tests: findVariableOccurrences span/word-boundary/unused cases.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 06:23:12 +07:00
Sergey KozyrenkoandClaude Opus 4.8 fb1c15926f chore(deps): dedup marked to one v18 via pnpm override
@tiptap/markdown pins marked ^17 while our direct dep (report-pdf + the
editor's parser instance) is ^18, so two marked copies were installed and
the marked config needed an `as never` to bridge the version skew.

Add `overrides: { marked: ^18.0.5 }` so @tiptap/markdown resolves onto v18
too — one copy. Behaviourally a no-op for the editor (it already passed a
v18 Marked instance to the MarkdownManager; only @tiptap/markdown's unused
default + types change, and the lexer API is identical 17↔18). Narrow the
createMarkdownLayer cast from `as never` to a typed cast now that the
versions match.

Verified on the forced v18: tsc, 747 vitest, lint, production build green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 01:22:05 +07:00
Sergey KozyrenkoandClaude Opus 4.8 7e6410debf refactor(editor): apply review follow-ups (history-key identity, narrow cast, trim comments)
Multi-dimension review found no bugs, leaks, or security issues; these are
the confirmed robustness + comment nits:
- resetUndoHistory: match the history plugin by PluginKey identity (a fresh
  history() shares prosemirror-history's module-level singleton key) instead
  of sniffing the undocumented stringified `history$` name — so an upstream
  change fails loudly instead of silently turning the reset into a no-op.
- MarkdownTable: narrow `pipeEscaping as never` to
  `as Parameters<typeof renderTableToMarkdown>[1]` so a future signature
  change is caught at compile time rather than swallowed.
- Trim restating/duplicated comments in markdown-editor.tsx and
  editor-markdown.ts; kept the genuine framework gotchas (reconfigure trap,
  onBeforeCreate timing, setContent contentType, pipe re-parse).

tsc, lint, 747 vitest green; verified history() spec.key is a singleton.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:55:02 +07:00
Sergey KozyrenkoandClaude Opus 4.8 e1e7817dd6 feat(editor): migrate markdown round-trip to official @tiptap/markdown
Replace the unified rich editor's markdown engine: drop the community
tiptap-markdown (markdown-it parse + prosemirror-markdown serialize) and
our 5 prosemirror-internal monkey-patch extensions for the official
@tiptap/markdown (marked-based MarkdownManager). The consumer moves to the
new API (editor.getMarkdown() / contentType:'markdown' / markdown.parse).

editor-markdown.ts adds three small, supported-API customizations:
- a private HTML-neutralized marked instance so literal <xml-tags> survive
  (marked otherwise swallows real-HTML-element names like <input>);
- FaithfulMarkdownText overrides MarkdownManager.encodeTextForMarkdown to
  drop entity-encoding and over-escaping of literal punctuation;
- MarkdownTable wraps renderTableToMarkdown to escape cell pipes (#7884).

Verified: tsc, 747 vitest (rewritten extension + 39-prompt corpus tests),
lint, build; live on the dev stand — knowledge tables render and resize,
prompt <tags> stay literal (124 tag + 116 variable highlights, no
entity-encoding), console clean.

Known accepted bug, pinned by a test (marked parser, 0 corpus impact):
a code block nested ordered-list > bullet-sublist > code is dropped on
parse. Repro recorded for an upstream report.

Folds in the in-progress unified-editor work it depends on: knowledge
Plain/Visual toggle, settings-prompt and template editor wiring, the
CodeMirror removal, and the editor table/tag/variable styles.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:33:22 +07:00
Sergey KozyrenkoandClaude Opus 4.8 18839b3487 refactor(templates): adopt the 2-panel resizable layout of the other detail pages
templates/{id} now mirrors knowledges/{id} and settings/prompts/{id}: a left
ResizablePanel (intro + title + the Presets panel, in the spot where the prompt
page shows "Available variables") and a right panel holding the Code/Plain
editor that fills the space. Save moves to the header (FormSubmitButton
form="template-form"); the right-side Presets sidebar/Sheet, the PanelRight
toggle, the in-input save button, and the Enter-to-submit handler are removed.

Live-verified on the stand: 2-panel render (create + edit), preset apply,
create->DB->reload byte-fidelity (incl. the blank line, reconstructed from
.cm-line), the Code editor filling the right panel, delete. tsc/eslint/build
+ 652 frontend tests green; console clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 01:41:26 +07:00
Sergey KozyrenkoandClaude Opus 4.8 d3c46c7158 test: close the coverage gaps flagged by the review
- providers: prove a stale user-provider row does not knock out a valid USER
  sibling (TestGetProviders_StaleUserRowSpansValidSibling — ollama builds keyless
  so it survives beside a skipped minimax; side-by-side verified).
- settings-provider: cover the create-form ?type=/?id= guards (disabled/unknown
  type and clone-of-disabled bounce to the list; an enabled type renders). These
  had zero coverage — a swap/drop-return regression would have shipped green.
- knowledge-form: cover performSave's server-document reset branch (untouched
  fields reflect the returned document under keepDirtyValues), and the useBlocker
  "Save and leave" path via a real data router (proceeds the blocked nav, does
  NOT honor a CREATE redirect). Swap the negative no-navigate assertion's
  setTimeout(0) flush for a deterministic Save-disabled anchor.

Each test mutation-verified to fail on the reverted production code; 652 frontend
tests + go test green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 00:55:29 +07:00
Sergey KozyrenkoandClaude Opus 4.8 84d87cf474 refactor(providers): apply review follow-ups (degrade log, count helper, empty menu)
From a strict re-review of the recent commits (no blocker/high/security found):
- providers.go GetProviders: the skip covers ANY unbuildable saved provider, not
  only a disabled type — drop the misleading "of unavailable type" wording and
  lower the line to Debug (it re-fires on every providers fetch; WithError keeps
  the reason).
- settings-prompt countVariableUses: drop the redundant seed-in-map side effect;
  the component already falls back to `?? 0` for unused variables, so the loop's
  own `?? 0` is the only seed needed.
- settings-providers create menu: render a disabled "No available provider types"
  placeholder instead of a silently-empty menu (loading / no-keys / failed-query
  states), with a test covering it.

Counts live-verified unchanged (AgentType 1->3 on the stand); go test + 646
frontend tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 23:45:36 +07:00
Sergey KozyrenkoandClaude Opus 4.8 579fac8b69 feat(providers): guard cloning a disabled-type provider + test the menu filter
Follow-up to b6d1036. Cloning an existing provider whose type is now disabled
(?id=) would have produced another dead provider — apply the same enabled-check
on the clone path. Adds a Vitest covering the create menu's enabled-only filter
(exports SettingsProvidersHeader for the render test; eslint sort-modules then
reorders it above the page component — declaration hoisting, no runtime change).

Live-verified: clone of the disabled minimax (?id=3) redirects to the list;
clone of bedrock (?id=2) still opens with a "(Copy)" name.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 23:24:52 +07:00
Sergey KozyrenkoandClaude Opus 4.8 b6d10362cf feat(providers): hide disabled types from create menu + guard ?type= URL
A user could create a provider of a type whose API key isn't configured: it
saved fine and showed in settings, but was dead for flow creation with no
signal (and used to break the whole providers query — see 08a24c9). Two layers:
- the "Create provider" menu now only offers types whose key is set
  (settingsProviders.enabled, already fetched by the page), and
- the create form bounces a hand-typed ?type= that is unknown or disabled to
  the list, closing the direct-URL bypass of the filtered menu.

Frontend-only. Live-verified on the docker stand: minimax/custom drop from the
menu (11 -> 9); ?type=minimax and ?type=garbage123 redirect; ?type=anthropic
still opens the form.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 23:07:53 +07:00
Sergey KozyrenkoandClaude Opus 4.8 4664cf8f53 test(knowledges): cover knowledge-form save/navigation + partial-update mapping
The knowledges feature had zero coverage while 01fa02d reworked its save/guard
logic. Add Vitest tests for the exported pure mappers (create/update inputs —
including the dirty-gated "" vs undefined distinction — and the zod
docType->subtype superRefine) and the component wiring (create -> navigate to
redirect, update -> no navigate, save-disabled-until-dirty, scoped
anonymize-disabled). The useBlocker dialog path stays covered by manual/live
testing (it needs a data router the component tests intentionally stub out).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 22:19:51 +07:00
Sergey KozyrenkoandClaude Opus 4.8 08a24c9abd fix(providers): skip unavailable user providers instead of failing the whole list
A saved user provider whose type is no longer enabled (e.g. its API key was
removed) made GetProviders return an error for the ENTIRE `providers` query —
one stale row blocked all flow creation in the UI ("No available providers").
Skip and log such rows, mirroring how startup already tolerates disabled
default providers. Pre-existing robustness gap, not introduced by this branch.

Verified live on the docker stand: the `providers` query went from a hard
error to returning all 10 enabled providers (the stale minimax row skipped),
and flow creation works again. Adds a side-by-side regression test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 22:10:40 +07:00
Sergey KozyrenkoandClaude Opus 4.8 c752bb2737 perf(settings/prompt): count variable uses in one pass + bound regex
The "Available variables" badges recomputed a per-variable `.match` over
the whole template on every keystroke (O(variables × length)); fold them
into a single pass over the `{{ … }}` blocks. Tighten the action regex to
`[^{}]` so an unclosed `{{` cannot drive quadratic backtracking — closes
the self-DoS flagged by the security review.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 20:37:26 +07:00
Sergey KozyrenkoandClaude Opus 4.8 4472daa0d1 refactor(knowledges): trim performSave doc-comment to the load-bearing invariant
Drop the function-behavior restatement; keep the actionable invariant (navigation
stays with the caller, else the guard↔onSaveFromDialog↔performSave cycle returns).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 15:45:58 +07:00
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