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>
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>
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>
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>
@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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
"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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
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>
- 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.
- 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.
- 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.
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>
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>
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>