From 94ee24c9f7375cdb3b282551d38bcd95dce85998 Mon Sep 17 00:00:00 2001 From: mattshax Date: Sat, 12 Sep 2026 18:18:21 +0000 Subject: [PATCH] docs: a change log and release notes generated from pull request descriptions Versions were tagged but never published as releases, so the change record existed only as pull request titles. Every merged pull request here carries a description written as a change note, so the notes for a version are those descriptions in order, and the change log is the same for every version at once. scripts/release-notes.mjs produces both from the tags and the gh CLI, dropping attribution trailers, and CHANGELOG.md is its output for all sixty versions to date. The README documents the release steps. Also makes the run-watch test deterministic. It slept a fixed span and asserted a count band, which is exactly the assertion that flakes on a loaded runner; it now waits for the give-up point and then proves no further polls happen. --- CHANGELOG.md | 3498 +++++++++++++++++++++++++++++++++++++ README.md | 16 + scripts/release-notes.mjs | 57 + server/test/runs.test.mjs | 10 +- 4 files changed, 3579 insertions(+), 2 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 scripts/release-notes.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..4d10ee0 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3498 @@ +# Change log + +Every version, newest first. Each entry is the description of the pull request that made the change, which is written as a change note when the change is made. + +## v1.59 (2026-09-12) + +### A manual macOS job that builds GUFI and exercises its vector stack (#337) + +Manual (`workflow_dispatch`) with inputs for the GUFI ref and whether to build the AI dependencies, since that path compiles llama.cpp and takes far longer than the test suite. + +It builds GUFI on a clean `macos-15` runner through `indexer/setup_gufi.sh`, indexes a small tree and queries it with `gufi_query`, then exercises the vector stack exactly as the Studio does: load the model, embed a phrase with `lembed`, store vectors in a `vec0` table, and take the nearest by distance. It ends by pointing the built server at that index to confirm binary discovery. + +The value is independent verification on a machine nobody has configured, which is the state a new Mac user is in, and it gives us a way to check a GUFI branch on macOS without owning a Mac. + +### Libraries: mount several indexes at once, read only beside the knowledge base (#338) + +The Studio built and owned its index, which suited a knowledge base and nothing else. It now mounts **libraries**: the knowledge base is the first, writable, and every request that does not name a library means it, so nothing that existed before changes. Any other library is read only: a site's root-built GUFI index or one someone handed over, reached through a local tree with an optional source root. + +**Registry** (`server/src/libraries.ts`): libraries come from the deployment (`STUDIO_LIBRARIES`, pinned; also an *Additional Libraries* field on the deploy form) or from Settings > Libraries, where an administrator adds one by path and the Studio probes the tree first (a GUFI index, the Studio's full-text tables, vectors, source readable). Nothing is asked of GUFI and nothing is written into the index. + +**Addressing**: a `library` parameter on every corpus and index route, defaulting to the primary. `gufi.ts` functions take an index root (per-root caches), `kb.ts` a source root. Read-only libraries refuse upload, move, rename, delete, labels, and re-index with a 403; a library with no files on this host answers 409 on file routes and the viewer says why. Chat tools read from the library the conversation names. + +**Client**: the Library rail header becomes the switcher when more than one library is mounted; the current library rides on every API call from one place (`api.ts`) and on deep links as `&lib=`. `STUDIO_SECTIONS` chooses which sections appear, so a site can run an index viewer with no assistant. + +**Verified** against a real second GUFI index on the dev server: probe reported capabilities per library; listing, filename search, and file reads followed the named library; the primary was untouched; mutations on the read-only library were refused (403), no-files library answered 409, unknown answered 404. + +Tests: 8 new server tests (registry, probe, addressing, guards, config), 3 new web tests (URL suffix, switcher, deep links). 182 server and 12 web tests pass. Docs: `docs/LIBRARIES.md`, README, ARCHITECTURE, `.env.example`, both deploy workflows. + +Not yet: enrichment sidecars for read-only libraries, the Query page's scope, cross-library search, remote libraries through GUFI's client. + +## v1.58 (2026-09-10) + +### A start-to-finish macOS guide, and a corpus default that works off a server (#336) + +**The first run failed for an unexplained reason.** `KB_ROOT` defaults to `/data/knowledge-base`, a server convention that does not exist on a laptop and cannot be created without root. When that path is absent the default is now `knowledge-base/` beside the code, seeded on first start, so `pnpm start` works from a fresh clone. Deployments set `KB_ROOT` explicitly and are unaffected. + +**`docs/MACOS.md`** is the path in the order worth doing it: run it first, add GUFI once it works, add a model last. It includes a table of what works in each of the three states, because a Mac user who builds with `GUFI_AI=0` still gets filename and full-text search over extracted document text and only loses the semantic blend, which is a much better story than "unsupported". Troubleshooting covers the failures that actually happen: Apple clang instead of Homebrew LLVM, and the llama.cpp build inside sqlite-lembed. + +GUFI binaries are not shipped here and should not be: the guide points at the GUFI project for installation, notes that a site package works, and documents that the Studio finds `gufi_query` on `PATH` or under `/opt/gufi/bin`. `indexer/setup_gufi.sh` remains available for building from source. + +174 server tests and 9 web tests pass, and the macOS CI job covers this on a real runner. + +## v1.57 (2026-09-10) + +### Build GUFI on macOS, test the Studio there, and correct the README (#335) + +The README said GUFI does not build on macOS. It does: upstream runs a `macos-15` job in its own CI with `-DDEP_AI=On`, and the difficulty is the toolchain rather than the platform, since libomp is not wired into Apple's clang and GUFI's scripts expect GNU utilities. + +**`indexer/setup_gufi.sh`** now handles macOS the way that CI does: Homebrew's LLVM as `CC`/`CXX`, libomp passed through the OpenMP variables, and coreutils/findutils/gnu-sed/grep/diffutils placed ahead on `PATH` for the build only. It also accepts `GUFI_AI=0`, which skips `sqlite-vec` and `sqlite-lembed` (the hard part, since lembed carries llama.cpp) for a fast build that keeps metadata, filename, and full-text search and loses only the semantic blend. `DEP_AI` defaults to On upstream, so a plain `cmake` attempts them; this makes the choice explicit. Installing to a prefix the user owns no longer calls sudo. + +**CI** gains a macOS job that installs Node, pnpm, tesseract and poppler, then builds and runs the suite. It deliberately does not build GUFI: that takes far longer than the tests and has upstream coverage. What this job catches is a Linux-only assumption in the server or a tool that only exists under apt. The repo is public, so macOS runner time is free. + +**README** now describes both build paths instead of claiming the platform is unsupported. + +## v1.56 (2026-09-08) + +### The suggestion chips size to their text, not the whole canvas (#334) + +`.chat-canvas > * { height: 100% }` forces every direct child to full height for the package's own layout, and the next-message suggestion row is one of those children. It filled the canvas, and with the default `align-items: stretch` each chip inflated with it, drawing giant circles over the conversation. + +`height: auto` plus `align-items: flex-start` on the row and `height: auto` on the chips. Measured in a headless browser against the live app: chips 26px tall in a 58px wrapped row, canvas 779px. The conversation scrubber and the persona buttons hit this same rule before; the comment names it so the next component does not. + +## v1.55 (2026-09-08) + +### The element between the app and the views can shrink, so pages scroll on a phone (#333) + +Measured in a headless browser at 390x844 rather than guessed at. + +`main.content`, between `.app-body` and the views, is a flex item whose default `min-height: auto` refuses to shrink below its content. In the phone's column layout that let a tall page grow the whole layout past the viewport: Settings measured **2102px inside an 844px app**, which pushed the navigation bar off-screen (`nav@2143`) and left nothing able to scroll, because `.help-content` and the other per-view scrollers never received a bounded height to work against. Chat looked fine only because its content already fits. + +`main.content { min-height: 0; min-width: 0 }` fixes it. Re-measured across every view, each now reporting an app that fits the viewport, the bar on the bottom edge, and a working inner scroller: + +``` +chat OK app=844/844 nav@844 +settings OK app=844/844 nav@844 help-content:735/2033* +help OK app=844/844 nav@844 +library OK app=844/844 nav@844 explorer:286/822* +stats OK app=844/844 nav@844 +agents OK app=844/844 nav@844 +search OK app=844/844 nav@844 +query OK app=844/844 nav@844 +``` + +## v1.54 (2026-09-08) + +### The mobile shell is pinned to the visual viewport, and a deploy is never cached (#332) + +Two faults, and the first hid the second. + +**Caching.** Every static file was served `public, max-age=0`: a phone could hold a stale shell after a deploy, while two megabytes of fingerprinted JavaScript were revalidated on every load. `@fastify/static` writes that header itself *after* any `setHeaders` hook, so `cacheControl: false` is required for ours to stand. Now `/assets/*` (content-hashed names) is `public, max-age=31536000, immutable` and the HTML shell is `no-store, must-revalidate`, so a deploy is picked up on the next load with nothing to clear. Verified on the live server. + +**Mobile shell.** The layout asked a chain of percentage heights to agree with a viewport that changes size as iOS slides its toolbars; it did not, so the bottom bar left the screen and the thread ran under the browser chrome. `.app` is now `position: fixed; inset: 0; height: 100dvh` under the 760px breakpoint, which is the arrangement iOS keeps correct through that transition. Out of flow, the page has nothing to scroll and the bottom bar cannot be carried away; views scroll inside themselves as before. Confirmed present in the built CSS. + +## v1.53 (2026-09-08) + +### Settings and help work on a phone, and three chat fixes (#331) + +Four things found on a phone. + +**Settings and Help on mobile.** Their 220px section rail sat beside a scrolling article; on a phone it took most of the width, and with the page itself no longer scrolling (the viewport fix) the article had nowhere to go, so the page looked frozen. The rail becomes a horizontal strip of section chips above the article, and views scroll inside themselves with `overscroll-behavior: contain`. + +**Chat controls overlapping the model picker.** They float over the package's header; on a phone the picker grew under them. Labels drop to icons there and the picker gets `max-width: calc(100vw - 190px)` with truncation (written defensively against the package's class names). + +**A retry duplicated the answer.** The client accumulates `content` events and cannot unsay them, so a retry after text had already streamed appended a second reply. A turn that has spoken is no longer retried; the error is surfaced. + +**Sol's first turn always failed.** That provider rejects every token-cap spelling (`max_output_tokens` explicitly, `max_tokens` and `max_completion_tokens` behind the masked 400; no cap works). The rejection is now remembered on disk, so only the first turn a deployment ever runs pays to learn it, and that retry keeps streaming, since a rejected parameter is not a generation failure and the unstreamed retry left the reader on a still "Thinking" line for the whole reply. + +Voice now opens at top level instead of in an iframe: the deployment is its own platform session on its own domain, so the frame demanded a second sign-in that mobile browsers cannot carry a cookie into. Proxying it from the Studio's origin needs a base path in Unmute's frontend build; noted for next. + +## v1.52 (2026-09-08) + +### The app fills the visible viewport, not the layout one (#330) + +On a phone the navigation bar floated above a band of background and the composer sat near the browser chrome. The dynamic-viewport fix was applied to `.app` alone, while `html, body, #root` were still `height: 100%` of the *layout* viewport, which on iOS is taller than the visible one: the app was laid out in a box bigger than the screen and the page itself scrolled. + +Under the existing 760px breakpoint, every ancestor now uses `100dvh` with `overflow: hidden`, so there is no page-level scroll and the bottom bar stays on the bottom edge; views scroll inside their own containers as before. The bar also gets `padding-bottom: max(2px, env(safe-area-inset-bottom))` to clear the home indicator, which requires `viewport-fit=cover` on the viewport meta for the inset to report a real value. Browsers without `dvh` ignore the declarations and keep today's behavior. CSS and one meta tag. + +## v1.51 (2026-09-08) + +### The model URL is the base, without /v1, and the brain is any API model (#329) + +Running the stack on a two-GPU node showed the Unmute backend appending its own `/v1`: a URL given with the suffix became `…/v1/v1/models` and the model never connected (`llm_up: false` with STT and TTS healthy). The workflow input is now **Model base URL** and says the suffix is added. + +The description also now matches what the architecture allows: only the speech services need a GPU (about 5.3 GB synthesis, 2.5 GB recognition, splittable across cards), and the conversational model is a configuration value. Point it at a Studio for the corpus, tools and workflows with an API model behind the voice (`studio-voice/`), or at the platform gateway for a plain conversation with that model. Settings text updated to match. Workflow and copy only. + +## v1.50 (2026-09-08) + +### The Unmute workflow starts on a real host (#328) + +Brought up on a two-GPU node (one busy card, one free 6 GB card). Three things the upstream compose assumed were not true there: + +- The speech services' start script runs `uvx hf auth login --token $HUGGING_FACE_HUB_TOKEN` unconditionally and dies on an empty token, although the Kyutai STT/TTS weights and stock voices are public (`gated: false`). A Studio start script mounted over the image's makes the login optional. +- GPU reservations by CDI device name failed on a stale CDI spec (`/dev/dri/card2` missing). Services are pinned by NVIDIA-runtime device index instead, with separate `stt_gpu` / `tts_gpu` inputs so the two can be split across cards (STT ~2.5 GB, TTS ~5.3 GB). +- The backend reaches a Studio running on the host through `host.docker.internal` (host-gateway). + +The compiled `moshi-server` lives in a named volume so only the first start builds it (about 90 s here with a warm crate cache). Verified: STT healthy on the 6 GB card at 2,516 MiB. Workflow-only. + +## v1.49 (2026-09-08) + +### Carry the features flags into the client config (#327) + +The client assembles its config object field by field, so the `features` block added to `/api/config` in #326 was dropped on the way in and the Voice button could never appear. One line. + +## v1.48 (2026-09-08) + +### Voice conversations as a feature preview, through Unmute (#326) + +Settings gains a **Feature previews** section; its first entry is voice conversations. A deployment switches it on and names the URL of an Unmute deployment (`voiceEnabled`, `voiceUrl`, or `VOICE_ENABLED=1` / `VOICE_URL`), `/api/config` reports `features.voice`, and a **Voice** button appears above the chat that opens Unmute's interface in an overlay (`allow="microphone; autoplay"`, with an open-in-new-tab fallback). + +Unmute (Kyutai, MIT) wraps any OpenAI-compatible text model with streaming STT, semantic turn-taking, and TTS. The model it wraps here is the Studio itself: the RAG endpoint now serves `studio-voice[/]`, the full assistant (tools, grounding, personas) with a spoken-answer style appended to the system prompt: one or two plain sentences, no markdown, lists, or paths, a few words before a slow tool call. It is advertised alongside `studio-agent` in `/v1/models`. + +`deploy/workflow-unmute.yaml` stands the stack up on a GPU node from the upstream compose: clones the repo, overlays a compose file that points the backend at the Studio (`KYUTAI_LLM_URL/MODEL/API_KEY`), disables the bundled `llm` service, publishes traefik on the endpoint agent's port, and serves it as a platform session. Unmute needs a 16 GB GPU for STT plus TTS; the Studio's model runs wherever it already does. + +Known limit of this slice: the transcript stays with Unmute; recording voice turns as Studio conversations comes when the turns flow through the Studio's own client. Help guide updated; two tests; 153 server and 9 web pass. + +## v1.47 (2026-09-07) + +### What to type next, as the composer's placeholder and chips, Tab to fill (#325) + +When a turn ends, `GET /api/chat/suggestions?conversation=` reads the tool calls stored with the last assistant turn (`suggestNext`) and offers up to three specific follow-ups: follow the run that was launched or show its output, launch a dry run for real, summarize a completed run, explain a failed one, open the file a search cited, run the workflow that was inspected, check on delegated agents, retry a failed tool step; a long plain answer gets "sources" and "save as a note". No extra model call. A reply ending in a question or `NEEDS DECISION:` offers nothing, and so does a conversation where the user spoke last. + +`NextUp` attaches to the package's composer textarea from outside (as the slash palette does): the first suggestion becomes the placeholder with "(Tab)" while the box is empty, Tab fills it through the native setter and an input event, the rest are chips above the composer, and typing hides everything. The palette's capture-phase Tab wins when it is open. Four tests on the rules; 151 server and 9 web pass. + +## v1.46 (2026-09-07) + +### The fleet: standing agents with goals that live in ticks (#324) + +The Agents tab gains a **Fleet** page, first among its three, as the operator base for long-running work. + +A standing agent (`server/src/fleet.ts`) has a goal, a persona, a model, a placement (local beside the studio, or campaign as a workflow run on a system), triggers, and a budget. It lives in **ticks**: a tick is one bounded delegated task (the existing engine, one agent, depth 0) whose objective is the goal, the event that woke the agent, and the tail of its own journal, so its memory is the journal on disk under the index base rather than a process a rollout would kill. Triggers: a cadence (15m to 1d, or manual), a workflow run ending (fed from the run registry), new or changed files under a watched knowledge-base folder, and a message from a person, answered on a tick that starts at once. A result ending in `NEEDS DECISION:` parks the agent as input-required under a **Needs you** strip. Budgets in ticks and tokens pause an agent that has used them. Pause, resume, retire, tick-now, and message are routes under `/api/fleet`. Every agent reloads at startup; an interrupted tick leaves the agent idle for its next trigger. Token usage per tick comes from the task's agents. + +Four starter goals ship as templates (`/api/fleet/templates`) for a demo on a small Slurm system: a nightly benchmark campaign (campaign_runner), a queue and run watcher (watcher), a results reviewer on a watched folder (reviewer), and a daily digest (reporter). The help guide describes the page. Five tests against a fake pw code cover the schedule tick with journal and tokens, the run-ended trigger and inbox, pause, tick budget, decision parking and messages, and reload after a restart. 148 server and 9 web tests pass. + +## v1.45 (2026-09-07) + +### Default personas, usable unselected, and token usage per agent and per turn (#323) + +Five personas ship with the studio for long-running fleet work: `campaign_runner`, `watcher`, `reviewer`, `steward`, `reporter`, each a short set of working rules under `extensions-starter/agents/`. `seedExtensions` now fills in starter files on deployments that seeded before they existed, through a ledger (`.seeded.json`): a starter file is copied only when absent and never seeded before, so an edited default is never overwritten and a deleted one never returns. The library gains **Duplicate**, which opens a copy in the editor as a new file. + +The system prompt carries a live persona catalog (rebuilt on every prompt) and a rule for using one without a selection: when a request clearly falls inside one persona's description the assistant reads it and follows its rules for that request, says which one it applied, and never switches mid-request; when delegating, each agent gets the persona that fits its objective. + +Token usage is counted where the data exists. `streamTurn` reads a `usage` chunk when the stream carries one (including a usage-only final chunk), the tool loop sums them (`addUsage`), and the total is stored on the message as `tokensUsed`. An agent's pw code envelope is read by `usageOf` for every spelling providers use; the count lives on the agent, sums on the task (`sumUsage`), and shows on the Tasks page row, drill-down, and card. Nothing is shown rather than zeros where the gateway reports nothing. + +Five new tests; 143 server and 9 web pass. + +## v1.44 (2026-09-07) + +### The semantic map caption says what proximity means (#322) + +The caption now states that the map places documents by similarity of the embeddings semantic search uses, projected to two dimensions, that the axes carry no meaning of their own, and that color is the cluster. Text only. + +## v1.43 (2026-09-05) + +### Neutral fixture names and comments (#321) + +Test fixtures, tool descriptions, the system prompt's example request, code comments, and the help guide use generic system and host names (vega, juno, example hosts), and the CFD training workflow's account field ships empty instead of a preset. No behavior change; 138 server and 9 web tests pass. + +## v1.42 (2026-09-05) + +### Runs launched from chat survive a rollout, and the page knows one happened (#320) + +A run lives on the platform and outlasts any Studio process; what did not survive a deploy was the knowledge that a conversation was waiting on it, and the process went down mid-answer with no drain. + +- **Run registry** (`server/src/runs.ts`): every real `run_workflow` launch is appended to `runs.jsonl` under the index base (preserved across redeploys) with the conversation and user that launched it. The server follows each run to its end (poll every 60 s, unref'd), re-attaches to every unfinished run at startup, gives up after 30 failed checks, and on a terminal state appends a note to the launching conversation via `appendConversationNote`, parented to the last message so it sits on the active branch. `GET /api/runs` serves the registry; the Tasks page lists runs with state and a link to the chat. +- **Drain**: `SIGTERM`/`SIGINT` now wait up to 20 s for in-flight streams (counted in the stream route) before `app.close()`, so a tool loop finishes and a launch gets registered. The deploy replaces the tunnel regardless, so this protects server-side work, not client connections. +- **Rollout notice**: the footer polls `/api/version` every 60 s and on tab focus; when the commit changes under an open page it shows "Updated to vX" with a Reload button. Conversations are saved server-side and survive the reload. + +Tool context now carries `conversationId` and `userId` into tools. Three tests on the registry (follow to the end with the conversation note; re-attach from the file after a restart; give up on a run the platform no longer reports); 138 server and 9 web tests pass. + +## v1.41 (2026-09-05) + +### Run detail shows the job's output, not the workflow's script (#319) + +`workflow_run_detail` handed the model the raw run record first. That record embeds the workflow's own script text and filled the 24,000-character output cap before a single line of job output; the errors and the log tail were truncated away. The assistant that had just launched a job could not see its result and went looking for it with shell commands on the cluster. + +`summarizeRunDetail` (exported, tested) now produces: run state with start and end times; each job's state, host, and per-step states; the platform's error report; and the tail of every step's output, failed steps first and longest, with the "(error fetching logs …)" placeholders dropped. Measured on a scheduler run: 24,000 characters without the marker or Slurm job id became ~1,500 characters with both. Three tests; 135 pass. + +## v1.40 (2026-09-05) + +### A scheduler submission with no partition gets one from the platform; help guide updated (#318) + +The first end-to-end submission on a scheduler-only site failed at sbatch with `No partition specified or system default partition`, which is what a Slurm system without a default partition says to any job that names none, and which a user asking in plain language should never see. When a workflow declares a `slurm-partitions` input and the request left it empty, `run_workflow` fills it with the system's `up` partition that has the most free nodes (from `pw environments ls`), after the saved-configuration merge so a configured partition always wins, and the tool result says which one was chosen and why. Account and QoS stay the caller's, since they are per-user site values. Three tests cover the choice (busiest-free up partition, never a down one), a caller-named partition left alone, and a direct-ssh submission left without one. + +Also: `server/scripts/e2e-hpc.mjs` accepts `E2E_PARTITION` and `E2E_SLURM` (a JSON object merged into the slurm group) for sites that need a partition, account, QoS, or GRES; and `docs/HELP.md` now covers the slash palette, HPC launching with the workspace auto-start, locked models, find in file, the search grammar, and the Agents tab's two pages. 132 server tests and 9 web tests pass. + +## v1.39 (2026-09-05) + +### A launch starts the user workspace when it is scaled down, then retries (#317) + +The platform runs a user's workflows in a per-user workspace that scales down when idle. A launch made while it is down fails with `User workspace not found or is not running` (and `Could not execute workflow` for a while after a start request), and the platform did not start it on demand for a CLI launch: every submission for the account failed that way for about half an hour tonight until `pw workspace start` was run by hand. + +`withWorkspace` (new `server/src/workspace.ts`) wraps the assistant's `run_workflow` and the campaign runner's agent submissions: on that failure it requests a start and retries the launch every 15 s until accepted or a four-minute budget is spent, then fails with a message that names the remedy. The launch itself is the readiness check (the status command's wording is not relied on); any other failure is rethrown immediately. The tool result and the campaign board both say when a start was needed. + +Six tests: the retry contract (start once, retry until accepted; healthy launch untouched; foreign errors rethrown before and after a start; budget exhaustion message; both wordings), plus `run_workflow` end to end against a fake CLI whose `workflows run` fails until `workspace start` has been called, asserting the exact call sequence. + +## v1.38 (2026-09-05) + +### A web test suite, and server coverage for the probe, model marks, slash forms, and defaults (#316) + +The web had no tests. It now runs vitest with jsdom and Testing Library under the root `pnpm test`, so CI exercises it: + +- the composer's slash palette end to end: opens on `/`, filters (`/sea` ranks `search_kb` first), Enter inserts `/search_kb ` through the native setter and an input event, and the Enter keystroke never bubbles to the composer's own handler; Escape closes; ordinary text never opens it +- the live theme hook, and an embed's `theme=` param following a toggle +- the scrubber's message-block selection, the library hash with its `&q=` query (moved into `nav.ts`, one implementation instead of three), and the find-in-file term parser + +Server side (node:test, stubbed `fetch`): the provider probe for every verdict it can reach (locked wording, `unlock_url`, the masked failure that only counts when it happens twice, healthy, cached, invalidated); `aiHealth` for its explained 401 and unlock link; the listing's model marking and inbound stripping, now the pure `markImpaired` and `stripAvailabilityMark`; three more slash-command forms; the delegation default and its `DELEGATION_ENABLED=0` kill switch; label normalization. + +123 server tests and 9 web tests pass. Lockfile updated with pnpm at the root. + +## v1.37 (2026-09-05) + +### A slash-command palette on the composer, and a repeatable HPC end-to-end check (#315) + +Typing `/` opens a palette above the composer listing everything slash-invocable (meta commands, skills, agents, tools from `/api/extensions` and `/api/chat/tools`), filtered as you type; arrow keys move, Enter or Tab inserts `/name `, Escape closes, mouse works too. The composer is the chat package's, so the palette attaches to its textarea from outside: a capture-phase keydown listener on the textarea sees the keystroke before the package's delegated handler and stops propagation, which is what keeps Enter from sending a half-typed command; acceptance writes through the native value setter and dispatches an input event so the controlled textarea updates. A MutationObserver re-attaches when the composer remounts on a conversation switch. + +`server/scripts/e2e-hpc.mjs` runs the HPC chain the assistant uses against a live platform through the same `executeTool` entry point the chat calls: `hpc_environments`, a real `run_workflow` with `scheduler: true`, `watch_run` to a terminal state, `workflow_run_detail`, and passes only when the run completes and a per-run marker comes back from the cluster. First pass today: `activatebatch-00009` on `a30gpuserver`, Slurm JobId 27, 33 s end to end. + +## v1.36 (2026-09-04) + +### A real query grammar, whole-word filenames, and no semantic noise for identifiers (#314) + +Quotes were stripped and every word AND-joined, so an exact phrase lost its adjacency and a literal `OR` became a search for the word "or". `parseSearchQuery` now understands the habits people arrive with: `"quoted words"` as an exact fts5 phrase, `OR` between alternatives, `-word` or `NOT word` to exclude (`(a NOT b)` form), `word*` for a prefix (`"word"*`), and whole words otherwise, which the index's unicode61 tokenizer already enforced for full text. Stopwords drop from bare terms only, never from inside a phrase. + +The partial-word matches people saw were never full text. They came from the filename substring search (`tin` inside `routine.md`) and from semantic vector hits. A filename term of three characters or fewer now has to start a word (approximated with separator-prefixed LIKE clauses), and semantic hits are omitted when the query is an identifier, an acronym, a quoted phrase, or carries operators, where a "similar" document reads as a wrong answer. + +Probed live against the dev index (all eight forms return without an fts5 syntax error); five new tests; 111 pass. The search box placeholder now states the grammar. + +## v1.35 (2026-09-04) + +### The find bar floats at the top of the scrolling pane (#313) + +It sat under the header and scrolled away with it, out of reach exactly when the reader was deep in the file. Now `position: sticky; top: 0` inside `.library-main` (the pane that scrolls), with a light shadow while the document moves beneath it. + +## v1.34 (2026-09-04) + +### The label overlay survives a rebuilt host, and recovers from the index (#312) + +On a filesystem that refuses xattrs (NFS), labels live in `tags-overlay.json` keyed by `.`. A rebuilt host mounts the same share under a new device id, so on its first index pass no entry matched, and `overlayPrune`, which treated "not seen" as "deleted", emptied the store. That is how a production deployment lost its labels on 2026-09-03 (host up 20:16, first deploy 20:35, overlay rewritten to an empty store at 20:35). + +- Identity tolerates the device id: an entry whose recorded path still resolves to the same inode and creation time is the same file and is re-keyed to the live device id, both in `reapplyTagOverlay` and in the prune. +- A prune that recognizes nothing at all is refused, since a corpus does not vanish between two index passes; the file is copied to `.bak` before any entry is removed. +- The index still carries the label rows written from the overlay, so an empty overlay beside an index with labels is treated as a lost overlay: `recoverOverlayFromIndex()` adopts them at startup (20 s after boot) and on demand via `POST /api/kb/tags/recover`. + +Three tests cover the host change (kept and re-keyed), a real deletion (pruned, backup written), and adoption. 106 pass. + +## v1.33 (2026-09-04) + +### A search result opens on its match, and every file has find (#311) + +Clicking a search result opened the file at the top and left the reader to find the match again. The result now opens with the query it matched: the viewer lands on the first occurrence, highlights every occurrence, and shows a find bar with a count, Previous/Next (Enter and Shift+Enter), and Escape to close. The same bar is available on any file from a new Find button in the viewer head. + +- Text and Markdown are searched in place, in whichever tab is showing. +- A PDF or office file previews as page images, which cannot be searched, so when opened from a search it lands on the **Indexed text** tab, which holds the very text the index matched; on the Preview tab the bar offers a one-click switch. +- Matches are painted with the CSS Custom Highlight API (`CSS.highlights` + `::highlight()`) rather than by wrapping text nodes in ``, so React's DOM (Streamdown output, the text bodies) is never rewritten underneath it and re-renders cannot trip over injected marks. Browsers without the API get the landing tab and the count-free bar, no highlight. +- The query travels in the URL hash as `#open=file:&q=`, so a link to a match is shareable and chat citations can carry one later; the `#open=` click handler and the hash parser both accept it. + +Search-side quoting and fts5 operators (AND/OR/NOT/NEAR) are stripped from the find terms. + +## v1.32 (2026-09-04) + +### The empty-chat mark and embeds follow a theme toggle (#310) + +Both read `document.documentElement.dataset.theme` once at render, so after a toggle the empty chat kept the other theme's icon until something else re-rendered it, and an embedded DAG or HTML page kept its old theme. `useEffectiveTheme()` subscribes to the attribute App writes (a MutationObserver on `data-theme`), and both consumers read from it: a toggle swaps the mark immediately and changes an embed's `theme=` param, which reloads it in the new theme the same way a fresh open would. + +## v1.31 (2026-09-04) + +### A rejected or expired credential is an explained state, not an internal error (#309) + +When a stored platform token aged out, three surfaces failed the user at once: the picker said "Failed to load models, internal error (ref …)" because the listing route let the 401 escape as a 500; Settings said "Your key ending NA== is not working (auth)", where the suffix was base64 padding; and the health line said "401: Unauthorized". None of it said the one thing that mattered: platform tokens last 24 hours from login, and an API key does not expire. + +- The listing answers a rejected credential as a structured 200 (`credential: 'rejected'`) with a message that names the expiry time when the token's payload carries one, or the likely cause when it does not, plus the remedy; the deployment-credential case points at the operator and the personal fallback. The thread banner shows it. +- Settings' headline reads "expired at