Elyra · · 6 min read

The Boring Release - twenty fixes, zero features - Elyra v0.8.0

No new features. No new tools. No new commands. Twenty fixes for the things that were quietly wrong under the surface — the three-second freeze, the silent failures, the slow leaks, and the crash that breaks your terminal.

The Boring Release - twenty fixes, zero features - Elyra v0.8.0

No new features. No new tools. No new commands.

This release fixes twenty things that were quietly wrong under the surface. The kind of things you don't notice until they bite you at the worst possible moment — a terminal that won't respond after a crash, memory climbing slowly during a long session, a provider error that vanishes into silence.

It's the kind of work that doesn't make for exciting screenshots. But if you've ever had Elyra freeze for ten seconds while editing a TypeScript file, or wondered why your terminal felt sluggish after a long session, this release is for you.

The three-second freeze

Every time you edited a TypeScript file, Elyra ran npx tsc --noEmit to check for type errors. Good idea. The problem: it ran synchronously. The entire Node.js event loop — streaming, rendering, input handling — stopped dead while TypeScript did its thing.

On a small project, this was maybe a second. On a large monorepo, it was fifteen. Fifteen seconds of frozen UI, no streaming output, no ability to cancel. Just a cursor sitting there.

// Before: synchronous, blocks everything
spawnSync("npx", ["tsc", "--noEmit"], { timeout: 15_000 });

// After: async, UI stays responsive await execFileAsync("npx", ["tsc", "--noEmit"], { timeout: 15_000 });

The fix is almost embarrassingly simple. The execute function was already async. The type check just never took advantage of that.

Same story with two other synchronous bottlenecks: the auth-storage and settings-manager both had retry loops that used CPU-burning busy waits. A while (Date.now() - start < delay) {} loop that pegs the CPU at 100% for the entire sleep duration. Replaced with Atomics.wait, which sleeps the thread properly:

// Before: burns CPU
while (Date.now() - start < delayMs) {}

// After: proper sleep, zero CPU Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, delayMs);

The silent failures

When something went wrong in Elyra's internals, you often wouldn't know. Errors were caught and discarded. Processes failed without explaining why. Promises hung forever without resolving.

Here's a sampling of what we found:

exec.ts discarded error messages. When a process failed to spawn (binary not found, permission denied), the .catch() handler threw away the error and returned { code: 1 } with no explanation. Now the error message is preserved in stderr.

Memory generation failures were invisible. If the LLM call that generates session memory failed — network error, malformed response, anything — the error was caught by a bare catch {} and silently returned stale data. No log, no warning, nothing. Now it logs to stderr.

Agent retry failures were invisible. Three different places in the agent session called this.agent.continue().catch(() => {}). If the retry failed for a non-transient reason, you'd never know. Now they log.

EventStream.result() could hang forever. If a stream ended without pushing a terminal event, the promise returned by result() would never resolve. No timeout, no rejection, just a promise that sits there until the process dies. Now it rejects with an error.

These aren't glamorous fixes. But silent failures are the hardest bugs to diagnose. When something goes wrong and there's no evidence of what happened, you're left staring at symptoms with no trail to follow.

The provider contract

Elyra's StreamFunction interface has a documented contract: once invoked, errors should be encoded in the returned stream, not thrown. Four providers violated this. When you called streamSimpleAnthropic() without an API key, it threw synchronously instead of returning an error stream.

This matters because the caller expects to iterate an async stream. A synchronous throw bypasses that entirely, bubbling up as an uncaught exception. The fix wraps the error in a proper stream:

// Before: violates contract
if (!apiKey) {
throw new Error(No API key for provider: ${model.provider});
}

// After: returns error stream as documented if (!apiKey) { const stream = new AssistantMessageEventStream(); const output = createOutput(model); output.stopReason = "error"; output.errorMessage = No API key for provider: ${model.provider}; stream.push({ type: "error", reason: "error", error: output }); stream.end(); return stream; }

Fixed in Anthropic, OpenAI, Azure, and Mistral. While we were in the Mistral provider, we also added the onResponse callback (which was never called, making rate-limit monitoring blind) and timeoutMs support (which was silently ignored).

Anthropic's mapStopReason also threw on unknown stop reasons. If Anthropic ships a new stop reason tomorrow, that would crash your stream mid-response. Now it returns "error" gracefully.

The slow leaks

Long sessions accumulate state. That's fine, as long as the state is bounded. Several data structures in Elyra weren't:

Structure What it stored Cap UndoStack structuredClone of entire editor state, per edit Now 200 KillRing Killed text segments Now 100 Editor pastes Full text of every paste event Now 100 Editor history Every submitted prompt Now 500 errors arrays Every error in auth-storage and settings-manager Now 100 Codex WebSocket maps Debug stats and fallback session IDs Now cleaned up on idle timeout

The UndoStack one was particularly fun. Every keystroke that modified the editor state pushed a structuredClone of the entire state object. In a long session with hundreds of edits, that's hundreds of deep-cloned arrays sitting in memory, never evicted.

The crash that breaks your terminal

This one's subtle. Elyra uses synchronized output — a terminal protocol that buffers rendering between \x1b[?2026h (begin) and \x1b[?2026l (end) markers. This prevents screen tearing during rapid updates.

The problem: if a render crash occurred between the begin and end markers, Elyra called this.stop() without ever sending the end marker. The terminal was left in synchronized output mode, buffering everything and displaying nothing. Your shell would appear to hang after Elyra exited.

One line fix. Emit \x1b[?2026l before stopping.

The small things

A few more that didn't fit neatly into a category:

  • Atomic checkpoint writes. Checkpoint data was written directly with writeFileSync. If the process was killed mid-write, the file would be truncated and all checkpoint data lost. Now writes to a temp file and renames.

  • Bedrock stream guard. A response.stream! non-null assertion. If Bedrock returned a non-streaming response (permission error, malformed response), this would throw a cryptic TypeError with no context. Now guarded with an explicit check.

  • PageUp/PageDown in SelectList. The keybindings were defined but never wired up. Now they work (jumps 10 items).

  • Loader.dispose(). The spinner component started a setInterval in its constructor but had no way to clean it up other than calling stop() manually. If the component was removed from the tree without someone remembering to stop it, the interval fired forever. Now has a proper dispose() method.

  • resolveCacheRetention deduplicated. The same 8-line function was copy-pasted in four provider files. Extracted to the shared simple-options.ts where the other shared helpers already live.

Breaking change

One: EventStream.result() now rejects instead of hanging when a stream ends without a result. If you were relying on the old behavior (which was to wait forever), your code will now get a rejection. This is almost certainly what you want.

The takeaway

A codebase has two kinds of problems: the ones users report, and the ones they work around without realizing it. A mysterious pause here, an occasional freeze there, a vague sense that things get slower over time.

This release is about the second kind. Twenty fixes, zero features. Sometimes that's the most valuable release you can ship.


npm install -g @elyracode/coding-agent@0.8.0

Full changelog: v0.8.0