Repository navigation
Releases: hrygo/dsh-speechrail-bundle
Release list
v0.7.1 — proven against the running service
Three leftovers, closed: the request shape is now proven against the running service, CI really runs the
registry check, and the two dead exports are gone. No runtime behaviour changed.
Verified against the running service (with the owner's authorisation)
A 3.13 s canonical WAV was synthesised locally and sent through the plugin's own transcribe() to the
live SpeechRail service, then reproduced independently with curl. language=zh, language=en and no
language all answer 200 with the exact transcript of the synthesised sentence, so the service accepts
our whole request shape — the multipart fields, the file part's name and type, and the Bearer plus
Accept headers. Until now that rested on the service's OpenAPI document and its capability snapshot
alone; a real transcription now backs it.
The same probe measured a service-side limitation, recorded rather than worked around: any language
value with more than one subtag (zh-CN, zh-Hans, zh-Hans-CN, en-US, zh-Hant-TW) answers 500
with a plain-text body. This plugin cannot reach it — the language is the closed union auto|zh|en and
the offered options are the intersection with a fixed known set — so nothing changed for it. The
plain-text 500 does confirm the unusable-body path in the wild: the attempt stays unproven and needs an
acknowledgement, exactly as designed.
CI
verify:ci's registry schema check still prints SKIPPED on a clean runner by design (the package must
not become a workspace dependency: it pulls @deepseek-ai/dsh-llm into the Typert program and breaks
generation). A new step does the real check instead, installing @deepseek-ai/dsh-tools at the version
the other @deepseek-ai devDependencies pin into a throwaway prefix and running the check without
--optional, so a wrong install fails loudly. Measured: 23 packages in 8 seconds.
Removed
decodeBase64Audio (the entry the 0.6.0 scan work replaced, which still read like the validating entry
point) and UNAVAILABLE_JOURNAL (no references anywhere). Deleting them left pnpm verify green with
246 tests, which is the evidence they were dead.
v0.7.0 — the cause belongs to the operation that holds the gate
Two surfaces that only a human was checking are now guarded, and the framing rule from 0.6.0 composes
with the client's cause line: an unusable chunked reply stays unproven and the user is told why.
Tooling
- The locale keys are checked against the code. The dictionaries were only ever compared with each
other, so a mistypedt('...')rendered the key itself to the user with the whole suite green. A new
case scans the client sources, compares every static key with both dictionaries, and asserts the scan
found more than thirty of them, so a broken scan cannot pass silently. - The published file list is checked against what npm packs.
filesdecides what is published while
main/types/exports/dsh.bundle.patchdecide what the Harness loads at install; when those drift
the tarball still publishes and still installs, and only fails later inside someone's profile.
tools/check-package-contents.mjscompares both againstnpm pack --dry-run --jsonand runs in
pnpm verifyand CI. Nothing was wrong: 15 manifest references and 2 globs are covered.
Fixed (from the v0.6.0 review)
- A reused operation id could carry the previous attempt's cause. Ids come from the client, so a
client may reuse one. An acknowledged attempt leaves its cause in memory; reusing that id for an
attempt refused locally, whose own settle then fails, reopens the gate on the same id and the report
blamed it for a failure that never reached the network. A new operation now clears the cause when it
takes the gate — not when it is refused, because a refusal happens because the gate is still held
and that cause is what the report needs. - A closed gate with no operation id said only "gate unknown" — the shape an unreadable journal
produces, so the report was silent in the one state where local storage is the problem. It now says
the journal could not be read. - The framing comment claimed more than the rule delivers. A reply cut at a JSON boundary parses
and settles as a success under either rule, so a stated length governs unusable bodies only; the
comment now says so, and acceptance was deliberately left unchanged. - The registry schema check sorted stored copies as strings, so
0.9.0ranked above0.10.0and
the wrong copy could be checked; it sorts by version now, printsSKIPPEDin upper case so a clean CI
runner cannot look like a check that passed, and the workflow documents that skip as deliberate. - The tool's renderer lost its open-gate guard and the case meant to pin it had been weakened; both
are restored, and the language token cap is pinned at exactly 24 characters admitted and 25 rejected.
Checked, no change needed
cordis.patch.ymlcarries no config defaults (only aninsertof the plugin), so the install patch
and the code schema cannot disagree about defaults.- The install patch's defaults aside, the framing change is safe against truncation because a truncated
message never reaches theendhandler: Node reports it on the error/abort path, which fails the
upload as unproven before framing is consulted.
v0.6.0 — prove the framing, key the cause
Two surfaces now say why the gate is closed, not only that it is; and the release itself has a guard,
so a version, a tag and a documented install URL can no longer drift apart unnoticed.
Added
- Both surfaces name the cause of a closed gate, even after a reload. The composer's acknowledge row
and the settings page's gate row used to say only that an upload was unconfirmed, because a page
reload clears everything the page knew about the attempt — while the host still holds the failure that
left it unproven, and that failure already crossed the boundary. Both rows now append it, and invent
nothing when the host has no cause to give. No contract change.
Tooling
pnpm verifyfails when the release is inconsistent. Publishing is three hand edits that must
agree — the version, the install URL in each README, and the tag the tarball is attached to — and
nothing noticed a missed one.tools/check-release-consistency.mjsnow asserts the agreement (and,
given a tag, that the tag names the version), requires the English entry point and the package README
to each carry an install URL, and runs insidepnpm verifyand CI'sverify:ci.- CI runs both branches of the declared Node range. The workflow ran only Node 22 while the package
declares^22.19.0 || >=24.0.0; it now runs 22 and 24 withfail-fast: false.
Measured, not changed
- A near-ceiling carrier (6,291,446 bytes) costs 24.86 ms to parse:
Buffer.byteLength0.18 ms per
pass,JSON.parse2.47 ms, and the base64 scan the rest. The duplicate byte-length pass therefore
stays, and the scan stays a provable rule: a decoded-length check would be ~20 ms faster but would
accept a data-less====carrier and blur malformed-carrier (bad-request) from unusable-audio
(audio-invalid).
Fixed (from the v0.5.0 review)
- Framing is proven, not believed. Node reports header values trimmed while it decides framing on
the raw bytes, so a legalTransfer-Encoding: chunked<HTAB>arrives close-delimited yet reports
exactly what a genuinely chunked reply reports —rawHeadersincluded, so no value test can separate
them. That settled an unproven outcome, clearing the journal and opening the gate. Framing now
requires aContent-Length; the price is that a genuinely chunked 200 with an unusable body stays
unproven until acknowledged, and four tests pin it (the substring rule and the tab case included). - The retained cause belongs to the operation holding the gate. It is stored as
{operationId, failure}and reported only while that matches the pending id, so an acknowledged
failure can no longer be presented as the reason a later, locally refused attempt is blocked. Only an
unproven outcome is recorded at all — pinned by a test that kills both mutants. - The tool's two flat fields are exercised end-to-end through a real core (writing them as empty
used to pass the whole suite); the unreachableaudio === nullbranch is gone; abusygate is no
longer rendered as "awaiting acknowledgement". - The snapshot's language tokens are capped at 24 characters. The token shape admits any number of
subtags, so a service could put a 256 KiB string into a selector entry and the agent's report.
Tooling
tools/check-tool-schema.mjsnow finds the stored@deepseek-ai/dsh-toolscopy itself and runs
insidepnpm verifyand CI as--optional: it really checks the registry where the package resolves
(reporting the version it checked) and prints an explicit skip where it does not.
v0.5.0 — keep the vocabulary closed, and say why the gate is closed
The v0.4.0 review found three things, one of them a defect this loop introduced itself. All are fixed,
every fix is mutation-checked, and the tool can now say why the gate is closed.
Fixed
- A declared error code could escape the closed vocabulary. The declared-code lookup was a plain
object index, and the envelope's own filter admits exactly two inherited names:constructorand
__proto__. A service answering422with{"error":{"code":"constructor"}}therefore produced a
failure whosecodewas a function — dropped byJSON.stringify, and rejected by the generated
Remote codec, turning a plain refusal into an unlocalizable boundary error. The lookup now goes
throughObject.hasOwn, with the four inherited names pinned by a test. - Framing was a substring test.
notchunked,xchunkedandchunked-fakeall contain "chunked",
but Node does not de-chunk them: the body still ends at the close, and the old test settled an
outcome the service never confirmed. The final transfer coding must now be exactlychunked(so
gzip, chunkedstill frames a body, verified with a real chunked terminator). - A declared code overrode every status. The mapping was measured on
422, but it applied
everywhere: a401carrying an audio code was reported as a bad recording instead of a refused
credential. It now applies only to the status it was observed on.
Added
- The agent tool names the cause of a closed gate. A closed gate says an upload is unaccounted
for; the journal holds an operation id and nothing else, so a report could not say whether the
service answered badly, never answered, or was cancelled mid-upload. The host now keeps the failure
that left an outcome unproven — only that one: a settled refusal, a local refusal and the
"still unconfirmed" the closed gate hands back are not its cause — and reports it through the status
and two new tool fields. The reported value is derived from the gate state, so an open gate can
never carry a stale cause.
Verification
pnpm verifygreen (exit status checked): 232 tests in 16 files; the tool schema is accepted by
the registry at 18 fields.- The declared Node range again measured on the final tree: 22.23.4 and 24.22.1 both ran the
full suite green. - Both review notes about tests that passed for the wrong reason are fixed: the empty carrier in the
base64 boundary table is refused before the scan it claimed to bound, and the client's
"never reached the wire" case now cancels while the take is still being encoded and asserts the
upload is never attempted.
v0.4.0 — name the real refusal, and verify the runtime range
An adversarial review of v0.3.0 produced nine findings. All are addressed, every fix was
mutation-checked, and the Node versions this package declares are now measured rather than assumed.
Fixed
- The take that could not be sent was still called a bad request. A recording longer than roughly
147 seconds does not fit the base64 carrier this bundle puts it in, and the parser reported that as
bad-request— "check the configuration" — which is exactly the mislabeling v0.3.0 set out to
remove. A carrier that does not fit is nowaudio-too-large. Related:audio-too-largewas
unreachable for a canonical 32 kHz recording under the package's own ceilings, because 120 seconds
of it is smaller than the 4 MiB ceiling; the byte ceiling is what a service-tightened limit, or
audio that is wider than canonical, now trips. - Framing was header presence, not framing. A reply with
Transfer-Encoding: identity(or
gzip) is delimited by the close, exactly like an HTTP/1.0 reply, but it counted as framed and so
settled an outcome the service had never confirmed. Now only aContent-Lengthor a chunked
encoding counts.response.completeis deliberately not used: it would also settle the
close-delimited case this bundle keeps unproven. 413read as a format problem. Payload-too-large is about the take's size, and it is now
audio-too-large;415staysaudio-invalid.- A near-ceiling recording blocked the host event loop for about 132 ms. The base64 check was one
regexp test per character, and it ran twice. Comparing code units and dropping the duplicate scan
brings the whole parse to 20.8 ms (measured; the regexp alone was 61 ms).
Added
rate-limited: the endpoint declares 429, and the service is throttling — worth retrying —
where the bundle previously reported a non-retryable bad request. Copy in both languages.- A small, evidence-based table of service-declared error codes, so a condition the service names
is not overwritten by a status guess. It contains only what has been measured or documented for
this endpoint: a non-audio upload answering422withaudio_decode_failedis an audio problem,
not a configuration one. An unrecognised declared code changes nothing.
Verified
pnpm verifygreen (exit status checked): 226 tests in 16 files.- The declared Node range is measured: 22.23.4 and 24.22.1, both official tarballs with SHA256
checked against the published sums, each ran the full suite green. Node 26 is the development
runtime. Previous reviews had listed "only Node 26 tested" as an open gap since v0.1. - Against the live service, with no inference authorized (zero-audio probes): its OpenAPI declares
400/401/413/422/429/502/503for the transcription endpoint; a wrong key answers401with
invalid_api_keybefore any audio is touched; a non-audio body answers the422above. Its
declared refusals are exactly the statuses this bundle treats as proving the request was not
processed, and its two backend statuses are exactly the ones that do not — the settlement rule now
has the service's own response set behind it. A temporary probe (deleted after use) also confirmed
the three-state credential verdict end to end: the real key is accepted, a wrong key is refused
while reachable, and a closed port stays unverified. - A cancellation no longer wastes a take. Cancelling while an upload is on the wire leaves the
host with an unproven outcome and a closed gate, because it cannot prove what the service did with
audio it already sent. The composer used to return to idle without reading that, so the next take
was recorded in full and only then refused; it now reconciles the gate and says what happened.
Notes
- The
try/catcharound the request parser is defence without a test seam: the input that used to
throw is gone, and removing the guard fails nothing. It stays so that a future parser failure is
reported in the closed failure vocabulary instead of as an unlocalizable Remote-level error.
v0.3.0 — prove a completed answer, keep a usable transcript
Two findings from the v0.2.9 review, plus a crash that both the review and I reproduced independently.
Fixed
- A long recording crashed the request parser. The base64 check was
/^(?:[A-Za-z0-9+/]{4})*(?:…)?$/; V8 recurses on that pattern and threw
RangeError: Maximum call stack size exceededat roughly 5 MB of input — which the carrier limit
admits, so a take of about two minutes and up escaped the closed failure vocabulary entirely and the
page could only show "internal error". The check is now a linear scan (the second use of the same
pattern, indecodeBase64Audio, is fixed with it), and the public entry point keeps any future
parser throw inside the vocabulary. - A 200 whose body cannot be used is only settled when the service framed its reply. With no
Content-Lengthand no chunked encoding the body ends when the socket does, and a reply cut
mid-transcript is indistinguishable from a complete one. Framed replies still settle (the service
proved it had finished); unframed ones now stay unproven like every other ambiguous ending. - A malformed optional
usageno longer discards a real transcript. The field has no consumer,
and refusing a whole response over it threw away the take the service had actually produced. Only
textdecides whether an answer is usable. This reverses a decision recorded earlier; the record
says so in place. - An arrived refusal outranks the transport code, and now has a test. v0.2.9 changed that
precedence but shipped without one, and its release note claimed the branch could not be
reproduced on loopback. That was wrong: a server that flushes a401head and closes while several
megabytes are still being written reproduces it — the old order reportedunreachablewhile the
capability probe called the same credential refused. Pinned in
tests/http-transport.host.spec.ts, and it fails if the old order returns. - The dead
unprovenparameter ofrefuse()is gone: every caller passedfalse, and atrue
would have produced a settled journal beside an unproven message.
Added
- A take over a ceiling is refused by its real reason:
audio-too-longandaudio-too-large
join the closed vocabulary, with copy in both languages. The enforced ceilings are this package's
own and deliberately stricter than the service's (4 MiB / 120 s against an advertised
512 MiB / 3600 s), because a whole take travels as one base64 JSON carrier — and now the refusal
says so instead of calling a well-formed recording malformed. The exhaustive
Record<SpeechRailFailureCode, true>table made the compiler demand both keys. - A
provesNotProcessedtruth table, and the status ladder now asserts the failure code and
httpStatusas well as the gate: narrowing the 2xx rule back to202alone used to leave the
suite green, and no longer does. - The client's unproven-outcome message carries the failure detail (
{detail}): a complete 500 now
says both that the service errored and that it never confirmed finishing, instead of only the
second. This had no test at all, which is why it shipped.
Verification
pnpm verify green (exit status checked): 217 tests. Every change in this release was
mutation-checked — reverting the scan, the framing rule, the precedence order, the 2xx branch or the
client detail each fails a named test. Installed into the profile, with the shipped files
byte-compared against the repository build.
v0.2.9 — a refusal outranks a transport code
One failure code that could contradict itself, and the live path re-checked.
Fixed
- When a status line arrived while the request body was still being written, the failure carried the
transport code (unreachable/timeout) and that status. A client then showed "service
unreachable" beside a401, while the capability probe - which judges from the status - called the
credential refused. An arrived refusal now outranks both the transport code and thewrote
bookkeeping. This branch could not be reproduced: on loopback the body write always finished before
a401arrived, in every attempt, so the rule is argued rather than measured, and the comment says
so.
Re-checked
- The live local service, through the real probe: reachable and authorized, transcription supported,
languages{"status":"unknown","values":[]}, limits 4 MiB / 120 s - unchanged by this release,
and the exact shape that makes the configured language travel as an unverified hint.
Verification
pnpm verify green (exit status checked): 212 tests. Installed into the profile, with the
shipped files byte-compared against the repository build.
v0.2.8 — settle only on what is proven
A review of 0.2.5 found that its new "a status arrived, so the upload was refused" rule claimed more
than the evidence did. This release narrows it to what is provable, and fixes the rest of what the
review found.
Fixed
- The gate no longer opens on a status that proves nothing. 0.2.5 settled the pending journal on
any status >= 300 that reached the client. A 504 is the sharpest counter-example: a gateway gives
up while an upstream worker may still be transcribing the recording it just read in full, and the
review measured exactly that - the journal was cleared and the user was invited to dictate again
over work that may still have been running. Settlement now follows one rule,provesNotProcessed:
only a 4xx other than 408 proves the request was refused. 5xx, 3xx, 408 and a 202 Accepted all keep
the outcome unproven, which is what the design always said a network-level failure means. - A 2xx that is not 200 is no longer a proven failure.
201,202and204used to settle as an
internal error;202means "still working" in so many words. They now report
malformed-responsewith an unproven outcome. One deliberate exception remains, and it is now
stated where it is implemented: a complete 200 whose body cannot be used still settles, because
the service finished that request and there is no work in flight to wait for. - Report wording does not promise the wire. The composer hint, its comment and the agent tool's
line said the hint "is being sent"; a check that changes the snapshot between a status read and the
upload changes the bytes. They now say what the snapshot in force puts on the wire
(language hint zh in force). - The settings page cannot describe two snapshots at once.
view.capabilities ?? checked.capabilities
fell through when the projection legitimately saidnull("no snapshot in force"), mixing the
checked snapshot's support and limits with the projection's language options. It is now
view.capabilitieswhenever the projection has been read at all. - Removed an unread second "proof" channel.
HttpFailure.sentwas documented as deciding
provability and read by nothing; the outcome decision has one source.
Tests
- Two status ladders against the real core over real sockets - one where the engine reads the whole
recording and then stalls, one where it answers with a complete body - pin settlement for400,
401,404(settle) and408,500,502,504,302,200,201,202,204(do not),
asserting the gate and the pending journal, not just the failure code. The gap the review found was
real: the previous suite never asserted either case, so the wrong rule passed. - The composer's language test now interpolates its translator stub, so it can tell
zhfrom any
other non-empty value; the settings single-snapshot test reaches the mixed state for real (a
successful check followed by a status read with no snapshot) and fails if the fall-through returns. - Every test added this round was mutation-checked: reverting the settlement rule fails the ladder,
reverting the settings fall-through fails the snapshot test.
Note on the review's one open question
The review could not check whether the real service can report languages.status: "unsupported"
while still accepting a language hint. It cannot: the reference implementation emits only supported
or unknown (capability_snapshot.py:367), so the unsupported => auto only rule is unreachable for
it. The rule stays for other engines, where withholding the hint is the correct reading.
Verification
pnpm verify green (exit status checked): 212 tests. Installed into the profile, with the
shipped files byte-compared against the repository build.
v0.2.7 — both engine profiles, documented
Documentation and metadata only; nothing about the plugin's behaviour changes.
Updated
- The root README no longer claims the bundle is "not installed or enabled in any profile" - it is
running in this machine'sdesktopprofile, and every release is installed from its own GitHub
Release asset afterpnpm verifyand a byte-comparison against the repository build. - A Two engine profiles, one contract table now states what each kind of service gets and what it
does not: the capability snapshot, the transcript shape, the three-state credential verdict, the
language rule, the journal/gate/acknowledgement semantics, and the limits actually enforced. It
names the conformance suite that pins both profiles. - The language limitation is restated accurately: the snapshot never expires (deliberate - a re-probe
before every upload would add a round trip after the user has already spoken), and language is the
one documented exception to "unstated means unsupported", because the service's own endpoint
documents the hint. - The package description and keywords, the package README's opening line, and the GitHub repository
description and topics now describe both supported engine profiles rather than SpeechRail alone.
Verification
pnpm verify green (exit status checked): 209 tests. The release asset is installed into the
profile and its five shipped files are byte-compared against the repository build.
v0.2.6 — two names that meant different things
Two field names that meant different things, and the tests that keep them apart.
Changed
status.transcriptionSupportreplacesstatus.transcriptionSupported. The capability view
already had atranscriptionSupported- the service's own boolean claim - and the status field
added in 0.2.5 was a three-state string of the same name. Both travel on the same Remote boundary,
so the same name meant two different things depending on which object you held. The status field is
now named for what it is: what the plugin knows (supported/unsupported/unknown), next to
the view's boolean that records what the service said.- A test now asserts against the generated boundary schema that the status carries the tri-state,
that the capability view keeps its boolean, and that the two do not share a name. It proved itself
immediately: right after the rename the generated schema still held the old name until the
generation step ran again.
Tests
- The language labels are now pinned by a literal table of every snapshot shape (no snapshot, cannot
transcribe, languages refused, languages unstated,zhenumerated, onlyenenumerated) with two
independent assertions: that the host's own rules match the literals, and that the page's labels
match them. The first version of this test built its fixture and its expectation from the same
function and passed even with the host rule deliberately broken; both halves are now
mutation-checked - breaking the host rule fails one, breaking the label condition fails the other.
Nothing about what the plugin does changes in this release.
Verification
pnpm verify green (exit status checked): 209 tests. node tools/check-tool-schema.mjs accepts
all 16 output fields.