Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 43 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,19 +149,30 @@ Recovery is restart-only: start the VS Code extension, then restart your host pr
Hosts can route local-unreachable separately from auth failures by narrowing on the error class:

```ts
import { init, RecostAuthError, RecostFatalAuthError, RecostLocalUnreachableError } from "@recost-dev/node";
import {
init,
RecostAuthError,
RecostFatalAuthError,
RecostLocalUnreachableError,
RecostInterceptorAlreadyInstalledError,
RecostInterceptorPatchOverwrittenError,
} from "@recost-dev/node";

init({
// No apiKey — local mode.
apiKey: process.env.RECOST_API_KEY,
projectId: process.env.RECOST_PROJECT_ID,
onError(err) {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if (err instanceof RecostLocalUnreachableError) log.warn("recost: local extension unreachable; check VS Code");
else if (err instanceof RecostFatalAuthError) pagerduty.fire(err);
else if (err instanceof RecostAuthError) log.warn(err);
else log.debug(err);
else if (err instanceof RecostInterceptorAlreadyInstalledError) log.warn("recost: duplicate package load — first install is active");
else if (err instanceof RecostInterceptorPatchOverwrittenError) log.warn(`recost: third-party wrapper detected on ${err.skippedBindings.join(", ")} — restart to recover`);
},
});
```

These two interceptor errors are advisory: the SDK either keeps the first install active (`AlreadyInstalled`) or detaches the callback and refuses re-install (`PatchOverwritten`). Recovery from `PatchOverwritten` requires a process restart with the conflicting library either removed or installed before recost.

### Custom providers

```ts
Expand Down Expand Up @@ -191,15 +202,40 @@ So a custom catch-all (`{ hostPattern: "api.openai.com", provider: "openai-mock"

### Cleanup / teardown

`init()` returns a handle with a `dispose()` method that stops the interceptor, cancels the flush timer, and closes the transport connection. Useful in tests or when you want to reinitialize with different config.
`init()` returns a handle with two lifecycle methods:

- **`dispose(): Promise<void>`** — stop intercepting, perform one final shutdown flush (bounded by `shutdownFlushTimeoutMs`), close the transport. Calling this twice is a no-op.
- **`flush(): Promise<void>`** — flush the current aggregator window *without* disposing. Useful before a known process-exit boundary where `dispose()` doesn't fit your shutdown ordering. After `dispose()` has run, `flush()` resolves immediately.

Both methods route flush errors through your configured `onError`; they never reject.

```ts
const recost = init({ … });

// Manual checkpoint flush before a non-graceful exit:
await recost.flush();

// Graceful shutdown:
await recost.dispose();
```

**Cross-SDK parity.** The Python SDK's `dispose()` is synchronous (returns immediately after spawning a flush thread); its `flush_blocking(timeout_s=…)` blocks the calling thread until the flush completes. Node's `await handle.dispose()` already provides blocking semantics, and `await handle.flush()` is the direct parallel of Python's `flush_blocking()`. There is no thread-blocking primitive in JavaScript, so the awaited promise is the only honest analogue.

### Worker threads

`init()` patches `fetch`, `http`, and `https` for the worker that calls it. Workers spawned via `node:worker_threads` get their own module instances and their own `globalThis`, so they will not be instrumented until you call `init()` inside the worker's own entry point. SDK errors thrown in a worker route through that worker's own `onError`.

```ts
const recost = init({ apiKey: process.env.RECOST_API_KEY });
// In worker.ts (the worker entry point)
import { init } from "@recost-dev/node";

// Later — e.g. in a test afterAll() or process shutdown handler:
recost.dispose();
init({ apiKey: process.env.RECOST_API_KEY });

// …rest of worker logic…
```

The main thread's `init()` does not propagate to workers, and the SDK does not detect or warn about worker spawns — instrumenting workers is the host's responsibility.

### Disabling in tests

```ts
Expand Down
1,742 changes: 1,742 additions & 0 deletions docs/superpowers/plans/2026-05-18-multi-realm-and-dispose-parity.md

Large diffs are not rendered by default.

12 changes: 9 additions & 3 deletions docs/superpowers/roadmap-2026-05-13-issue-waves.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,9 @@ Both touch `WindowSummary` serialization. Coordinate Python + Node together so n

## Wave 4 — Provider registry overhaul

**Status:** in-progress
**Status:** done

**Merged PR:** https://github.com/recost-dev/middleware-node/pull/38

**Plan:** `plans/2026-05-15-provider-registry-overhaul.md`

Expand All @@ -101,7 +103,11 @@ Both touch `WindowSummary` serialization. Coordinate Python + Node together so n

## Wave 5 — Architectural / lifecycle (riskiest, save for last)

**Status:** pending
**Status:** in-progress

**Spec:** `specs/2026-05-18-multi-realm-and-dispose-parity-design.md`

**Plan:** `plans/2026-05-18-multi-realm-and-dispose-parity.md`
Comment thread
coderabbitai[bot] marked this conversation as resolved.

**Theme:** Larger-scope changes that touch the patch model (`init.ts` install/uninstall, interceptor patching strategy) and shutdown semantics. Defer until Waves 1–4 land so the small fixes don't collide with the rewrite.

Expand All @@ -112,7 +118,7 @@ Both touch `WindowSummary` serialization. Coordinate Python + Node together so n
| [#11](https://github.com/recost-dev/middleware-node/issues/11) | Multi-realm: workers, dual-package hazard, third-party patch interaction | `src/core/interceptor.ts`, `src/init.ts`, README |
| [#19](https://github.com/recost-dev/middleware-node/issues/19) | Python sync vs Node async `dispose()` parity (Node may need `flushBlocking()`) | `src/init.ts` (this repo), Python side bigger |

**Recommended PR shape:** two plans, two PRs. #11 alone is a substantial design effort with 3 sub-problems (workers / dual-package / third-party patches); each could even be its own PR.
**PR shape (actual):** one plan, one PR. #19's Node-side change is small (one new `RecostHandle.flush()` method plus README parity note), so it rides with #11 in a single bundled PR per the wave-execution convention. The Python-side parity work tracks separately in `recost-dev/middleware-python`.

---

Expand Down
Loading