Skip to content

Releases: hrygo/dsh-speechrail-bundle

v0.7.1 — proven against the running service

Choose a tag to compare

@hrygo hrygo released this 24 Oct 13:52

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

Choose a tag to compare

@hrygo hrygo released this 24 Oct 13:08

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 mistyped t('...') 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. files decides what is published while
    main/types/exports/dsh.bundle.patch decide 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.mjs compares both against npm pack --dry-run --json and runs in
    pnpm verify and 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.0 ranked above 0.10.0 and
    the wrong copy could be checked; it sorts by version now, prints SKIPPED in 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.yml carries no config defaults (only an insert of 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 the end handler: 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

Choose a tag to compare

@hrygo hrygo released this 24 Oct 12:53

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 verify fails 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.mjs now 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 inside pnpm verify and CI's verify: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 with fail-fast: false.

Measured, not changed

  • A near-ceiling carrier (6,291,446 bytes) costs 24.86 ms to parse: Buffer.byteLength 0.18 ms per
    pass, JSON.parse 2.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 legal Transfer-Encoding: chunked<HTAB> arrives close-delimited yet reports
    exactly what a genuinely chunked reply reports — rawHeaders included, so no value test can separate
    them. That settled an unproven outcome, clearing the journal and opening the gate. Framing now
    requires a Content-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 unreachable audio === null branch is gone; a busy gate 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.mjs now finds the stored @deepseek-ai/dsh-tools copy itself and runs
    inside pnpm verify and 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

Choose a tag to compare

@hrygo hrygo released this 24 Oct 12:24

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: constructor and
    __proto__. A service answering 422 with {"error":{"code":"constructor"}} therefore produced a
    failure whose code was a function — dropped by JSON.stringify, and rejected by the generated
    Remote codec, turning a plain refusal into an unlocalizable boundary error. The lookup now goes
    through Object.hasOwn, with the four inherited names pinned by a test.
  • Framing was a substring test. notchunked, xchunked and chunked-fake all 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 exactly chunked (so
    gzip, chunked still 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: a 401 carrying 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 verify green (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

Choose a tag to compare

@hrygo hrygo released this 24 Oct 12:04

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 now audio-too-large. Related: audio-too-large was
    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 a Content-Length or a chunked
    encoding counts. response.complete is deliberately not used: it would also settle the
    close-delimited case this bundle keeps unproven.
  • 413 read as a format problem. Payload-too-large is about the take's size, and it is now
    audio-too-large; 415 stays audio-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 answering 422 with audio_decode_failed is an audio problem,
    not a configuration one. An unrecognised declared code changes nothing.

Verified

  • pnpm verify green (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/503 for the transcription endpoint; a wrong key answers 401 with
    invalid_api_key before any audio is touched; a non-audio body answers the 422 above. 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/catch around 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

Choose a tag to compare

@hrygo hrygo released this 24 Oct 11:40

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 exceeded at 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, in decodeBase64Audio, 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-Length and 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 usage no 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
    text decides 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 a 401 head and closes while several
    megabytes are still being written reproduces it — the old order reported unreachable while 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 unproven parameter of refuse() is gone: every caller passed false, and a true
    would have produced a settled journal beside an unproven message.

Added

  • A take over a ceiling is refused by its real reason: audio-too-long and audio-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 provesNotProcessed truth table, and the status ladder now asserts the failure code and
    httpStatus as well as the gate: narrowing the 2xx rule back to 202 alone 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

Choose a tag to compare

@hrygo hrygo released this 24 Oct 11:25

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 a 401, while the capability probe - which judges from the status - called the
    credential refused. An arrived refusal now outranks both the transport code and the wrote
    bookkeeping. This branch could not be reproduced: on loopback the body write always finished before
    a 401 arrived, 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

Choose a tag to compare

@hrygo hrygo released this 24 Oct 11:22

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, 202 and 204 used to settle as an
    internal error; 202 means "still working" in so many words. They now report
    malformed-response with 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 said null ("no snapshot in force"), mixing the
    checked snapshot's support and limits with the projection's language options. It is now
    view.capabilities whenever the projection has been read at all.
  • Removed an unread second "proof" channel. HttpFailure.sent was 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 for 400,
    401, 404 (settle) and 408, 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 zh from 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

Choose a tag to compare

@hrygo hrygo released this 24 Oct 11:12

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's desktop profile, and every release is installed from its own GitHub
    Release asset after pnpm verify and 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

Choose a tag to compare

@hrygo hrygo released this 24 Oct 11:06

Two field names that meant different things, and the tests that keep them apart.

Changed

  • status.transcriptionSupport replaces status.transcriptionSupported. The capability view
    already had a transcriptionSupported - 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, zh enumerated, only en enumerated) 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.