Skip to content

v0.16.0

Choose a tag to compare

@mikecarroll mikecarroll released this 20 Oct 17:25
· 9 commits to main since this release
650477f

[0.16.0] - 2026-10-20

What's New

  • withContext({ reporter, workload, template }, fn) declares which workload/template the LLM calls captured inside fn belong to. The declaration is sent as llm_request_log.declared, so the server skips log-to-template matching. It uses AsyncLocalStorage (loaded lazily, so Edge runtimes still load), nests with the innermost value winning per field and workload/template replaced wholesale, and a workload without a template does not inherit the outer template. It is snapshotted synchronously when a call is intercepted (http, https and fetch, on both the auto-monitor and Coolhand paths), so logs sent later from a background context keep it, and it never passes through body redaction. Keys and descriptions are validated client-side and throw before your callback runs. Server warnings on the log response (for example a declaration ignored because the public-key workload/template cap was reached) are now logged, never thrown. Older servers ignore the unknown declared key. See docs/declared-workloads.md. (#279, #285)
  • Coolhand#upsertWorkload / upsertTemplate (also WorkloadService#upsert / TemplateService#upsert, private API key) create or rename a workload or template by key via PUT .../by_key/*key. They send only the fields given, flatten a template's workload to workload_id/workload_key, percent-encode each key segment with / kept literal, and return { workload | template, created } (201 vs 200). (#279, #285)
  • Client-file admin methods: Coolhand#listClientFiles, downloadClientFile and updateClientFileStatus (also on ClientFileService), next to the existing uploadClientFile. listClientFiles({ status?, fileType?, page?, per? }) returns { files, pagination } with pagination read from the X-Page/X-Per-Page/X-Total-Count/X-Total-Pages headers; downloadClientFile(id) returns { content: Buffer, contentType, filename } as raw bytes; updateClientFileStatus(id, status) sets draft, published or archived (returns null in dryRun). All three require a client admin's per-user API key and throw HttpError (with .status) on a non-2xx response, unlike uploadClientFile, which resolves to null. Needs a backend carrying Coolhand-Labs/coolhand#1762. See docs/client-file-upload.md. (#278, #280)

Upgrade Notes

  • Bare openai.com is no longer an OpenAI domain. It matched every *.openai.com host (auth, platform, www), so TLS-intercepting proxies hit the apex domain and OAuth token exchanges on auth.openai.com were captured. api.openai.com and its subdomains still match. If you relied on another openai.com host being logged, add it through a custom patternsFile. (#73, #281)
  • excludeApiPatterns entries are plain substrings, not globs, and the SDK now warns when an entry contains *. Matching behavior is unchanged: '/internal-*' has always matched only URLs that literally contain /internal-*. Non-string entries (e.g. a RegExp from untyped JavaScript) are now ignored instead of throwing a TypeError inside the patched request path. See the new docs/exclude-patterns.md. (#231, #283)

Security

  • Generic credential keys are now redacted in JSON and form bodies. String values under api_key, password, access_token, refresh_token, id_token, secret, private_key, session_token and similar names (matched case- and _/--insensitively, including bracketed names such as user[password]) are replaced with [REDACTED] anywhere in a JSON or FormData body, and application/x-www-form-urlencoded bodies get the same names plus bare token, assertion and client_assertion. Object and non-string values (tool-schema properties named password) are left alone, and pwd is deliberately not redacted in bodies. Known gaps are documented in docs/global-monitoring.md. (#276, #284)

  • More credential names are redacted. URL query strings now also redact oauth_token, passwd and secret_access_key, and JSON, FormData and form-urlencoded bodies now also redact string values under the env-var style names GOOGLE_API_KEY, ANTHROPIC_AUTH_TOKEN, GITHUB_TOKEN and apiSecret (the same case- and separator-insensitive matching as the existing names).

Bug Fixes

  • Capturing a stream-bodied Request no longer lets the host's branch queue the whole upload. fetch(new Request(url, { body: stream })) capture now cancels its own branch of the clone() as soon as the 50 MiB cap is exceeded, so queued memory is bounded at about the cap plus one chunk and an endless stream no longer grows memory without bound. CappedBuffer also ignores empty chunks, which removes a false "truncated" flag when a stream emits a zero-length chunk after exactly filling the cap (affects the http/https paths too). Backpressure is still not restored for the first ~50 MiB of a stream upload. (#277, #282)

Documentation

  • New docs/exclude-patterns.md covering how excludeApiPatterns matches (substrings of the full URL, case-sensitive), the built-in defaults, and how to avoid over-matching; the README option and COOLHAND_EXCLUDE_API_PATTERNS rows now state the correct default and semantics. (#231, #283)

Maintenance

  • BaseService#putByKey is the shared write path for the workload and template upserts, fetchWithHeaders now also returns the HTTP status, and utils/entity-key.ts holds the key/description rules. (#285)
  • BaseService#fetchBytes (raw-bytes downloads) shares a new private fetchResponse with fetchWithHeaders; fetchWithHeaders behavior is unchanged. (#280)
  • Removed a stray node_modules symlink that was committed to main by mistake in #285. It is not part of the published package.
  • GitHub Actions bumps: actions/setup-node 7.0.0 → 7.1.0, actions/download-artifact 8.0.1 → 8.0.2, actions/upload-artifact 7.0.1 → 7.0.2. (#275, #274, #273)