You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This commit was created on GitHub.com and signed with GitHub’s verified signature.
[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 FormDatafetch() 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/FormDatafetch() 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)