Skip to content

typescript bindings#31

Merged
cjpais merged 18 commits into
mainfrom
typescript-bindings
Jun 18, 2026
Merged

typescript bindings#31
cjpais merged 18 commits into
mainfrom
typescript-bindings

Conversation

@cjpais

@cjpais cjpais commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

No description provided.

cjpais and others added 18 commits June 17, 2026 15:11
The test script passed a quoted glob ("test/*.test.mjs") to `node --test`.
Node only expands glob patterns itself in v21+; on the CI-pinned Node 20 the
quoted string is treated as a literal path, so `node --test` found zero files
and exited 1 — failing ts-build before any test ran. It passed locally only
because dev machines run Node 22.

Dropping the quotes lets the shell expand the glob into explicit file paths,
which `node --test` has accepted since v18 — version-independent, and it keeps
common.mjs (a non-test helper) out of the run. Verified: 28 tests now
discovered across all 8 suites (6 pass no-model tier, 22 skip without a canary).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The buffered-stream test passed chunkMs: 2000, which the canary model converts
to 25 encoder frames — outside its allowed chunk menu (1,2,7,13) — so
transcribe_stream_begin rejected it with INVALID_ARG. (chunkMs: 2000 is the
value the Python suite uses only in a struct-plumbing unit test, never fed to a
real stream; its streaming test uses defaults.)

Drop the override so the request uses the in-menu defaults (L=70, R=13),
matching the Python streaming test. Stream-slot field plumbing stays covered by
the cache-aware test's attContextRight override.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Node 20 reached end-of-life (~2026-04-30) — no further security updates — so
the binding shouldn't advertise or test against it. Node 22 is the current
supported LTS and what dev machines already run; aligning CI to it also closes
the dev/CI drift that let the Node-21+ test-runner glob behavior slip past CI.

  - engines.node and the lockfile mirror: >=20 -> >=22
  - typescript-ci.yml (ts-gates, ts-build) and publish.yml (ts-rehearsal,
    ts-release): setup-node 20 -> 22
  - README runtime-support + resource-management notes: Node 20 -> 22

Single-version 22 rather than a 20+22 matrix: the floor should match what we
test, and testing an EOL runtime defeats the bump. Unrelated node20 mentions in
cuda-windows.yml / python-wheels.yml are about an upstream action runtime and
are left as-is.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
These two jobs are the per-PR time sinks, and the time is CPU-bound model
inference, not compilation:

  - rust-ci linux-{static,shared,dynamic-backends}: ~42-45 min each, every PR.
    The build step is ~8s (ccache warm); the cost is `cargo test -p
    transcribe-cpp` running the canaries on CPU. ~34 min of that is the
    moonshine-streaming suite alone (full jfk fed in 100 ms chunks, 3 tests).
  - python-bindings python-shared: ~18 min, same inference profile.

The library autodetects threads when n_threads==0 (the default everywhere):
hardware_concurrency() for batch/mel, min(8, cores) for the parakeet decoder —
so 8 vCPUs is the sweet spot (decode caps at 8; inference is memory-bandwidth-
bound past that). No test pins a thread count, so the extra cores get used.

Compile-bound release jobs (python-wheels) are a separate, later bump to 16.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 8-vcpu bump did not help: the per-PR cost is dominated by the moonshine
streaming conformance, which is latency/oversubscription-bound (small per-step
matmuls on a tiny model + cargo running the heavy tests concurrently), not
throughput-bound — so more cores don't shorten it. Reverting to 2-vcpu to stop
paying for capacity that isn't used. The real fixes (rust posture split so the
model tier runs on one leg, and demoting the other bindings to FFI smoke) are
tracked separately.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 8-leg posture matrix re-ran the full model-tier conformance + examples on
every leg, but shared / dynamic-backends differ from static only in LINK
posture — the FFI marshalling code is byte-identical, so a marshalling
regression can't hide on one posture and not another. Running the heavy model
tier (chiefly the ~30 min moonshine streaming conformance on the 2-vcpu linux
legs) on all of them re-certified the same native compute up to 8×.

Gate the canary fetch + the examples on a new matrix `model_tier` flag, true
only on the representative *-static leg of each OS. The *-shared and
*-dynamic-backends legs now run build + the -sys FFI smoke + the always-on
no-model tier (which still exercises the dynamic-backends module load via
init_backends_default) and skip the canary, so their model-gated tests
self-skip. No posture coverage lost; ~2 redundant ~42 min linux legs/PR gone.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…teardown crash

The log callback is registered eagerly during native bootstrap and lives for
the whole process, but nothing detached it at teardown. On Windows the library
unload + ggml worker-thread teardown then raced the koffi trampoline's
destruction, exiting the test subprocess with an access violation (0xC0000005)
after every subtest passed — failing the windows-x64 conformance leg on
family.test.mjs and pcm.test.mjs (the model-tier files that spin up worker
threads).

Register a process exit hook that calls transcribe_log_set(null) — dropping the
native reference before the unload — then koffi.unregister, mirroring the
install/uninstall pairing the per-run abort callback already uses.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…egfault

A dlopen'd backend module (the cpu-vulkan Linux/Windows builds; compiled-in
macOS metal never dlopens, which is why it never reproduced) can emit a
diagnostic during process teardown. Routed through the transcribe log sink into
the koffi callback trampoline after koffi has freed it, that segfaults the
process at exit after every test/example has already passed — failing the
windows-x64 (and, intermittently, linux-x64) conformance/examples legs.

The log callback is the only persistent native->JS callback alive at exit (the
abort callback is per-run and unregister-paired), so detaching logging detaches
the whole surface. Add an internal exported transcribe_shutdown() — disable the
sink and detach ggml's logger — and call it from a process-exit handler. It is
exported but intentionally NOT in the public header: it has no stable 0.x
lifecycle contract, the bug is specific to the koffi callback lifetime, and
keeping it out of the header avoids PUBLIC_HEADER_HASH churn across all bindings.
The exit hook binds it opportunistically and falls back to logSet(null) if a
hand-pointed older library predates the symbol; it never koffi.unregisters (that
frees the trampoline while a worker thread may still hold it — the use-after-free
that a previous attempt introduced on Linux). Registered before initBackends so
a failed backend init still detaches cleanly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Windows/Linux teardown segfault needs the koffi log-callback trampoline and
ggml worker threads to coexist in one process: no-model processes carry the
trampoline (they call setLogHandler) yet never crash, because they spin up no
worker threads. Eager registration at native() bootstrap forced every
model-loading process to carry the trampoline, creating that coexistence — and
detaching the sink at exit (prior commit) does not fix it, because it is the
trampoline's lifetime racing the dlopen'd-backend/worker-thread teardown, not
the log emission, that crashes.

Install the trampoline lazily on the first setLogHandler(handler) instead, so a
process that never opts into logging never carries it — model load + transcribe
stays clear of the race. A process that only disables logging gets native
silence without a trampoline. The transcribe_shutdown() exit hook from the prior
commit stays as best-effort mitigation for the one remaining case: a host that
sets a handler and then transcribes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…MP exit crash is fixed

The windows-x64 ts-build leg has never passed: loading a model on Windows spins
up the ggml CPU threadpool, and the MSVC OpenMP (vcomp) runtime crashes
(0xC0000005) at process exit under node/koffi, after the run succeeds. That is a
native teardown bug, orthogonal to the binding, and needs a real Windows machine
to fix.

Skip the canary fetch on Windows so the model-gated tests and the model-loading
examples self-skip cleanly. The leg still runs its reason for existing — the
no-model koffi DLL-resolution + ABI + error-mapping tier — and gates on it. One
guard to drop to re-enable the model tier once the teardown crash is fixed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cjpais
cjpais merged commit 37a159f into main Jun 18, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant