Skip to content

v0.15.0

Choose a tag to compare

@mikecarroll mikecarroll released this 14 Oct 21:41
· 19 commits to main since this release
d91b977

[0.15.0] - 2026-10-08

What's New

  • Cost-report read surface: Coolhand#searchWorkloads(params) + new WorkloadService, and metrics/cost options on templates and logs. searchWorkloads lists workloads (GET /api/v2/workloads) as { workloads, pagination }. searchTemplates/getTemplate now accept includeMetrics, daysBack, since and until; searchLogs gains since, until, minCost, order: 'cost_desc', projectPath and sourceApplication, and log rows carry cost. since/until take a Date (sent as ISO 8601 UTC; an invalid Date throws before any request) or a string passed through for the server to validate. New exported types include LlmMetrics, WorkloadSummary, SearchWorkloadsParams/SearchWorkloadsResponse and GetTemplateOptions; getLogContent now types cost and cost_breakdown. Needs a backend carrying Coolhand-Labs/coolhand#1749 (an older server ignores the new parameters). See docs/workload-search.md, docs/template-search.md and docs/log-search.md. (#268, #269)
  • Coolhand#linkFeedback, bulkLinkFeedback and unlinkFeedback + new OptimizationFeedbackLinkService — attach feedback to an optimization as evidence via POST/DELETE /api/v2/optimizations/{optimization_id}/feedback_links. bulkLinkFeedback batches in groups of 100 and merges the results. Requires the private API key and throws HttpError on non-2xx, like the other read services. See docs/feedback-links.md. (#259, #260)
  • requestTimeoutMs option on Coolhand and initializeGlobalMonitoring (default 30000) to tune the timeout on Coolhand API calls, e.g. for slow self-hosted backends. File uploads keep their separate 10-minute limit. A non-integer, non-positive or > 2^31-1 value throws at construction. There is no COOLHAND_REQUEST_TIMEOUT_MS env var yet, so auto-monitor users cannot set it. (#254, #263)
  • Blob and FormData fetch() request bodies are now captured. Text-like Blobs are read up to the 50 MB cap; a FormData body is logged as a JSON object of its string fields (file parts skipped, repeated keys become arrays), so key-based redaction applies as it does for JSON bodies. Binary Blobs (e.g. image/png) and ReadableStream bodies still log null, since reading a stream would consume the caller's only copy. (#254, #263)
  • coolhand-node/test-utils subpath export (ESM + CJS) — captureInterceptionSnapshot() and assertInterceptionOccurred(before, message?) let your own tests/CI assert that a request was really intercepted, catching the silent bypass where a client library captured a stale fetch/http.request reference before initializeGlobalMonitoring() ran. It reads the global-monitoring counter only, so it does not observe instance-based new Coolhand({...}) monitoring, and concurrent unrelated traffic can produce a false pass. Also adds docs/diagnostics-channel-interception.md, a design exploration (documentation only, no implementation) of diagnostics_channel-based interception as the structural fix. (#211, #258)
  • Coolhand#loadPatterns() / PatternMatchingService#loadPatterns() — finishes loading a custom patternsFile on native ESM where Node has no synchronous way to reach fs (Node < 20.16 / < 22.3). initializeGlobalMonitoring() and coolhand-node/auto-monitor (COOLHAND_PATTERNS_FILE) call it for you; direct Coolhand class users on that combination call await coolhand.loadPatterns() themselves. See docs/global-monitoring.md. (#253, #264)

Upgrade Notes

  • An unsanitized request body is never logged. If sanitizeBody returns null or throws, or there is no pattern service, request_body is now null instead of the raw body — including on the fetch path, which previously fell back to the raw body. No public API change. (#252, #262)
  • Default authorization redaction is now "[REDACTED]" on every runtime. It was "[REDACTED]" when patterns loaded from api-patterns.json but "Bearer [REDACTED]" from the hardcoded Edge-runtime fallback (native ESM, Edge, or any path that fell back). If you depended on the exact "Bearer [REDACTED]" string in sanitized logs, update it. (#253, #264)
  • request_body values that were null for Blob/FormData fetch() bodies may now contain data — check any downstream expectations. A polyfilled fetch response without body.getReader whose Content-Length exceeds the cap is no longer buffered via .text(): it logs an empty body and reports truncation (chunked bodies with no Content-Length are still buffered). (#263)
  • The fetch() Response your code receives is now a thin wrapper around a pull-driven stream, not the original object. Status, headers, url, redirected and type are mirrored and every body-reading API behaves as before, but response.body.getReader({ mode: 'byob' }) is not supported on it. Logging for a host that never reads (or cancels) the body is now deferred up to 30 s (previously immediate), and a body the host abandons without cancelling is logged truncated to what was read by then. (#251, #265)
  • Response types gained required fields (cost, metadata, ingest_evidence, …) to match the API definition. This only breaks code that constructs those types by hand (e.g. test fixtures). (#269)
  • Contributor note: the repository's agent instructions moved from CLAUDE.md to AGENTS.md. Claude Code older than 2.1.277 will not pick them up until updated. (#257)

Security

  • FormData request-body capture is now linear-time. Serializing a FormData with one key repeated many times copied the whole value array on every repeat (quadratic) and ran synchronously before the host's fetch() was dispatched, so a user-controlled form forwarded to a monitored provider could stall the host's event loop for seconds (≈3.6 s at 50,000 repeats, ≈15 s at 100,000). Values are now appended in place.
  • sanitizeURL now also redacts X-Goog-Signature, X-Goog-Credential and session_token query params (presigned Google Cloud Storage URLs and session-token query auth), the same defense-in-depth as X-Amz-Signature/X-Amz-Security-Token before them.

Bug Fixes

  • Request-body capture no longer delays the host's send. Parsing and sanitizing up to 50 MB of request body now runs after the original end(...), on both the http/https and fetch interception paths, and is flushed before the request is logged, so request_body is always populated and sanitized. req.end(callback) also no longer throws from Buffer.from(fn) before the request is sent. (#252, #262)
  • Native ESM now finds the built-in default patterns. The eval('import.meta.url') lookup always threw, so native ESM silently fell back to process.cwd() and effectively never loaded the packaged dist/api-patterns.json. The defaults are now compiled into src/default-api-patterns.ts at build time from src/api-patterns.json, so CommonJS, native ESM, Edge and every supported Node version get identical defaults with no filesystem lookup (this also removes the duplicated Edge-runtime fallback array). scripts/verify-esm.mjs/verify-cjs.cjs now assert the loaded defaults equal the packaged dist/api-patterns.json, and the ESM build is scanned for stray eval() calls. (#235, #253, #264)
  • The fetch response capture is now bounded by construction and a host cancel ends the download. The capture no longer uses response.clone() (a native tee() whose unread branch queues the whole body); a pull-driven wrapper copies each chunk into a byte-capped buffer as the host reads it, response.body.cancel() now cancels the real underlying stream instead of leaking it, and a host that never reads the body (e.g. a status-only if (!res.ok) check) is finalized after a 30 s idle timeout or on beforeExit, so the request is still logged and its dedup entry released. (#251, #265)

Maintenance

  • BaseService.setQueryParams / toTimestampParam replace three copies of the same query-param loop across the read services. (#269)
  • examples/fastify-openai-unbundled: fastify 5.12.1 → 5.12.5 (a security release; example app only, not part of the published package). (#270)
  • Harness docs: AGENTS.harness.md updated with the R8 review handshake. (#261)
  • npm run test:live-examples runs the live example scripts in parallel with a pass/skip/fail line per script, loading provider keys through zsh -ic when available. (#272)