Skip to content

Merge main into major/agentic-chat (catch the base, through #866) - #867

Merged
ViaJables merged 96 commits into
major/agentic-chatfrom
catch/main-into-v5-2026-09-08
Sep 8, 2026
Merged

Merge main into major/agentic-chat (catch the base, through #866)#867
ViaJables merged 96 commits into
major/agentic-chatfrom
catch/main-into-v5-2026-09-08

Conversation

@ViaJables

Copy link
Copy Markdown
Collaborator

Catch-the-base merge: brings major/agentic-chat up to origin/main through #866 (22 first-parent merges since #856).

What comes across

The 2026-09-08 batch — #795 (one chip row for documents, folders, KBs), #799 (certification rework), #800, #815, #857 (vLLM structured output + Test probe), #818 (provider-native token counting), #821/#824 (branding on dark surfaces and chrome), #866 (podman) — plus the model-eval stack #831#838, #842 (workflow default model), #859 (dep bumps), #860 (CHANGELOG repair), #796 (step-level input/output), #797, #798, #801, #820 (app name vs org name).

Conflicts (11 files) and how each was resolved

File Resolution
backend/app/database.py, chat_service.py Union of both sides' imports and document registrations.
LoginForm.tsx Branch's "Need an account?" link to the v5 landing page, with main's --highlight-on-dark hover token.
ChatInput.tsx Branch's document-aware placeholder, with main's appName.
LeftPanel.tsx Branch's useWorkspace destructuring plus setSelectedFolderNames from #795.
modules.ts Main's broader Module 0 description (kept "validate"), and both lesson sets at the two collision points — main's worked examples first, then the v5.0 insight lessons.
exercises.json, Landing.tsx Branch side (chat-driven lab instructions, v5 launch page); main's change there was the orgName→appName rename.
AttachmentList.tsx Main's unified chip row, with the branch's processing/ready state ported onto the document chips.
ChatPanel.tsx Branch's agentic panel with main's chip-row wiring (folder/KB props on AttachmentList), the old KB bar removed, unused Link2 import dropped.
WorkflowEditorPanel.tsx Main's step-level input/output restructure, with the branch's punctuation pass re-applied (35 strings; 3 more followed the text into StepInputSources.tsx).

CHANGELOG merged clean: [Unreleased] is byte-identical to main's (23 bullets), the 5.0.0 section is untouched.

Verified on the merged tree

  • tsc -b: clean
  • Backend: 4997 passed, 161 skipped
  • vitest on chat/, contexts/, certification/, auth/, workspace/: 278 passed; one HomeExperience test hit the 5 s timeout on the first cold run of 42 files and passes on every re-run, so it's a cold-start flake rather than a merge effect.

Owed after this lands

From the #839 review: chat_tools.get_quality_info's per-item model_comparison now duplicates main's quality_service.get_quality_item_detail (#833, same shape). Fold it into the shared helper, move the demo_seed exclusion and (unattributed) label there, and add source != "demo_seed" to get_quality_by_model as #839 promised.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Qk88vvuDRJz5Nft2pRNpeY

ViaJables and others added 30 commits September 1, 2026 11:18
Documents, uploads and links showed as pills under the chat header while an
attached knowledge base showed as a full-width bar above the composer, and a
folder — which scopes the chat and is sent on every message — showed nothing
anywhere. All three answer the same question about a conversation, so they
share one row.

The pill row was already the deliberate merge of chat uploads and file-browser
selection; the KB bar was never brought along. This finishes that.

- Type is icon + text tag + tint, never tint alone: --highlight-color is
  deploy-customisable and colour alone fails a colourblind reader. The KB and
  Folder tags preserve what the bar's "Knowledge Base: " prefix said.
- Scope chips (KBs, capped at 3; folders) render first and always; the
  unbounded document tail collapses behind "+N more" past six, so a heavy
  library selection cannot push the knowledge base out of view.
- FileBrowser reports selected folder titles alongside uuids, mirroring the
  document path, so a folder chip can name itself.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKDBCBvedFaffXibXnFM2g
A step's tasks all receive an identical copy of one payload and run in
parallel, but input was configured per task — so a two-task step asked the
same question twice and let the answers disagree.

The step editor gains Basic Setup / Input / Output tabs. Input is set once
and shared by every task; Output describes what the engine actually does
with the tasks' results rather than offering settings that do not exist.
The task editor collapses to a single Design surface, keeping only what is
genuinely per-task: what the task does, and post-processing of its own
output. Different-source-per-task survives as a buried Advanced disclosure,
badged OWN INPUT in the step's task list.

Existing workflows are untouched: the engine falls back to a task's own
keys when its step defines no input, and the step's Input tab seeds from
the tasks rather than defaulting. Lifting a shared answer off tasks that
disagree pins the odd ones out to the override instead of repointing them.

Also makes a step's combined output deterministic — results were collected
in completion order, so the same two-task step could emit [A, B] or [B, A]
depending on which model answered first, with field_sources padded
positionally against that list.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011tbi5K9tPm3DNKC5VJyXyQ
…tracts nothing

The challenge step said "Include fields for: ... budget categories
(personnel, equipment, travel, indirect costs) ...", so users typed
"budget categories" as a field name and it extracted blank — extraction
has no grouping; each category must be its own field. The chips below the
sentence already showed the right names; the sentence contradicted them.

Reword the step and star criteria 2 to name each budget category as its
own field, matching the chips exactly, and say outright that a single
"budget categories" field would extract nothing. The module objective in
modules.ts repeated the same phrase and is reworded the same way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011tbi5K9tPm3DNKC5VJyXyQ
…the previous content

Refreshing a 2 CFR 200 Subpart source fetched eCFR's JavaScript shell
instead of the regulation. The shell carried none of the phrases the
bot-challenge and boilerplate gates recognise, so it passed them, and the
refresh replaced 208 chunks with 1 — marked "Refreshed", green check. Chat
then answered §200.414 questions from general knowledge.

Phrase lists recognise the shells they have seen. The one thing known
about a page that no phrase list can miss is the size of what is already
indexed, so a refresh now refuses a page under a quarter of the retained
text as not having loaded, and takes the failed-fetch path: previous
content kept and served, source shows "Refresh failed — previous content
kept: …" with the numbers, retrieval date unmoved.

Prevents recurrence; does not restore a source already overwritten.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKDBCBvedFaffXibXnFM2g
…feedback)

Six changes from a sit-down with a DGA working through the course:

1. Module 0 opens with the big picture — what AI is, why it's in research
   administration, what the course delivers — before any terminology, and
   teaches structured vs. unstructured data as the course's central idea
   with no under-the-hood mechanics.
2. Jargon is defined in RA terms: JSON as "the fill-in-the-form format you
   read, never write", Token via attachment-size limits, and Module 1's
   pipeline lesson drops chunking/embedding/ChromaDB for "it reads the text
   and builds an index, like the back of a book".
3. Every module now carries a worked example — eleven new walkthroughs
   grounded in the modules' own sample documents.
4. Time estimates show everywhere they were missing: journey-map total,
   locked cards, and the open module's header ("your place is saved").
5. An explicit pop-out button opens the course in its own browser window
   for a second monitor, and the five cryptic mode icons get descriptive
   tooltips with the pin group visually separated.
6. The Module 1 lab now says the Run button stays greyed out until a
   document is ticked in the file browser — the exact stuck point.

Point 7 of the feedback (wrong duplicate-name message on import) is a
backend bug, fixed separately.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011tbi5K9tPm3DNKC5VJyXyQ
An Explore import told a user she "already had a workflow with the same
name" while her personal library showed no such row — and she was right
to disbelieve it. The uniqueness scope counts more than the personal
library: a teammate's team-shared workflow, her own workflow filed under
the Team tab, and a workflow whose library bookmark was removed while
the object (and its name) lives on. The flat "already exists in your
library" message claimed all of these were sitting in front of her.

The 409 now resolves the conflicting workflow and names the case — team
library (hers or a teammate's), or existing-but-unlisted — and quotes
the stored name's exact capitalization, since the match is
case-insensitive and "budget analyzer" is invisible to someone scanning
for "Budget Analyzer".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011tbi5K9tPm3DNKC5VJyXyQ
…don't offer a File view that 404s

A KB built on the NSF PAPPG answered from text that held the Introduction,
Chapter II and Chapter XII and none of I, III, IV or V, behind a green
check; the inspector's File view answered {"detail":"File not found"}.

The document pipeline already records a partial conversion on the document
(ingestion_warnings: partial_ocr, sparse_text) and chat and the file list
say so. The KB did not: not the source row, the inspector, the export, or
source health — a fifth of the KB quality score — which scored a partial
document as fully healthy.

- Source row: amber incomplete icon + a line with the pipeline's reason,
  the treatment a truncated URL source already gets.
- Inspector: banner above the text; export: ingestion_warnings per document
  source; source health: "partial", not counted healthy.
- Read live from the document, so a successful re-extraction clears it.
- Inspector detail reports document_file: available / no_access / missing,
  decided the way the download route decides it; the File view is offered
  only when available, with the reason in the header otherwise.

Surfaces what the pipeline detected; does not detect more. sparse_text is a
40-chars-per-page floor and partial_ocr needs the converter to report it, so
a document missing half its chapters can still pass both.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKDBCBvedFaffXibXnFM2g
The recipe called `build`/`update` without `--repo`, so the tool discovered
the repository from the working directory. Under `git worktree` that is the
worktree, not the checkout holding the graph, so a run from one built a
second index there containing only the files that worktree had touched --
and then answered every query from it reporting `"status": "ok"`.

Measured on a working copy: four worktrees carried indexes of 7, 27, 26 and
6 files against a real 967. A partial index returns 0 for anything it never
parsed and a true 0 is indistinguishable, so `callers_of` on a live symbol
reports no callers, which reads as "safe to change".

The first `git worktree list` entry is always the main working tree, so one
graph now serves every worktree. ORIG_HEAD is read from that same tree,
since that is the tree being indexed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KBMRynZ8iqpC7JKvwdBw9m
The page noted that semantic search "needs an extra" but not what happens
without it. It does not fail: `search` returns `"status": "ok"` with
`"search_mode": "fts"` -- keyword matching under a semantic name. A query
whose terms appear literally still looks right, so the degradation is
invisible until a conceptual query quietly returns nothing, which reads as
"there is nothing there".

The install line now takes `[embeddings]` and the setup adds the separate
`embed` step, since building the graph does not populate vectors. Three
`search_mode` values are documented -- `semantic`, `hybrid` (also correct)
and the degraded `fts`.

Also documents two traps that belong to the tool rather than the recipe:
`detect-changes` reads its diff from the same `--repo` path it reads the
graph from, so from a worktree it reports on main; and a symbol the graph
never indexed is answered exactly like one with no callers, which only
2.3.8+ annotate with a `confidence` field.

The network note now separates the local model -- downloaded once from
Hugging Face, then run on-device, sending no repository content anywhere --
from the optional remote backends, which stay off for the reason already
given.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KBMRynZ8iqpC7JKvwdBw9m
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KBMRynZ8iqpC7JKvwdBw9m
`token_safety_margin` gains a rung between the tiktoken-is-exact check and
`DEFAULT_TOKEN_SAFETY_MARGIN`. When a provider has counted the request, the
margin becomes the measured ratio of its count to our local one, rather than
the 1.5 that covers the worst content anyone measured.

Deliberately a ratio and not a total. The margin is applied per component,
`BudgetPlan.total_input_tokens` is a computed property summing those parts, and
the planner recounts on mutated text inside its trim loop. A recorded total
stops being true the moment anything is trimmed; a ratio stays approximately
valid.

Clamped at 1.0: a provider counting below tiktoken may only reduce
over-inflation, never reclaim window. Rejected outright on a model-name
mismatch, a zero baseline, a non-positive count, or a non-finite ratio, each of
which falls through to the existing ladder — so an absent or broken measurement
is the 1.5 guess, by construction rather than by a second code path
remembering. The "estimated, not exact" warning stays quiet only when the
measurement actually supplied the margin.

Inert until a caller passes one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HucxUJ7gaA6iKveF8aruW2
`_sized_for` re-derived the current model's safety margin from its name and
config in order to restate a request in a candidate's units. That is correct
only while every count comes from the same estimate-plus-default path.

A provider-counted request carries a much tighter margin, and
`token_safety_margin` cannot know that happened — it still answers 1.5. Dividing
an already-tight number by a factor never applied understates the request by the
whole difference, at the exact point the router is deciding whether a candidate
can hold it. That is the #648 defect — an estimate that reads low, so the
request hard-fails — relocated to the routing boundary, which this function's
own docstring says it exists to prevent.

Callers may now state the margin their number was measured with.
`choose_document_model` and `suggest_document_model` pass it through; the
candidate's margin stays derived, because the candidate has not been counted.
Omitted, every caller keeps today's arithmetic to the token.

A supplied margin below 1.0, non-finite, or not a number at all is refused in
favour of the honest derivation, matching how `context_budget._configured_margin`
refuses the same thing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HucxUJ7gaA6iKveF8aruW2
New `native_token_count`: fetches a provider's own count for a prepared
request, or reports plainly that it could not.

Two properties, and the second is the load-bearing one. A returned figure is the
provider's own, unmodified. Anything going wrong returns nothing at all — the
caller drops its safety margin on the strength of a usable result, so a count
that comes back zero, short, or from a provider that counted half the request is
worse than no count, converting a conservative over-estimate into a confident
under-estimate.

So: gated before any I/O on a protocol that can actually count, since
`OpenAIModel.count_tokens` raises and every OpenAI-compatible model would
otherwise decrypt a key and build a client per chat turn to learn that. Wrapped
in a five-second timeout that is deliberately not the request's own settings —
`build_thinking_model_settings` sets 120s and the shared client retries a 429 six
times honouring Retry-After up to 60s, which would put a two-minute stall in
front of every message. Blanket `except` by necessity: Anthropic raises
ModelHTTPError, Google's genai errors are unwrapped, and a misconfigured
protocol reaches OpenAIModel; there is no complete and stable union of those.
CancelledError passes through, being the caller going away rather than a failure.

Google's count omits the system instruction — pydantic-ai attaches
`system_instruction` only for providers other than `google-gla`, and an
api-key GoogleProvider is `google-gla`. `covers_system_prompt` says so rather
than letting a caller silently drop a multi-kilobyte grounding preamble from its
budget.

Nothing imports this yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HucxUJ7gaA6iKveF8aruW2
Wires the native count into `chat_stream`. Before the request is sized, the
provider is asked what the assembled prompt actually costs; the same payload is
counted locally; the ratio becomes this turn's safety margin in place of the 1.5
guess.

The payload counted is the whole user prompt, documents included. `_build_chat_prompt`
is the existing assembly extracted verbatim so the pre-flight counts the request
chat is about to make rather than a reconstruction of it — in document chat the
documents are the payload, and they are also the digit-dense content the 1.5 was
sized for. Sampling the question and history alone would measure prose and apply
the answer to a budget table.

The baseline covers exactly the components the provider counted, which differ by
provider: Anthropic's count includes the system prompt, Google's does not for
api-key providers. Counting it on one side and not the other would depress the
ratio for a reason unrelated to tokenizer divergence, and a depressed ratio
clamps to a margin of 1.0 over a tiktoken figure that itself under-counts these
models — the hard-fail direction.

Routing is given the margin the number was measured with, rather than
re-deriving it. That value survives a model switch on purpose: it describes how
`requested_input_tokens` was measured, and that does not change when the model
does.

A count for one model is not passed to the planner for another. `_native_margin`
already rejects on a name mismatch, but relying on that silently is how the next
person learns it the hard way.

Anything unusable — ineligible protocol, timeout, 429, malformed response — leaves
the stream exactly as it is today.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HucxUJ7gaA6iKveF8aruW2
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HucxUJ7gaA6iKveF8aruW2
`org_name` was doing two incompatible jobs. It is the word users read in
conversation — the chat welcome, the input placeholder, "I'm your X assistant",
the browser tab — and it is also an institutional field that leaves the
deployment: `verification_service` stamps it as the institution in a creator
credit built so attribution survives catalog export into other installs, and
`export_import_service` writes it as `deployment_org`.

No single value is correct for both. Set it to the institution and the assistant
introduces itself as a university. Set it to the product and every catalog item
the deployment contributes is credited to an institution that does not exist,
somewhere its readers cannot tell it was a branding setting.

`app_name` resolves as `app_name or org_name or "Vandalizer"`. Conversational
surfaces read it; the footer, email, team naming, issued credits and every logo
`alt` keep reading `org_name`.

The fallback is the compatibility story: an install that already set `org_name`
and leaves `app_name` empty is byte-for-byte unchanged.

`isCustomized` now also trips on a customised `app_name`. That flag keeps the
"Powered by Vandalizer" credit and the NSF acknowledgement visible under GPL v3,
and without this someone could rename every conversational surface while never
touching the field it watched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HucxUJ7gaA6iKveF8aruW2
The sign-in, password-reset and invite-acceptance pages hardcoded Vandalizer's
gold instead of reading the configured brand colour. A white-labelled install
therefore greeted new users with its own logo above a button in someone else's
brand — on the first screen anyone sees. Landing.tsx was migrated for exactly
this reason; these surfaces were missed.

Doing it safely needed a token that did not exist. `getAccessibleOnLight`
darkens the brand colour until it passes contrast on white; nothing did the
same for the near-black auth and footer surfaces. Without it, migrating a
deployment branded to anything dark would trade a legible gold for an
unreadable version of its own colour: RWU navy #163A64 measures 1.72:1 on
#0a0a0a, against 4.79:1 for the lightened variant, hue preserved.

Seventeen buttons paired the gold background with hardcoded `text-black`. Those
move to the runtime-computed contrast colour in the same edit — changing the
background alone is how black text ends up on navy.

Deliberately left: the footer's "Powered by Vandalizer" credit, GPL link and
NSF acknowledgement (the obligation is the words; only colour classes changed),
two SSO buttons whose black text sits on permanent white, and one
`hover:text-[#d49e00]` on a dark surface — `--highlight-hover` is a *darkened*
brand colour and would read worse there than what it replaces, so a correct fix
needs an on-dark hover variant that does not exist yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HucxUJ7gaA6iKveF8aruW2
`--color-panel-dark` was a fixed #191919 that nothing derived from anything, so
a deployment branded to navy got navy accents on neutral-black chrome — the
workspace tab strip, the document-panel header, the knowledge panels, the left
rail.

Derived now from the brand's hue and saturation with the lightness pinned. That
pin is what makes it safe rather than reckless: contrast against the white
chrome text stays predictable whatever an admin picks, where tinting neutrals
freely makes contrast a runtime property of an arbitrary input. Navy #163A64
gives #0f1d2e at 16.99:1 on white, against #191919's 17.58:1. Saturation is
capped so a saturated brand does not read muddy; a greyscale brand comes back
neutral rather than faintly tinted.

Thirteen hardcoded #191919 backgrounds move onto the token, so the chrome cannot
end up half-branded — including LeftPanel's "Black header bar", which is the
surface that prompted this.

Unbranded deployments are unchanged. When highlight_color is still the shipped
default the variable is *removed* rather than set, so the :root neutral wins.
removeProperty and not an early skip: applyTheme runs for the cached theme and
again for the server's, so an admin resetting a brand back to the default has to
clear the value this same call previously wrote. Mirrors the _THEME_DEFAULT_COLOR
guard in email_service.py, which protects branded email for the same reason.

Left alone: both certification-badge gradients (the second stop is out of scope,
and tinting one end gives a hue break), four text colours sharing the hex, and
one 8-digit alpha form needing color-mix() rather than a swap.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HucxUJ7gaA6iKveF8aruW2
…odel requests

A ValidationRun's model field recorded the requested model, but a model
pinned in system or per-set extraction config silently won inside the
engine — so a run could be labeled with a model that never executed, and
the regression suite's "run under model X" measured the pinned model
instead. Workflow and KB validation runs recorded no model at all.

- ExtractionEngine.effective_model_info() resolves what extract() will
  run (model, provenance, configured temperature) without running it.
- Extraction validation forces an explicitly requested model into the
  engine config (including two-pass per-pass models) so it actually
  runs, and persists the resolved model plus a model_settings snapshot.
- Workflow validation attributes the models its graded WorkflowResults
  snapshotted at dispatch — a single label only when they agree.
- KB validation records the answer-generation model (distinct from the
  judge), takes an explicit answer model that beats a KB config
  override, and pins the no-KB baseline to the same model so lift
  measures the KB, not a model swap.
- ValidationRun gains model_settings so later edits to a model's live
  config can't rewrite what a historical score measured.

Wiring tests verified red against the old behavior before the fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018f8shnU7x3BV6PiMZV928r
…omparison

The suite ran every verified item synchronously inside the HTTP request
(a catalog-wide sweep of LLM calls no gateway timeout survives) and
returned rows into component state where a reload or a second run
destroyed them. It computed no catalog-wide mean, never echoed which
model produced a table, validated an item once per user who had added
it, ignored the model parameter for KBs, and labeled rows with raw ids
(LibraryItem has no .name).

- RegressionSuiteRun document + tasks.passive.regression_suite Celery
  task; progress (completed/total, per-item rows) lands on the doc as
  the sweep advances.
- POST /quality/regression-suite dispatches and returns run_uuid; new
  GET runs list/detail endpoints; endpoint map regenerated.
- run_regression_suite dedupes (kind, item_id), forwards the model to
  KB validation (workflow validation grades historical executions, so
  no model can be forced onto it), resolves real item titles, and
  computes the mean over successfully validated items only.
- Quality tab polls the running job, lists past runs as reopenable
  chips, and puts two completed runs side by side per item with a
  mean-vs-mean header - model A vs model B over the verified catalog.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018f8shnU7x3BV6PiMZV928r
…dation_runs indexes

Fleet-wide, validation results could not be grouped by model at all --
the only per-model data was the single-item model_comparison drill-down.

- get_quality_by_model(days) + GET /api/admin/quality/by-model: avg
  score, run count, items covered, per-kind split, grouped by the model
  that executed each run; runs with model=None reported as their own
  "(unattributed)" row rather than hidden.
- Admin Quality tab renders it as a Model Performance panel, refreshed
  when a regression sweep completes.
- QualityTimeline gains a "model changed" chip symmetric to the existing
  judge-model warning, so a score dip from swapping the model under
  test stops reading as a content regression (the history payload
  already carried the model; the component dropped it).
- First indexes on validation_runs: (item_kind, item_id, created_at)
  and (model, created_at); the mgmt API model filter was a full
  collection scan.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018f8shnU7x3BV6PiMZV928r
…y what they measured

Drift monitoring compared the pinned official_baseline_score against the
item's latest (possibly unrelated) validation score; the frozen
official_baseline dict was inert payload nothing executed, and
last_drift_score's docstring described a re-run that did not exist.

- reexecute_official_baseline(): replays an extraction baseline's frozen
  test cases against the current config via run_validation_v2 -
  source text recovered from the stored ExtractionTestCase or the
  examiner's inline addition, expected values from the frozen rows,
  the baseline's own num_runs replicated so the sample-size discount
  matches (fewer replicates would read as false drift).
- Gated behind quality_config.monitoring.baseline_reexecution (default
  off - a re-run is real LLM spend per pinned item per day). Workflow
  and KB baselines cannot be replayed from the frozen dict alone and
  keep the proxy.
- Every check records last_drift_basis (baseline_reexecution |
  latest_validation_proxy); the drift alert message names its basis;
  the Catalog coverage tab marks proxy deltas "(proxy)"; the model
  field's documentation now tells the truth for both modes, and the
  false "full re-run happens in drift monitoring" comment on
  pin_retroactive_baseline is corrected.
- Metadata is reloaded after a re-execution before drift fields are
  saved, so the save doesn't clobber the quality scores the re-run
  itself just refreshed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018f8shnU7x3BV6PiMZV928r
…inned baselines

The bundled catalog's gold/silver tiers are hand-typed literals sitting
almost exactly on the items with no evaluation data (the 11 gold
workflows have zero checks; the two workflows with checks are "fair" or
untiered), and the badge rendered a hand-typed word identically to a
measured score.

- quality_asserted (tier present, no measured quality_score) exposed on
  the catalog listing, library listing, and coverage report.
- QualityContractBadge renders an asserted tier in the neutral style as
  "Gold (asserted)" with a tooltip naming the provenance; the flag
  clears itself the moment a real validation run lands.
- export_validated_seeds.py also exports a pinned official_baseline
  (+score) into seed files; seed_catalog.upsert_verified_metadata pins
  a shipped baseline as "catalog-seed" on install, refreshing its own
  prior pin but never overwriting one a local examiner pinned.
- Deliberately does NOT author eval content - test cases and expected
  answers must come from measured runs, not the seed-file author.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018f8shnU7x3BV6PiMZV928r
… regression suite

- The "Intelligence" bar beside every configured model (admin model
  list + chat model picker) was computed from the admin's own tier
  dropdown and thinking checkbox, rendered like a measured figure. It
  is now labeled "Tier" with a tooltip stating it reflects configured
  settings, not measured capability, and pointing at Admin - Quality -
  Model Performance for measured numbers.
- The regression suite's model dropdown was fed by the superadmin-only
  system config endpoint, leaving staff-level admins with only
  "Default Model" and no way to target a model. It now uses the same
  non-privileged /api/config/models listing the chat picker uses.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018f8shnU7x3BV6PiMZV928r
Adds a workflow-level default model applied to every step that has no
Model Override of its own. run_workflow / run_workflow_batch fall back to
input_config.default_model before the user default; a per-step override
still wins, and "Automatic (system default)" (blank) keeps prior behaviour.

- backend: fallback in run_workflow and run_workflow_batch
- frontend: "Model — all steps" selector on the Design canvas + type field
- test: 5 unit tests covering the resolution precedence

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…on catch it

Extractions and workflows failed against a vLLM model registered under its
HuggingFace repo name — "Native structured output is not supported by this
model." on every run — while chat worked and the same weights under a bare
name were fine.

JSON-schema output is a property of the server: vLLM enforces `response_format`
via guided decoding for anything it serves. The profile answered for the model
family's own hosted API instead — `Qwen/Qwen3-32B` resolved through OpenRouter's
family map to a profile leaving `supports_json_schema_output` at its False
default, and pydantic-ai refused the request. VLLMProvider now declares the
capability for every model it serves, keeping each family's own schema
transformer.

The per-model "supports structured output" toggle now does something. It was
written by the model editor and read by nothing on the extraction path, so
switching it off — the obvious escape hatch here — changed nothing.

The reason none of this was visible: the admin Test button ran one free-text
completion, so it went green for the entire outage. It now makes a second,
schema-constrained round trip using the same output-mode decision the
extraction engine makes (shared as one function, so the diagnostic cannot pass
on a configuration a real run fails on), and reports a model that chats but
cannot do that as failing. "Connected" and "usable" are different claims.

Two supporting fixes from the same incident:

* An unsupported output mode is classified with its two real remedies instead
  of falling through to "read the raw error".
* The Endpoint step reports the URL actually dialed, not the one typed — the
  vLLM and Ollama providers append `/v1` to a stored endpoint and the OpenAI
  one does not, so changing the protocol dropdown silently changed the URL.

Separately, found while building a model to test that: the external-OpenAI
branch passed `openai_client=` to a constructor taking only provider/profile/
settings, so any model added through the "OpenAI" or "Custom" setup preset
raised TypeError before a request left the server. No test built a model.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJ3BAa7MWo8CySegCumLEC
ViaJables and others added 29 commits September 8, 2026 12:25
… own workflow bookmark

LibraryItem.find_one({item_id}) matched any bookmark of that object,
including a teammate's team-library row, so a workflow the user had
removed from their library could still be reported as listed in it.
Filter by kind and by who added it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qk88vvuDRJz5Nft2pRNpeY
…tes up front

mkdir -p runs before the tool's first build, so the tool may never write
its own .gitignore into the directory; without this the flock lock file
shows up as untracked.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qk88vvuDRJz5Nft2pRNpeY
…tually breaks

Only steps that route through the extraction engine use NativeOutput;
LLM prompt steps answer in free text and keep working.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qk88vvuDRJz5Nft2pRNpeY
…r Unreleased, not v4.12.0

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qk88vvuDRJz5Nft2pRNpeY
getPanelDark on an empty or short highlight_color produced a non-colour,
and var(--panel-dark, #191919) does not fall back for an invalid value,
so the whole app shell painted transparent. Treat anything that is not
#rrggbb like the default. Also files the CHANGELOG entry under Changed:
the chrome following the brand is new behaviour, not a bug fix.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qk88vvuDRJz5Nft2pRNpeY
…ntainers, trim the changelog claim

compose.yaml carried two healthcheck keys on the celery service; docker
compose config rejects the file outright (podman-compose's PyYAML kept
the last key silently, which is why the live test passed). main already
had a compose-level inspect-ping probe for celery, so the new block goes.

compose_container used ps -a and took the newest match, so exec targets
could land on an exited or one-off container; it now prefers a running
one and falls back to any.

The CHANGELOG entry no longer claims a latent Docker celery bug that
main had already fixed, and notes that a stopped stack now counts as an
existing deployment.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qk88vvuDRJz5Nft2pRNpeY
's test block

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qk88vvuDRJz5Nft2pRNpeY
feat(chat): one chip row for documents, folders and knowledge bases
Certification: start broad, cut jargon, worked examples, time estimates, pop-out window (DGA feedback)
…cation

Duplicate-workflow-name error: say where the conflicting workflow actually is
…search-mode

fix(review-graph): one shared graph across worktrees, and say that semantic search degrades silently
…d-config-probes

fix(models): make vLLM structured output work, and make the Test button catch it
…nting

feat(budget): measure the token margin against the provider instead of guessing 50% high
fix(branding): a white-labelled sign-in page shouldn't show our gold button
feat(branding): the dark app chrome follows the brand colour
fix: generic container-engine support (podman / podman-compose)
Brings the v5 branch up to origin/main: the 2026-09-08 PR batch
(#795, #799, #800, #815, #857, #818, #821, #824, #866) plus the model-eval
stack (#831-#838), #842, #859, #860, #796-#798, #801, #820.

Eleven files conflicted; how each was resolved:

- backend/app/database.py, chat_service.py: union of both sides' imports
  and document registrations.
- LoginForm.tsx: the branch's "Need an account?" link to the v5 landing
  page, with main's --highlight-on-dark hover token.
- ChatInput.tsx: the branch's document-aware placeholder, with main's
  appName instead of orgName.
- LeftPanel.tsx: the branch's useWorkspace destructuring plus
  setSelectedFolderNames from #795.
- modules.ts: main's broader Module 0 description (with "validate" kept),
  and both lesson sets at the two collision points — main's worked
  examples first, then the v5.0 insight lessons.
- exercises.json, Landing.tsx: the branch's chat-driven lab instructions
  and v5 launch page (main's changes there were the orgName/appName
  rename, already covered).
- AttachmentList.tsx: main's unified chip row (documents, folders, KBs),
  with the branch's processing/ready state on document chips ported on.
- ChatPanel.tsx: the branch's agentic panel, with main's chip-row wiring
  (folder and KB props on AttachmentList), the old KB bar removed, and
  the unused Link2 import dropped.
- WorkflowEditorPanel.tsx: main's step-level input/output restructure,
  with the branch's punctuation pass re-applied where the text survives
  (35 strings; 3 more that moved to StepInputSources.tsx).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qk88vvuDRJz5Nft2pRNpeY
@ViaJables
ViaJables merged commit 7651071 into major/agentic-chat Sep 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants