Skip to content

feat(stt): manage speech models in a table with download, select and remove - #17624

Open
Pearcekieser wants to merge 1 commit into
kirodotdev:mainfrom
Pearcekieser:feat/voice-settings-model-table
Open

Pearcekieser wants to merge 1 commit into
kirodotdev:mainfrom
Pearcekieser:feat/voice-settings-model-table

Conversation

@Pearcekieser

@Pearcekieser Pearcekieser commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Problem / Motivation

Goal: In Settings > Voice > Speech-to-Text, a user can see every local speech model's size and state, and download, select or remove each one from one place.

The Model row is a dropdown. It shows the state of the selected model only, and a Download button appears only when that model is missing. A user cannot see which other models are on disk, cannot fetch a model without first selecting it, and cannot delete one. Models run from 78 MB (tiny) to 1.6 GB (large-v3-turbo), so a user who tried the large one has to delete files under <data home>/models/whisper/ by hand to get the space back. A transfer of an unselected model is also invisible: the panel shows progress only for the selected model.

Why it matters

The space cost is real and the only remedy today is a shell command in the gateway's data directory. Without a list of what is installed, a user also cannot tell which models the next selection will download.

Not a goal

What changed (motivation → approach → change)

A user can now manage the speech models from Settings instead of the data directory, and the panel says what each model costs and whether it is on disk.

Before After
dropdown, selected model's state only one row per catalog model: name, size, Selected / Installed / Not downloaded, live progress; a failed download or a refused Remove gets a full-width row under that model
Download only for the selected model Download on any missing row; every Download waits while one transfer runs
no removal Remove on installed, unselected rows, behind a confirmation that names the size

The new route DELETE /api/stt/models/{name} in src/kiro_crew/dashboard/handlers/core.py looks the name up in the catalog, so an alias, a filename or a traversal is a 404 and never becomes a path. ModelStore.remove in src/kiro_crew/stt/models.py deletes only models_dir() / model.filename; every catalog filename is one plain component, which a test pins, so the delete cannot name another directory. A symlink at that path is unlinked as a link, never followed. It refuses with 409 and a code instead of changing anything else: the configured model, aliases included (stt_model_selected); a model that work still reads (stt_model_in_use): a live voice session keeps the model it started with after the user selects another, and a batch transcription fixes its model before the upload and transcode awaits. A removed file would make that work re-download it, or fail offline and lose the audio. Both take a ModelClaim from src/kiro_crew/stt/models.py (LocalSession until finish has decoded or cancel runs, POST /api/stt/transcribe for the whole request, _transcribe_local for the Slack path, and a meetings audio import from its config snapshot until the transcript is back, so a split import keeps its model across every segment; the transcribe route, the meetings import, Slack voice memos and channel attachments all take it through transcribe.local_model_claim, which claims only for the local provider), and ModelStore.remove refuses a claimed model in the same synchronous step as the unlink. The route also checks before releasing anything. Claims sit in a weak set, so a holder dropped without releasing cannot pin a model; the model resident in the recogniser (stt_model_in_use), matched on the residency key's full path and never the file name, because a custom model may sit in a subfolder under a catalog file name (sub/ggml-small.bin) and is then the user's own selected model; any model while the recogniser is loading one (stt_model_loading); and the model being downloaded (stt_model_downloading). A deselected model is often still resident, because only the idle sweep unloads it (600 s by default). Removing it is the table's main use, so the route first releases it with the new WhisperEngine.release_if_resident, under the load and decode locks the way idle eviction takes them, and re-reads the selection after that await; stt_model_in_use then answers only a model something put back. The loading check reads the new WhisperEngine.loading in src/kiro_crew/stt/engine.py, which covers the load lock and a build that outlived its timeout, because the resident key is set only when a load finishes. No await sits between the remaining checks and the unlink, so no load or download can start in the gap.

SttModelTable in website/src/pages/settings/SttSettings.tsx marks the row the status endpoint resolves the configured model to, so an old medium config shows large-v3-turbo as Selected; the download prompt and the CPU-slowness warning follow the same resolved name, so a medium config on a CPU-only build now gets the warning large-v3-turbo always had. Every row has a state badge: Installed, Not downloaded, or progress. The old dropdown showed no line for an installed selected model, and the table keeps that, adding no reassurance line under it; the per-row Installed state is what lets a user see what is on disk, which is the point of the table. Each refusal code maps to a sentence that says what to do, shown under the row it was for. When the selected model is not on disk yet, the download prompt is a row under that model, not a line below the table. A failed download says in plain words that it stopped and to choose Download now again; the gateway's reason rides in the agent hand-off. The row button reads Select, the same word as the Selected badge. The selected row never offers Remove and adds no standing line explaining why; trying to remove the configured model through the API returns the stt_model_selected sentence. At most two buttons per row. The table uses the shared table-striped class, and each model's rows carry that model's stripe, so a failure or refusal row stays on its model's band instead of shifting the stripes below it. Below sm each row stacks its parts (name and size, then the status badges, then the buttons) and the column header is hidden: at 320px the pane is 288px wide, and in German the "Nicht heruntergeladen" badge beside a "Jetzt herunterladen" button pushed the buttons 74px past the edge. The new client call is api.sttDeleteModel in website/src/api/client/voice.ts. website/scripts/settingsExtract.ts indexes the table as the stt.model setting, so the settings registry and its deep link are unchanged.

Backwards compatibility

Compatible: GET /api/stt/status, POST /api/stt/prepare and the stt.model config key are unchanged; the new route only adds a way to delete a file the user could already delete by hand.

Removes nothing: the Model dropdown is replaced by a table that writes the same stt.model key, keeps the label, hint and configKey, and the registry entry for voice.model is byte-identical. The retired model_option catalog key had no other reader.

Tests

  • test/test_stt_model_delete.py, 40 tests: a model claimed by a batch transcription, or claimed during the release, is refused stt_model_in_use, and ModelStore.remove refuses a claimed model itself; the transcribe route and the local batch path hold the claim for the whole request and drop it after; a deselected model a live session still uses is refused stt_model_in_use without being released, and removed once that session is cancelled; a session started during the release also refuses it; a finished or garbage-collected session does not pin its model; a deselected model still resident (the idle sweep has not run) is released and removed; one something put back during the release is refused stt_model_in_use; one selected again during the release is refused stt_model_selected; a non-resident model releases nothing; release_if_resident drops only the named file and waits for a running decode; a selected, resident custom model at sub/ggml-small.bin is neither released nor touched when small is removed, and release_if_resident leaves it loaded; removal of an installed unselected model and only its file; a second removal is a no-op; refusals for the configured model, a configured alias, the resident model, any model during a load, and the downloading model; a download of another model does not block; exact catalog names only, with alias, filename, traversal and case variants all 404 and untouched; an app claim is 403; the route is registered; WhisperEngine.loading for an idle engine, a held load lock and a load past its timeout; the store resets a stale ready status and removes a symlink without its target; every catalog path is one file directly in the models directory.
  • test/test_meetings_audio_import.py, 3 new tests: a local-provider import holds its model claim through the duration probe and the split, for a split that succeeds and one that fails, and drops it after; a cloud-provider import claims nothing. Both local cases fail without the route's claim.
  • website/src/test/SttSettings.modelTable.test.tsx, 17 tests: the shared striped-table class, and a notice row on its model's stripe with the stripes below it unshifted; every row's state; an alias selects the resolved row; no Remove on the selected row and no standing hint; the selected row offers Download when its model is missing; a failed download renders on its own full-width row with a plain sentence and keeps Download; a refusal renders under its row; at most two buttons per row; Select writes stt.model; Download fetches without selecting; one transfer holds every Download; Remove confirms with the size, deletes, and re-reads status; cancel deletes nothing; each 409 code shows its sentence.
  • Existing panel tests now address the table instead of the dropdown: SttSettings.modelDownloadDisclosure, SttSettings.surface, SttSettings.decoder. ApiClient.refactor.surface pins the new client key; destructiveConfirm records why the two confirm keys need no quoting.

The loading refusal test fails with the engine.loading check removed.

Catalog keys under pages.settings.sttSettings.model_* are translated in all 12 shipped languages in website/src/i18n/locales/, with en-XA.json regenerated and a translator note for the Select button in website/src/i18n/en.context.json. src/kiro_crew/docs/configuration.md, docs/system-specs/modules/stt-streaming.md, docs/system-specs/modules/learn-cron-dashboard.md and docs/feature-map/README.md describe the route and the table.

Manual verification

End to end in a disposable pod on an earlier head (before the deselected-model release, which the unit tests above cover), with sparse files of the exact catalog sizes planted under the pod's models/whisper (no real model downloaded), stt.model=base:

Request Result
DELETE /api/stt/models/small 200 removed: true, file gone, status shows small not present
same again 200 removed: false
DELETE /api/stt/models/base 409 stt_model_selected, file kept
medium, ggml-small.bin, ..%2Fconfig.json 404
no token 403

Not exercised in the pod: stt_model_in_use, stt_model_loading and stt_model_downloading, which need a real whisper load or a live transfer; the unit tests above cover them. A browser pass against the pod stopped at its first-run onboarding, so the screenshots come from website/capture/voice-settings.tsx (shipped SttSettings, API stubbed) in Chromium.

Screenshots / video

The Custom model row, added by the rebase onto #14918. These frames come from main's website/scripts/capture-stt-custom-model.mjs, which serves the real built SPA from this head with the API stubbed. The Custom row is always the last row of the table. It has no size, and while it is not selected its only action is Select, with no Download or Remove.

Custom row, not selected, a catalog model in use

Custom selected while its file is missing from the speech models folder. The row has no actions, the status reads "Model file problem" and the reason shows under the file name field:

Custom selected, file missing

Custom selected with its file in place, shown as Selected and Installed:

Custom selected, file present

The frames come from website/capture/voice-settings.tsx (shipped SttSettings with the API stubbed). The wide frames are from an earlier head, before the table was striped; this head changes nothing else in the wide layout. The 320px, 390px and German 820px frames are from this head and show the stripes. The two refused-Remove frames are from this head too: website/capture/voice-settings.tsx takes &refuse=stt_model_in_use to stub the 409, and they show the current text, "Voice input is still using this model. Try again when it finishes."

At 320px in German, before and after (the GPT narrow-viewport finding). A check in the capture script measured every table button ending at 394px in a 320px window before, and inside the window after:

Before, from an earlier head, with the model name broken at its hyphens and the button label wrapping:

before, German 320px, a model not downloaded

After, on this head:

after, German 320px, a model not downloaded

German 320px idle, before and after, and German at 820px

Before, from an earlier head:

before, German 320px, idle

After, on this head:

after, German 320px, idle
after, German 820px

Before, dark (main's dropdown):

before

After, dark:

after

After, a transfer of an unselected model running (main showed nothing for it):

after downloading

After, Remove confirmation:

after confirm

After, a refused Remove (stubbed 409 stt_model_in_use), on a row under that model, with the Ask the agent hand-off:

after refused
after refused narrow

After, a failed download on its row, at 390px:

after failed narrow

After, the selected model not yet downloaded, with the download prompt on the row under it (German, 820px):

after selected missing

Failed download at full width, narrow table, light theme, and main during the same transfer

after failed
after narrow
after light
before downloading

Related Issues

Closes #17602

Review items deferred from this PR, one issue each:

Checklist

  • At most two commits (one is the norm), with a Conventional Commits title (feat|fix|docs|style|refactor|perf|test|chore|ci|build|revert: ...)
  • Existing tests pass and new tests added for new functionality
  • Self-review completed; code follows project style guidelines
  • Documentation updated (if applicable)
  • No secrets, credentials, or internal references in the diff

Contribution License Agreement

Contributions are accepted under the project's Apache-2.0 license; see the Licensing section of CONTRIBUTING.md.

@Pearcekieser
Pearcekieser requested a review from a team as a code owner October 7, 2026 07:01
@Pearcekieser
Pearcekieser requested a review from buluoray October 7, 2026 07:01
@github-actions github-actions Bot added fork Pull request from a fork (external contributor) readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Oct 7, 2026
@Pearcekieser
Pearcekieser force-pushed the feat/voice-settings-model-table branch from cdeb2c1 to a9a4d59 Compare October 7, 2026 07:46
@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Design Review (Opus 5.5, fork) — ✅ PASS

Design-level review of 384569df6e4546f0515779d4e55451f398746b7a via the fork AI-review pipeline — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS does not.

Design-Verdict: PASS

No design-level blocker: removal goes through catalog names only, refuses every model that something is still reading, and rendered evidence is attached.

Notes

  • The claim mechanism (ModelClaim, claim_any, _accepting, _transfers, building_path) now runs through five callers and the engine. A new STT caller that skips local_model_claim silently brings back the delete-under-read race. Consider a test that lists every transcribe_audio call site.
  • The take-away check covered only the model_option catalog key. Its only reader is SttSettings.tsx, which this patch rewrites.

[DESIGN-REVIEWED] 384569d

Description read

[DESCRIPTION-READ] d43a629e3e8ceac8c1bb559ec7c690eaebe5e2826afd4c274b5563e292025080

That sha256 names everything this verdict read: the PR description, plus the 1 evidence file(s) the workflow collected from it and handed this review. It covers that evidence because the media strip replaces every attachment URL with the same placeholder, so an edit swapping one attachment for another would leave the description's captured text identical while what this review judged changed. Recompute it with .github/scripts/pr-description-capture.sh over the same inputs; if it differs, the description or that evidence changed after this verdict was formed and any finding drawn from either is unproven.

@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

First Principles Review (Opus 5.5, fork) — ✅ PASS

Premise-level review of 384569df6e4546f0515779d4e55451f398746b7a via the fork AI-review pipeline — why this exists and whether the shipped surface is the smallest honest version. Updated in place on each push. A BLOCK verdict blocks PR readiness; PASS does not.

First-Principles-Verdict: PASS

Nothing blocks. Every item serves the frozen Goal. The one cost worth weighing is how far the model-claim mechanism spreads.

Notes

  • Item 5 — oversized: claims are taken at seven sites (LocalSession, ensure_loaded, _transcribe_local, the transcribe route, meetings import, Slack memos, channel attachments), plus claim_any/claimed_stt_snapshot. They guard one race: a Remove landing between a config read and a load. Its worst cost is "re-download it, or fail offline and lose the audio". Subtraction: keep the claims in WhisperEngine.ensure_loaded and LocalSession (every load passes through the first). Delete claim_any, claimed_stt_snapshot, local_model_claim and the five per-caller wrappers, and accept a re-download in that window.

What this change ships

Inventory (9 items) — 8 justified

Intent: let a user see, download, select and delete local speech models from Settings instead of the shell — ADDITION.

  1. The Model dropdown is replaced by a table with one row per model, showing size and Selected/Installed/Not downloaded — justified
  2. Download is offered on any missing model, not only the selected one — justified
  3. A Remove button on installed, unselected models, with a confirmation naming the size (DELETE /api/stt/models/{name}) — justified
  4. Remove is refused (409 with a reason sentence) for the selected, downloading, loading or in-use model — justified
  5. Live sessions, batch, meetings, Slack and attachment transcriptions now claim their model so Remove can't delete it mid-use — oversized
  6. A deselected model still in memory is released immediately on Remove instead of waiting for the idle sweep — justified
  7. A legacy medium config now shows large-v3-turbo as Selected and gets the CPU slowness warning (configured_model status field, 1 consumer) — justified
  8. Progress and failures for any model's download now show on that model's row; the failure reason moved into the agent hand-off — justified
  9. A download whose caller was cancelled keeps running, and a new request joins it instead of starting a second writer — justified

[FIRST-PRINCIPLES-REVIEWED] 384569d

Description read

[DESCRIPTION-READ] ad44921e03f77f72bb4c797146f1562322541efe6340a26655683d75cad7a10d

That sha256 names the PR description this verdict read. Recompute it with .github/scripts/pr-description-capture.sh; if it differs, the description changed after this verdict was formed and any finding drawn from the description is unproven.

@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

UX Review (Opus 5.5, fork) — ✅ PASS

UX-level review of 384569df6e4546f0515779d4e55451f398746b7a via the fork AI-review pipeline — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS does not.

UX-Verdict: PASS

No blockers. The screenshots cover every control and state the diff adds, but no first-time reader has looked at them, because this fork lane runs a single pass. Pushing the branch to this repository runs the blind read.

Notes

  • attachment-04 and attachment-06 split "large-v3-turbo" and "Jetzt herunterladen" over two lines. attachment-07 shows the fix (whitespace-nowrap), so retake 04 and 06 or drop them from the description so they don't read as current.

[UX-REVIEWED] 384569d

Description read

[DESCRIPTION-READ] bfa0e29e5f0f47013c832e7d3d55f14e9f2fc54226348036f0b7ae1ea950e0a9

That sha256 names everything this verdict read: the PR description, plus the 22 evidence file(s) the workflow collected from it and handed this review. It covers that evidence because the media strip replaces every attachment URL with the same placeholder, so an edit swapping one attachment for another would leave the description's captured text identical while what this review judged changed. Recompute it with .github/scripts/pr-description-capture.sh over the same inputs; if it differs, the description or that evidence changed after this verdict was formed and any finding drawn from either is unproven.

@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Opus 5.5 Review (fork) — ✅ no blocking findings

Reviewed 384569df6e4546f0515779d4e55451f398746b7a via the fork AI-review pipeline; updated in place on each push.

Review details

No findings.

Notes:

  • If any model is loading, the remove handler skips releasing a deselected resident model and answers stt_model_in_use. That 409 goes away on retry, but the spec says another model's load does not block a removal.
  • When a remove is refused because the file is being verified, the refusal uses the default stt_model_downloading code, so the panel tells the user the model is downloading.

[OPUS-REVIEWED] 384569d

@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

GPT 6.1 Review (fork) — ✅ no blocking findings

Reviewed 384569df6e4546f0515779d4e55451f398746b7a via the fork AI-review pipeline; updated in place on each push.

Review details

No findings.
[GPT-REVIEWED] 384569d

@Pearcekieser

Pearcekieser commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor Author

span=ec7d7961be59

  • Three words for one action.

fixed in fa3bd9c: the row button reads "Select" ("Select {{name}}" for its accessible name), matching the "Selected" badge and the hint "select another model first", in all 12 languages.
The evidence gaps are closed in the description: a refused Remove (stubbed 409 stt_model_in_use), a failed download at 390px, and a selected model not yet downloaded, all captured at this head. The 390px failure frame showed the error wrapping one word per line in the status cell, so the failure now renders in a full-width row under its model; the test "shows a failed download on its own full-width row" pins it.

@Pearcekieser

Pearcekieser commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor Author

span=6b1de615f2e3

  • Item 7: Installed badge on every on-disk row.

rebutted: the goal is that a user can see each model's installation state (#17602: "understand each model's state from settings"). A table row needs a state; without Installed, an installed selected row would show only "Selected" and read the same as a selected model that is not on disk.
The recorded decision was against a separate reassurance LINE under the dropdown for the ordinary case. The table keeps that: no line appears under it for an installed selected model. In fa3bd9c the test comment in SttSettings.modelDownloadDisclosure.test.tsx states this instead of the old wording.

@Pearcekieser

Pearcekieser commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor Author

span=b265d3e80576

  • Item 9: CPU warning follows the resolved name.

accepted as described: a medium config loads large-v3-turbo, so the slow-on-CPU warning applying to it is correct. The description now states it.

@Pearcekieser
Pearcekieser force-pushed the feat/voice-settings-model-table branch from a9a4d59 to fa3bd9c Compare October 7, 2026 08:16
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Oct 7, 2026
@Pearcekieser

Pearcekieser commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor Author

span=12977fcca565

  • A guard that can never fire, presented as protection.

fixed in 2865c06: the branch and MODEL_REMOVE_OUTSIDE are removed. Confinement now rests on what holds by construction: the path is models_dir() / model.filename, and the new parametrized test test_every_catalog_path_is_one_file_directly_in_the_models_dir pins that every catalog filename is one plain component directly in that directory.
The description and docs/system-specs/modules/stt-streaming.md say that instead of claiming a redirect guard.

@Pearcekieser

Pearcekieser commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor Author

span=86e05a4c8cfc

  • Item 6: the outside-store refusal has no reachable input.

fixed in 2865c06: removed, as above; the test that reached it only by monkeypatching model_path is replaced by the catalog-path invariant test.

@Pearcekieser

Pearcekieser commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor Author

span=9eebae47fd57

  • The removal refusal is shown away from its row.

fixed in 2865c06: a refused or failed Remove renders in a full-width row directly under the model it was for, the same shape as the failed-download row. The refusal test asserts the notice sits inside stt-model-small-remove-error; the description attaches it at 820px and 390px.

@Pearcekieser

Pearcekieser commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor Author

span=2c8aa6fc69d9

  • The failed-download row shows raw diagnostics.

fixed in 2865c06: the row says "The download stopped before it finished. Select Download now to try again." (new key, 12 languages; the old download_failed_reason key had no other reader and is removed). The gateway's reason goes into the agent hand-off report as its detail.

@Pearcekieser

Pearcekieser commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor Author

span=1e4cecfe0caf

  • Suggestion: hint always visible.

fixed in 2865c06: the "select another model first" hint shows only when the selected model is installed, the one case it explains; the selected-not-downloaded test asserts it is absent there.

@Pearcekieser
Pearcekieser force-pushed the feat/voice-settings-model-table branch from fa3bd9c to 2865c06 Compare October 7, 2026 10:21
@github-actions github-actions Bot removed the readiness: action required A blocking check or review needs attention label Oct 7, 2026
@Pearcekieser

Copy link
Copy Markdown
Contributor Author

Rebased onto main 561b345 as 1c780aa to clear a conflict in website/src/i18n/en.context.json. Main had appended new entries where this PR adds pages.settings.sttSettings.model_use. I kept every main entry and put this PR's key after them. The range-diff against de83cb6 changes only that file, and the rest of the PR diff is identical. Locally the backend file list passes 1,604 tests, and tsc, i18n and vitest (192) are clean. This push also re-runs GPT 6.1 Review, which ended "review incomplete" on de83.

@Pearcekieser

Pearcekieser commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor Author

Backend Lint & Type Check (3.12) failed on 1c780aa because of a flake8 error on main. Nothing in this change caused it.

flake8 reports src/kiro_crew/publish_governance.py:299:57: F821 undefined name 'lists_checked_by_caller'. The error arrived on main when #24498 and #24343 landed together, and main b06d6a8 fails the same check. This PR does not touch that file. #24764 tracks it, and #24767 is open to fix it.

Job: https://github.com/kirodotdev/KiroCrew/actions/runs/40460056895/job/122047892437

A retrigger cannot pass until #24767 lands, so I will not push before then. Once it does, I will rebase onto main.

Opus 5.5 has no findings on 1c780aa, and UX passed.

@Pearcekieser

Pearcekieser commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor Author
  • Explain why the other Download buttons are disabled during a download.

    acknowledged, deferred to the follow-up that exposes active-transfer state in /api/stt/status. That is the same change as the downgraded GPT finding on 140ccda and the UX note on de83cb6, and both touch this gating.

@Pearcekieser

Copy link
Copy Markdown
Contributor Author

Rebased onto main f3a7d42 as 826debe to pick up #24767, which fixes the flake8 F821 in publish_governance.py that failed Backend Lint (and Coverage Gate after it) on 1c780aa. The rebase had no conflicts, and the PR diff is unchanged. Locally the backend file list passes 1,604 tests, and tsc, i18n and vitest (192) are clean.

I also edited the description for the UX note on attachments 04 and 06. My disposition said I would re-capture them, but I captioned them instead. They are the deliberate "before" frames for the narrow-layout fix, each sits next to its "after" frame from this head, and a fresh capture would only reproduce the "after". Each one now has a line saying it is from an earlier head and what it shows.

@Pearcekieser

Copy link
Copy Markdown
Contributor Author

Backend Tests (3.12, 2) failed on 826debe because of another stale strict xfail on main. Nothing in this change caused it.

test/test_crew_log_fullscan_guard.py::test_a_warm_session_read_that_earns_a_savepoint_reads_only_the_gap is marked xfail(strict=True). #24276 fixed the behaviour, so the test now passes and strict mode turns that into a failure. It fails the same way on a clean checkout of main 2ec42c9. #24765 tracks it, and #24774 is open to drop the marker. This PR does not touch crew-log code, and the other 31,415 tests in the shard passed.

Job: https://github.com/kirodotdev/KiroCrew/actions/runs/40485411971/job/122144664292

A retrigger cannot pass until #24774 lands, so I will not push before then. Once it does, I will rebase onto main.

@Pearcekieser

Copy link
Copy Markdown
Contributor Author

Rebased onto main 6632fd7 as 6c7daa5 to pick up #24774. That change drops the stale strict xfail on test_a_warm_session_read_that_earns_a_savepoint_reads_only_the_gap (#24765), which failed Linux and Windows shard 2 and then Coverage Gate on 826debe. The rebase had no conflicts, and the PR diff is unchanged. Locally the guard file passes 10 with 4 xfailed, the backend file list passes 1,604 tests, and tsc, i18n and vitest (192) are clean.

@Pearcekieser

Copy link
Copy Markdown
Contributor Author
  • Item 6 is oversized: claim_any and claimed_stt_snapshot guard a window of a few milliseconds.

    acknowledged, kept. The verdict is PASS and the note is advisory. claim_any came in for a GPT finding on an earlier head. Between a caller reading stt.model and taking its claim, a removal of that model could pass the claim check and delete the file the caller was about to load. The suggested shape, which reads the config and then calls local_model_claim(stt_config), takes the claim after the read and so reopens that gap. Claiming every model across the read closes it, and the claim is released as soon as the config names a model. Removing it would bring back a finding the gate has already raised once. I would rather a maintainer make that call than trade one review lane against another.

@Pearcekieser

Copy link
Copy Markdown
Contributor Author

Rebased onto main c55d16f as 3ec5ff8 to clear a conflict with #24769 in src/kiro_crew/apps/builtins/meetings/backend/routes/audio_import.py. #24769 only removes review-round tags from comments, and this PR re-indents the same block under async with claimed_stt_snapshot(...). I kept the PR side and reapplied the three #24769 comment edits to the re-indented lines. The resolved file has the same AST as the PR version, and ignoring whitespace its diff against main contains only this PR's change. The range-diff against 6c7daa5 shows only those three comment edits. Locally the backend file list passes 1,604 tests, black is clean on the file, and tsc, i18n and vitest (192) are clean.

This push also re-runs Cross-Platform Portability and Automated Rule Check, which were cancelled on 6c7daa5 after every other check had passed.

@Pearcekieser

Copy link
Copy Markdown
Contributor Author

Rebased onto main 1b91dd9 as 87f07b8 to clear another conflict in website/src/i18n/en.context.json. Main had appended entries where this PR adds pages.settings.sttSettings.model_use. I kept every main entry and put this PR's key after them. The range-diff against 3ec5ff8 changes only that file. Locally the backend file list passes 1,607 tests, and tsc, i18n and vitest (192) are clean.

This push also re-runs the checks that did not settle on 3ec5ff8. GPT 6.1 and Opus 5.5 ended "review incomplete", Security Scope Review reported no leg, and Backend Tests (Windows) (7) hit the known timeout #23365. None of them was a finding. All three review lanes passed on 6c7daa5, which has the same code.

@Pearcekieser

Copy link
Copy Markdown
Contributor Author
  • Pending prewarm can reinstall a successfully removed model.

    fixed in 88dc875. I moved the claim into WhisperEngine.ensure_loaded itself instead of adding one to the prewarm handler. The method now claims its model before its first await and releases the claim when it returns, raises or is cancelled. Every load goes through it, so prewarm, the batch decode and live sessions are all covered from the first line, including the binding probe that runs before the load lock. Previously neither loading nor any claim covered that probe window. This is the fifth removal-race finding on this PR. The earlier fixes each added a claim to one caller, and this one makes the load path claim for itself, so the next caller that loads a model cannot miss it. TestEngineLoadClaims in test/test_stt_model_delete.py stalls a real prewarm in its probe and checks that removal is refused stt_model_in_use and that the file stays. It also checks that a cancelled load releases its claim. Both tests fail on 87f07b8 and pass here.

@Pearcekieser

Copy link
Copy Markdown
Contributor Author
  • While the first load of an undownloaded model is fetching it, Remove fails for every other model.

    fixed in 384569d. The route no longer refuses on the global engine.loading. That check existed because the route could not tell which file a load was reading until it finished. Since 88dc875, WhisperEngine.ensure_loaded claims its own model for the whole call, so the claim check already refuses the model being loaded, and a load or download of another model no longer blocks. The one reader a claim misses is a native build that outlived its timeout, which keeps reading after ensure_loaded returns. The new WhisperEngine.building_path names that file while the build is still running, and the route refuses stt_model_loading only when it matches. In test/test_stt_model_delete.py, test_a_load_of_another_model_does_not_block_removal removes small while a load builds large-v3-turbo. It got 409 on 88dc875 and gets 200 here. Two more tests cover the remaining cases: an overrun build refuses its own file, and a load still probing refuses its own model through the claim. building_path is None while only the lock is held. The stt_model_loading copy ("Try again in a few seconds") now applies only to the overrun build, and I updated the spec in docs/system-specs/modules/stt-streaming.md and the route's docstring to match.

@Pearcekieser

Pearcekieser commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor Author
  • While any model loads, a deselected resident model is not released and answers stt_model_in_use.

    acknowledged, not changed here. The note is advisory and the verdict is clean. The release is skipped on purpose: release_if_resident takes the load lock, and during a first load that lock is held for the whole download, so releasing inline would hang the DELETE request for up to 1.6 GB of transfer. The 409 is accurate, because that model really is resident, and the load in flight drops it before building the new one, so a retry succeeds. The spec sentence covers the non-resident case. I would rather tighten its wording in the follow-up than push for it now.

@Pearcekieser

Copy link
Copy Markdown
Contributor Author
  • Watch: a "Select" button beside a "Selected" badge.

deferred to the follow-up PR: the selected row already shows no Select button, so the two never share a row, but adjacent rows can show one of each. Renaming the button means a new string in 13 catalogs, so it goes into the follow-up with the other table wording items rather than this push.

@Pearcekieser

Copy link
Copy Markdown
Contributor Author
  • Suggestion: Select does not line up down the actions column.

deferred to the follow-up PR: Select moves to a fixed first slot there, alongside the Select and Selected wording item.

@Pearcekieser

Copy link
Copy Markdown
Contributor Author
  • Suggestion: the "Model" field label sits above a "Model" column header.

deferred to the follow-up PR: the model_table_model header becomes "Name", a string change across the 13 catalogs that belongs with the other wording items.

@Pearcekieser

Copy link
Copy Markdown
Contributor Author

@Pearcekieser

Copy link
Copy Markdown
Contributor Author
  • Rely on ModelStore.remove's claim refusal and drop the early is_claimed check in api_stt_model_delete.

    kept, by design: the route's check runs before engine.release_if_resident. Without it, a delete request for a model that a live session or a batch decode has claimed would first release that resident model, forcing the claiming work to reload it, and only then be refused by ModelStore.remove. The early check refuses with the same stt_model_in_use code and releases nothing. ModelStore.remove still decides at unlink time, with no await between its refusals and the reservation.

@Pearcekieser

Copy link
Copy Markdown
Contributor Author

@Pearcekieser

Copy link
Copy Markdown
Contributor Author

@Pearcekieser

Copy link
Copy Markdown
Contributor Author
  • "Ask the agent" appears on refusals that only mean "wait".

    deferred to the follow-up PR, which will keep the hand-off only on the failed-download row and the generic removal failure. Its tracking issue opens with that PR.

@Pearcekieser

Copy link
Copy Markdown
Contributor Author

@Pearcekieser

Copy link
Copy Markdown
Contributor Author
  • Mid-word wrapping at in-between widths.

    deferred to the follow-up PR, which will add whitespace-nowrap to the model name and the action buttons. Its tracking issue opens with that PR.

@Pearcekieser

Copy link
Copy Markdown
Contributor Author

@Pearcekieser

Copy link
Copy Markdown
Contributor Author

@Pearcekieser

Copy link
Copy Markdown
Contributor Author

@Pearcekieser

Copy link
Copy Markdown
Contributor Author
  • Show "Removing..." while the delete request runs.

    acknowledged, not changed here. The Remove button is disabled for the length of the request, so a second click cannot land. A pending label would make that visible, and it fits the same follow-up. I am not pushing for an advisory note on a PR that is otherwise waiting on maintainer review.

@Pearcekieser

Copy link
Copy Markdown
Contributor Author
  • Attachments 04 and 06 show an earlier wrapping layout.

    accepted. They predate the whitespace-nowrap fix. I will re-capture them and edit the description together with the next push, which is already due once fix(governance): pass lists_checked_by_caller to session check #24767 fixes main lint. That way the description edit does not reset readiness on its own.

@Pearcekieser

Copy link
Copy Markdown
Contributor Author
  • Only one of the Remove refusal messages is shown.

    acknowledged, not changed. All of them render in the same error row, so the screenshot shows the layout. The copy for each one is in en.manual.json under pages.settings.sttSettings.

@Pearcekieser

Copy link
Copy Markdown
Contributor Author
  • A removal refused during verification reports stt_model_downloading.

    acknowledged, not changed here. That refusal comes from ModelStore.remove using its default code while a transfer or verification is in flight, and the panel then says the model is downloading. A separate stt_model_verifying code and copy fits the planned follow-up that exposes active-transfer state in /api/stt/status, together with the earlier UX notes about the Download gating. I am not pushing for advisory notes on a PR that is otherwise clean.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fork Pull request from a fork (external contributor) merge conflict Branch has merge conflicts with its base — author must resolve before merge readiness: passed Eligible automated validation passed for the current revision

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Voice settings: manage local speech models in a table (download, select, remove)

4 participants