With the embedding server unavailable every chunk failed, yet the document
was stored as indexed=true with 0 chunks, so the knowledge base listed it as
searchable while queries could never match it. The red use-case test caught
this; it had been written off as an environment failure.
Now a run with zero embedded chunks leaves indexed=false and throws
RAGEmbeddingFailed (surfaced by reindex, logged by addDocument).
Tests that only hold on the Windows developer machine now declare it: the
bundled SoX binary and PowerShell device discovery run on win32 only, and
the sidecar venv test runs only when sidecar/.venv exists. The Linux Forgejo
runner skips them instead of failing.
The first real Forgejo CI run failed because the linux-builder runner ships
Node 18.19: scripts using import.meta.dirname threw, and its older npm
reported the mobile lock as out of sync with the file: dependency on
packages/core (the lock is in sync under Node 24's npm).
bootstrap-linux-toolchain.sh gains a checksum-verified `node` mode
(24.19.0, the .nvmrc version); ci.yml (quality, build-validation,
mobile-quality) and deploy-site.yml add it to GITHUB_PATH before npm runs.
Stripe is not used. Keeping its checkout, portal and webhook paths meant a
second payment provider, a second return-URL format and dead UI.
- Delete the stripe-checkout, stripe-portal and stripe-webhook functions and
their config; billing-catalog serves Payple prices only, and the web parser
rejects a catalog that still mixes in Stripe prices.
- Web: drop the Stripe checkout/portal buttons, provider toggle and return
notices; billing shows Payple only. Past rows with provider='stripe' are
still displayed ("Stripe (종료)") with a support contact instead of a portal.
- Desktop: delete the Stripe checkout modal, payment IPC channels, preload
namespace and their types; "Remove ads with Pro" opens the web billing page
via license.openBilling. Support/refund copy names Payple.
- billingUrl() loses the Stripe-only success/canceled result option; the
Deno contract is regenerated.
- Migrations and the DB's accepted provider values are untouched (history).
- Docs and the backlog record the removal (MON-04, EXT-STRIPE-01, GAP-BILL-03).
Verified: typecheck (desktop/web/admin/api-client/mobile), contract:check,
deno check all functions, deno test 80/80, desktop 1478/1480 on the Electron
runtime (2 known environment failures), web and admin builds, release
metadata and mobile boundary self-tests, eslint on changed files.
- .env.example lists the keys the scripts actually read (NAS_HOST/NAS_USER/
NAS_SSH_PORT/NAS_DEPLOY_PATH, web build args, Supabase access token and
project ref, CRON_SECRET, Cloudflare token/account/zone/tunnel) and drops
unused ones (DSM_HOST, NAS_DEPLOY_ROOT, NAS_ADMIN_PORT, NAS_LANDING_PORT,
APP_DOMAIN, ADMIN_DOMAIN).
- docs/map/02-infrastructure.md gains a credentials map (names only) with the
matching Forgejo/Supabase/NAS copies, plus the public routing summary.
- Deploy scripts no longer point to the removed /admin static page or portal.
Since 2026-09-19 the site bridge worker served every path from Pages, so the
API server on the NAS had no public route: admin login (API_SERVER_URL)
and stt-proxy (D3RO_API_URL) called https://d3ro.chanpaca.net/api/... and
got the landing page's 405.
- The worker passes /app, /api and /health through to the domain origin
(the kd-nas tunnel) and serves everything else from Pages.
- The kd-nas tunnel sends path ^/app on d3ro.chanpaca.net to the web app
(NAS 3002); other paths keep going to the API (NAS 5050). No extra
hostname or DNS record is needed, so WEB_APP_ORIGIN is removed.
- API_PATH_PREFIXES joins WEB_APP_BASE_PATH in packages/core/src/web-urls.ts
(contract regenerated).
Verified live: /app/login 200, /health and /api/health 200, API login 401 for
an unknown account (was 405), landing/legal/404 unchanged, git/sso/admin
hosts unaffected.
- docs/REFACTOR_WAVE3_REPORT.md and the Wave 3 policy: canonical map,
production changes, verification and remaining external steps.
- Gap backlog: GAP-BILL-01 resolved; new GAP-BILL-02 (Payple renewal never ran,
Payple client key never set), GAP-WEB-01 (tunnel host for /app), GAP-OPS-01
(NAS compose/.env drift), GAP-CI-01, GAP-I18N-02, GAP-TEAM-02.
- design.md: hero loop decision (numbers taken from the app capsule), pricing
mismatch closed; feature catalog SHELL-11 updated.
- docs/map, release guide and mobile release docs no longer describe the
deleted wwwroot, binaries, Dockerfile.admin, NAS site copy or .github CI.
- docker-compose.nas.yml gains the d3ro-whisper service that only existed in
the NAS copy, so the repository file is the complete definition.
- refactor-wave skill: Wave 3 index and lessons P10-P12.
Replace the press-to-play demo and the "see how it works" button with an
example that keeps playing on the right: hold Right Alt, the capsule appears
with live wave bars, elapsed time and the partial transcript, releasing shows
the processing bar, and the cleaned sentence is pasted at the cursor of a
notes window. It cycles through two samples per language.
- Same numbers as apps/desktop recording-tip: 9 bars with cos weights, 100 ms
updates, 0.5 smoothing, +/-35% jitter, 2-28 px, per-bar colours, 120 px
progress bar min(95, (1 - 1/(1 + 1.5t)) * 100)%.
- WCAG 2.2.2: a pause button; stops when off screen or the tab is hidden.
Reduced motion shows one still frame. The animation is aria-hidden with a
text description for screen readers. Chinese and Japanese transcripts
appear three characters at a time.
- Add 404.html so unknown paths stop falling back to the landing page, delete
the duplicate accept-invite.html (/accept-invite/ is canonical) and link it
straight to /#download, and move the legal pages off the retired orange
accent (primary button contrast 5.2:1).
The repository has Forgejo and GitLab remotes but no GitHub remote, so
nothing under .github/workflows ever ran - including the daily Payple
renewal job, which means Payple subscriptions were not being renewed.
- Port payple-renew (daily cron + manual dispatch) to Forgejo. Requires the
Forgejo secrets SUPABASE_URL and CRON_SECRET.
- Port the CI checks (quality incl. version:check and contract:check, build,
mobile quality, edge functions, API tests) to .forgejo/workflows/ci.yml.
macOS and Android emulator jobs are dropped: no macOS runner here, and the
signed Android release stays on GitLab mobile-production-release.
- Keep one site deploy workflow (Linux, Cloudflare Pages); remove the
manual Windows duplicate. The mobile release boundary self-test runs there.
- Release verifiers read the Forgejo/GitLab workflows, fail if .github
workflows come back, and check the rewritten site offers no mobile package.
Policy: docs/REFACTOR_POLICY.md Wave 3, W3-7 and W3-8.
The landing page, download pages, invite page, assetlinks and installers
existed in two or three places; only site/ and the Forgejo feed are served.
- apps/api-server/wwwroot: delete the stale site build, download/invite
pages, .well-known copy, legacy static admin and 1.0.0 binaries. The API
no longer serves static files (UseStaticFiles/fallbacks and the apk/zip
blocker removed); the Next admin is the only admin UI.
- Delete 19 tracked installers/packages (~568 MiB) under site/public/releases,
apps/web/public/releases and wwwroot/releases; .gitignore blocks them.
- apps/web: delete the download/releases pages, desktop-release.ts, the
download.html and assetlinks copies, and the accept-invite page (invites
are only issued to the site's /accept-invite/). e2e specs call the /app
base path and check the /download redirect instead.
- scripts: delete the retired release/NAS site scripts, drop the web target
from sync-version, and check assetlinks in site/public only.
- Delete the unused Dockerfile.admin (apps/admin/Dockerfile is used).
Policy: docs/REFACTOR_POLICY.md Wave 3, W3-5 and W3-6.
apps/web was never deployed, so /billing on the public domain returned the
landing page and d3ro.dev (desktop "upgrade") did not resolve.
- apps/web runs with basePath /app and output standalone; /download and
/releases redirect to the site's #download. A Dockerfile and a d3ro-web
compose service (port 3002) deploy it to the NAS with the other images.
- The site bridge worker forwards /app/* to WEB_APP_ORIGIN (the tunnel host)
and rewrites upstream redirects; everything else still goes to Pages.
With no origin configured /app answers 503 instead of the landing page.
- Desktop upgrade, desktop Stripe return, mobile subscription management,
the web checkout/portal returns and the site all use billingUrl(); the
return query is success=1 / canceled=1, which the billing page reads.
The billing page highlights ?tier=pro|pro_plus, and signing in from a
billing link returns to the same plan.
- auth/callback pins the redirect origin in production and rejects
protocol-relative next= values (open redirect).
- Mobile legal links use SITE_URLS (fixes the missing slash on /terms).
- Compose drops the unused NEXT_PUBLIC_API_URL and the dead wwwroot legal
mounts; deploy scripts add the web image and the SUPABASE_* values the NAS
compose already required; .dockerignore keeps app .env files out of images.
- Supabase auth redirects allow /app/** (remote dashboard must match).
Policy: docs/REFACTOR_POLICY.md Wave 3, W3-3 and W3-4.
Prices, quotas and site URLs were copied by hand into the edge functions,
admin, desktop and the landing site, and the copies disagreed (Payple billed
9,900/29,900 KRW, admin labels said 12,900/24,900 KRW and $9.9/$19.9, the
site said 2,900/8,900 KRW).
- packages/core/src/plan-catalog.ts is the single source for PLAN_PRICE_KRW
(Free 0 / Pro 2,900 / Pro+ 8,900 a month) and PLAN_QUOTA.
- packages/core/src/web-urls.ts is the single source for the public origin,
the /app web-app base path, SITE_URLS and billingUrl().
- Deno cannot bundle packages/core, so scripts/ci/sync-core-contract.mjs
generates _shared/core-contract.generated.ts; `npm run contract:check`
fails on drift (same pattern as version:sync).
- Payple checkout, renewal and webhook amount checks now bill the catalog
price, so existing subscribers move to the new price at their next renewal.
quota.ts, team-contract.ts and the tests read the generated values.
- Admin MRR/ARR is computed in KRW from the catalog; license labels, the
release link and desktop PREMIUM_LLM limits derive from core; the site
imports prices and quotas directly.
Policy: docs/REFACTOR_POLICY.md Wave 3, W3-1 and W3-2.
- Replace claims that contradicted the app: the default hotkey is Right Alt
(hold to talk), local features are free with no daily cap, there is no
14-day trial, Team/SSO/SCIM/ZDR are not offered, and the repository is not
public. Remove invented metrics, status badges, the competitor table, the
ad-mediation changelog, the hash calculator and the duplicate demo.
- Seven sections: hero with one labelled example, features, how it works,
privacy, pricing (Free / Pro 2,900 / Pro+ 8,900 KRW a month), download, FAQ.
Footer links the privacy policy, terms and account deletion pages.
- All copy moves into i18n and every one of the 10 locales is translated.
Pricing and cloud quotas come from site/src/pricing.ts.
- Accessibility: skip link, labelled sections, aria-expanded with Esc and
focus return for the menu, language list and FAQ, live status for the
example, reduced-motion support, 44px targets, AA contrast on the primary
button. Korean keep-all line breaking is scoped to :lang(ko) because
Tailwind's break-keep blocked wrapping in Japanese and Chinese.
- JS 111 -> 98 KB gzip, CSS 7.8 -> 5.2 KB gzip.
The caption model selector now sits directly under the dictation model in
the STT tab's Local Whisper card, where people look for it, and stays in the
General tab's caption section as well; both use one shared component. The
first selector is renamed to 'Dictation model' so the two are told apart.
Ctrl+Alt+Enter pasted while Ctrl+Alt were still down, so the target app got
Ctrl+Alt+V; accepting now closes the panel and waits for the modifiers to be
released. Candidates are accepted on pointer press because the list is
redrawn as new candidates stream in, which swallowed clicks.
The typing gate identified the focused field by its bounds, so chat boxes
that grow while typing looked like a new field on every keystroke and were
reported as "not typing". Fields are now keyed by window, control type and
name, and a mouse click re-baselines the text instead. The decision log
includes both gate values.
The live-caption model selector moves to the caption section of the General
tab, next to the other caption settings.
The speech engine now keeps an auxiliary model next to the dictation model
and transcribes with whichever the request names, reloading it once if the
engine restarted. Settings > STT gains a live-caption model so captions can
run on large-v3-turbo while dictation keeps its own model. The runtime
minimum rises to 1.7.0 because older engines would silently ignore the
model choice.
Suggestion paging moves to Up/Down: the page follows the selection and the
last item waits while more candidates are being generated. The Left/Right
page shortcuts are removed; they did nothing until a page had filled and
clash with Intel's display-rotation hotkeys.
Replaces the fixed six-second batches with a streaming track per audio
source: the uncommitted audio is re-recognised every second and sent as a
partial with its agreed (stable) prefix, a short pause finalises the line,
and long unbroken speech is committed at Whisper segment boundaries. Idle
audio is trimmed so silence cannot produce invented sentences.
Finished lines are corrected by the local model against the previous lines
and replaced in place; edits that change too much are rejected. The
behaviour can be switched off in Settings.
Notes the canonical feed, runtime and portable aliases serving 1.6.0 with the
installer hash verified, the site reporting commit 2fe20fa / 1.6.0, the new
minimum-runtime rule that spares 1.5.0 engines a re-download, and the local
packaging trap where a Node-ABI better-sqlite3 ships an installer without a
database.
Next-sentence suggestions now arrive one at a time up to twelve, shown three
per page with Ctrl+Alt+Up/Down to move, Left/Right to page, Enter to accept
and Esc to close; old default bindings migrate and the panel guide follows the
live bindings. The overlay is redesigned, stays put while candidates stream
and sits outside the input box when no caret is reported.
The personal phrase memory stops learning from terminals, code editors and
the coding-agent hub, ignores symbol-heavy lines and empty-field placeholders,
and prunes existing entries that break those rules.
Fixes suggestion keys starting dictation, installs stuck on a pre-1.5.0
speech engine without the focus endpoint, Ollama runner windows flashing
while typing, the speech engine starting twice, and cold-model timeouts.
Live captions can be dragged to a remembered position and show a waiting
notice until the first line arrives.
Bumps the product version to 1.6.0 (Android/iOS build 1060000).
Notes the canonical feed now serving 1.5.0 (installer hash verified), the
runtime and portable aliases pointing at the same version, and the landing
site reporting commit 5c11ee2 / 1.5.0. Also records that the site's CI
deployment still fails without a Cloudflare API token, so this refresh went
out through an authenticated local deploy, and that the stale-alias bug found
while publishing is fixed and closed.
The portable publisher decided a remote file was already up to date by
comparing size alone for anything over 1 MiB. A rebuilt runtime sidecar
produced a first split volume with exactly the same byte length as the
previous one, so the stale volume was skipped and the `runtime-latest`
alias ended up as an old first part next to a new second part. Downloading
that alias would produce a corrupt archive.
The remote hash now comes from the package file-list API, and when any file
in a version differs the whole alias version is deleted and republished, so
an alias can never hold a mix of old and new bytes.
Adds next-sentence suggestions while typing, weekly input insights and a
personal phrase memory to the desktop app, and fixes custom instructions so
they process the text instead of inserting the instruction's own wording.
Local model requests are now bounded and individually cancellable.
Bumps the product version to 1.5.0 (Android/iOS build 1050000), refreshes the
landing and web download links, and records the new INPUT feature rows and the
open verification gaps in the infrastructure map.
Running a custom instruction (translate, summarise, rewrite, explain code,
free prompt) inserted the instruction's own wording instead of the result.
Two faults stacked:
The instruction was passed as the text to process, leaving the system-prompt
argument empty. `BASE_SYSTEM_PROMPTS` has no `custom` key, so resolution fell
back to `refine` without saying so, and the model dutifully polished the
instruction it had been handed. The transcript never reached it.
And only `{{text}}` was substituted, which none of the five built-in
instructions use — they carry `{{targetLanguage}}`, `{{userPrompt}}`, or no
placeholder at all. The substitution was a no-op from the day it was written:
the presets landed ten hours before the code that expected them.
- Instruction prompts now go to the system-prompt argument and the transcript
to the text argument. Instructions that spell out `{{text}}` keep their old
meaning, so hand-written ones still work.
- `renderInstructionPrompt` resolves `{{text}}`, `{{userPrompt}}` and
`{{targetLanguage}}` in one place, and warns by name when a placeholder is
left standing rather than letting it reach the model.
- `resolveSystemPrompt` no longer drops silently to `refine` for `custom`.
- Voice shortcuts no longer die at the `defaultLLMAction === 'none'` gate; an
explicitly named instruction outranks the default. Without one, `none` still
passes the transcript through untouched.
- `translate` receives its target language instead of relying on a default two
call frames away. It is still always English — `AppConfig` has no key for it,
and neither `language` (UI locale) nor `sttLanguage` (source language) can
stand in. Choosing a target language needs a setting and is not in this fix.
- Chains ran instructions with placeholders intact; they share the same
resolution now.
- The command screen's pipeline bench called `llm.generate`, which preload does
not expose, so every run threw and the catch showed the input back as if it
had succeeded. It uses `llm.process` now, over the same path production
takes, and a failure reads as a failure.
Present since the feature shipped: the custom-instruction path has never
worked. Plain actions (refine, summarise, grammar, expand) were unaffected and
are now covered by tests so they stay that way.
Shortcuts gain several bindings per action, mouse buttons and a searchable
picker, and the stored shortcut schema changes shape, so this is a minor bump.
Existing shortcuts migrate on first launch.
Play release notes state that this release carries no mobile feature changes;
the work is desktop-only.
Shortcuts were defined in four places that drifted apart: per-action IPC channel
pairs, a hand-written VK table in the service, a second one in the renderer, and
three copies of the keycap styling. Adding an action meant editing all of them,
so two shortcuts stayed hardcoded in bootstrap and one had no settings entry at
all.
packages/core/src/keybinding.ts is now the single source for the binding type,
the selectable key catalog, the action catalog, normalization, validation,
conflict detection, display labels, search and deserialization. Main, preload
and renderer all read from it; nothing redefines keys or rules locally.
- Each action holds a list of bindings instead of one. AppConfig's four
*Shortcut fields collapse into a single keyBindings map, migrated on launch.
- Mouse buttons can be bound. Left click is refused, right/middle need a
modifier, side buttons are free. uiohook cannot swallow events, so the
original click still fires and the UI says so.
- Keys can be picked from a grouped dropdown with a search box, not only by
recording a keypress.
- HOTKEY's 14 channels become KEYBINDING's 9, taking the action as a parameter,
so actions no longer multiply channels. The history and command popups moved
out of bootstrap into ordinary actions.
- displayLabel is gone; labels derive from the binding and follow the app
language and platform.
Fixes found on the way:
- Double-press hands-free was unreachable: lookup returned only the first
matching action, and dictation shares its default binding.
- Reserved-combination checks compared joined key names, so a different modifier
order let Ctrl+C through.
- Disabling shortcuts released every global registration in the process,
including the popup ones, and never restored them.
- Enabling shortcuts after starting disabled left nothing registered.
- The dashboard stored the caption event payload instead of the state in it.
d3ro.chanpaca.net answered an empty Cloudflare 404 because the account had no
Pages project for the landing site, and attaching the custom domain still needs
a DNS CNAME that the local Cloudflare credentials cannot create. Add a small
Workers route bridge that serves the Pages deployment on that hostname so the
domain works now and keeps following Pages deploys, and record the cleanup
left to do.
💘 Generated with Crush
Assisted-by: Crush:deepseek-v4.1-flash
The Cloudflare account had no Pages project for the landing site, so the custom
domain had nothing to bind to. The project and a production deployment now exist
and the domain is attached; only the DNS CNAME is missing because the local
Cloudflare credentials cannot edit DNS.
The tag pipeline dies at the sidecar step before the signing guard because the
release runner cannot see Python 3.11, the site deploy now builds and fails only
for the missing Cloudflare token, and the portable channel published 1.3.7 with
only its evidence-upload step failing.
💘 Generated with Crush
Assisted-by: Crush:deepseek-v4.1-flash
The hero typed its timer ref with NodeJS.Timeout, which only resolves when
@types/node is visible from an ancestor node_modules. The deploy job installs
site dependencies alone, so tsc -b failed with TS2503 and the landing site has
not been redeployed since. Use ReturnType<typeof setTimeout> and record the
measured deploy blocker plus the runner's missing Python in the map.
💘 Generated with Crush
Assisted-by: Crush:deepseek-v4.1-flash
The download contract on both the web console and the landing site still
advertised 1.2.0, so the install button pointed at
D3RO-Voice-Setup-1.2.0-x64.exe, which the feed answers with 404 while 1.3.7
is live. Wire both contract files into version:sync so the installer version
and release date follow the version SSOT, and set them to the published 1.3.7.
💘 Generated with Crush
Assisted-by: Crush:deepseek-v4.1-flash
The overlay fix shipped as 1.3.7 through the local updater publisher, so the
map now carries the published version, the per-channel evidence, and the
correction that 1.3.2-1.3.7 are live on the feed even though the CI signing
gate still blocks tag-driven publication.
💘 Generated with Crush
Assisted-by: Crush:deepseek-v4.1-flash
Popup pages loaded their scripts as classic <script src> tags, which the
renderer build never bundles, so an installed app rendered only the static
markup: the recording tip stayed at 0:00 with no wave bars and live captions
showed nothing.
- declare popup scripts as modules so the build emits them, and fail
packaging when a renderer page references an asset that was never produced
- hold popup IPC until the renderer has loaded and re-assert visibility on
every show, so a popup hidden once still appears next time
- surface popup renderer console and load failures in the main log
💘 Generated with Crush
Assisted-by: Crush:deepseek-v4.1-flash
Part checks counted bytes from the network stream while the joined archive
was hashed from disk, so a truncated write passed part verification and only
failed later as "런타임 아카이브 해시 불일치". Verify size and hash from the
written file, check the joined size before its hash, and retry a failed part
up to 3 times.
The Ollama setup guide still led with two retired models while the app already
defaults to the newer one, so a fresh setup would install a model the app does
not use. The guide now recommends the same model as the rest of the app and
lists current lightweight alternatives.
Installing the previous build left an app that could not update itself: the
packaging path used to guarantee the native module build does not create the
updater configuration file, so the update client had nothing to read.
That file is now written from the single feed source and its presence in the
packaged app is checked before anything is published, so an installer that
cannot update can no longer be released.
The released installer could not start: it carried a better-sqlite3 build for the
host Node runtime instead of Electron, so the app died immediately with a module
version mismatch when it opened its database.
Packaging now proves the Electron build of every runtime-sensitive native module
before an installer or archive exists, and installers are produced only from that
verified tree, so the mistake cannot pass silently. The release pipelines run the
same check.
The default local model also pointed at a retired model: a *.gguf name that
Ollama cannot serve, while the settings, onboarding, and guide screens
recommended an older model. All of them now use the model the service code
already preferred.
Auto-update could not work at all: the installer was 189 MB because it carried
the local speech engine and ffmpeg, and the download feed rejects uploads over
about 100 MiB, so update metadata could never be published.
The installer now leaves those components out and the app fetches them the first
time they are needed, verifying every part and the joined archive before
installing. The installer is 90.6 MiB, the update feed is published again, and
updates stay small because the engine is not re-sent on every release.
The fetch is visible and recoverable: the download runs with progress, a failed
install cleans up after itself, and Settings > STT shows the runtime status with
a manual download action for when the automatic one cannot run.
The manual install path still needed 7-Zip, which the target machine does not
have, so "installable without a certificate" was not yet true.
The channel now also publishes the app as byte-split zip parts, and the install
script joins them and extracts with the built-in Windows Expand-Archive after
verifying every part and the joined archive. Version 1.3.1 republishes the
channel from a single build, because a version's artifacts can only match one
build and published volumes are never overwritten.
Installers could not be published at all: the signing certificate does not
exist yet, and the release pipelines stop at their signing guard. Users had no
way to install a fixed build, so the product was effectively stuck behind a
certificate that takes weeks to obtain.
There is also a second, independent blocker: the download feed sits behind
Cloudflare, which rejects any upload body over about 100 MiB, and the app with
its speech engine exceeds that even when signed.
A portable channel now publishes what can actually be delivered today: the app
compressed into 95 MiB 7z volumes (162 MiB total instead of 243 MiB), a Scoop
bucket for a normal install and uninstall experience, and a verifiable manual
installer script. It is deliberately separate from the auto-update feed, needs
no certificate, and refuses to overwrite an already published version.
Two tagged release pipelines failed and no installer has been published since
1.1.0. The cause is not in the build: the release repository has no Actions
secrets at all, so every run stops at the signing guard.
A helper now reports which release secrets are missing and registers them once
the signing material exists, and the release guide documents the required
values, the Forgejo-side check, and how to re-run a pipeline for an existing
tag without recreating it.
Packaging silently tolerates a missing resource directory: electron-builder
prints "file source doesn't exist" and continues, which is how installers that
could not transcribe were published.
Every pipeline that packages the desktop app now builds the sidecar and fails
when the engine or its VAD data is absent, so a release cannot ship without
local transcription.
Local dictation had never produced a transcript on an installed build. The
engine itself was healthy; every connection to it was broken.
Installed builds shipped no speech engine at all: the packaging config had no
entry for the faster-whisper sidecar and no pipeline step built one, so the app
always fell back to a system Python without the runtime. Development was broken
too, because the sidecar and SoX paths were resolved against the Vite output
directory instead of the app root, which also meant recording failed with a SoX
ENOENT. On hosts where localhost resolves only to IPv6, every local request was
refused outright, which silently disabled both local transcription and the local
LLM.
The sidecar is now built and bundled (including the Silero VAD data it needs),
gated by a packaging check that fails when the engine or its data is missing.
Paths are discovered from the app root and fail loudly when the engine is
absent. Local engine URLs are normalized to the IPv4 loopback, decoding is tuned
so repeated hallucinations cannot compound (the same transcript now takes about
a fifth of the time), the engine is warmed up at startup, and holding the hotkey
now shows the text forming live in the recording tip.
The changelog still described unreleased work under 1.1.0, which was already
published with its own notes. Those notes are restored verbatim for history,
and the new work has its own 1.2.0 section that the feed publisher will turn
into release notes.
The release guide, infrastructure map, and mobile SSOT now carry the 1.2.0
identity, state that installer binaries are distributed through the feed and
never committed, and record that the published 1.1.0 installer is unsigned and
is being superseded rather than rewritten. Backlog entries cover the remaining
external signing and token secrets.
Version 1.1.0 was published on 2026-09-15, so its tag is closed. Every commit
since then, the update-feed rework, dictionary import and export, the extra
push transports, and entitlement gating, needs its own immutable version.
Product version, Android version code, iOS build number, and all package and
store surfaces move to 1.2.0 / 1020001, with Korean and English store notes for
the new version code. The download centers read that version and its release
date from one place instead of repeating them inline.
The feed publisher overwrote whatever version-specific assets it found, so a
re-run of an old release tag could quietly replace the installer that
customers already downloaded under that version number.
Publication now compares the bytes already in the version-specific registry
path and stops when they differ, while still allowing an identical re-run to
finish. The metadata verifier gained a negative case so the guard cannot be
removed unnoticed.
Release notes for 1.1.0 were split between an Unreleased section and the
version section, so the published notes would have omitted the update-feed
and desktop changes. Everything shipping in this version now sits under one
`## [1.1.0]` heading.
`docs/map/` becomes the entry point for what infrastructure exists per
platform and how far each feature is developed, with a documented update
protocol so feature work and this map do not drift apart again. The release
guide now states that installer binaries live in the update feed rather than
the repository.
The download section described a release as pending while the product was
already installable, and its installer link pointed at a path the site build
never ships, so a visitor would have reached a missing file.
The section now states the released version and links the published update
feed that the desktop client itself uses, with the release notes and hash
metadata one click away. Repeated color literals behind the hero animation
moved into named tokens.