Change Log
4.15.0
- Changed
MockModelResponse.usageis a newMockUsagetype rather than the emittedUsage. The script is an input, not a result:MockModel.buildResponsehonours onlyinput/output/cachedTokens, so a fixture declaringcostorreasoningTokenswas silently discarded while the type promised otherwise.totalis optional and documented as derived, because the mock recomputes it asinput + output— an existing spec deliberately asserts that a mismatched scriptedtotalis overridden - Changed The mock honours
deltas. Fixtures already declared the field; the mock ignored it - Changed
MockSDK.model()declares itsMockModelreturn type — it always returned one, socallHistoryis now reachable without a cast.MockUsageis exported from the barrel - Fixed
ctx.run(agent, payload)now stringifies a non-string payload, as it always claimed to.coerceInlineInputinsrc/supervisor/execution.tsgated on!("signature" in executable)to decide whether the target was an agent — but every member ofSupervisableExecutable(AgentContract,WorkflowInstance,SupervisorContract) declaressignature, so the condition was permanently false and the coercion never ran. A supervisor intent callingctx.run(someAgent, { question: "why", attempt: 2 })handed the raw object toagent.execute(), where it landed as the user messagecontent—[object Object]in the prompt, or a provider-side payload rejection, depending on the adapter. The check now discriminates onisAnonymous, the one member unique toAgentContract. A regression test covers it; the old guard fails it withExpected: "string" / Received: "object". The unreferencedisSupervisor()duck-type helper — whose own JSDoc admitted it could not tell a supervisor from a workflow — is removed - Fixed
ToolMetano longer forceslabelandactionLabelon every tool that suppliesmeta. It was declared asRecord<"label" | "actionLabel" | (string & {}), unknown>, which makes both keys required, not optional — so any tool author who set one metadata field was made to set all of them. Now an optional-key shape with an index signature - Fixed
ToolConfig.actionis checked bivariantly, via aToolActionResolver<T>method-in-wrapper. The strictly contravariant parameter position rejected heterogeneous tool arrays that work correctly at runtime - Fixed
new Error(msg, { cause })compiles.tsconfig.jsondeclared nolib, so it inherited thetargetdefault of ES2020, whereErrorOptionsdoes not exist.libis now["ES2022"]; this also resolves theArray.atandString.replaceAllerrors. Emit is unchanged —targetis still ES2020. Notesrc/skills/sources/url-source.ts:122was not a defect: thecausewas always passed at runtime, the compiler simply had no type for it - Fixed
TeamMemberValueaccepts the callback member form (IntentCallback), which has always worked at runtime and was only rejected by the type - Fixed
PlanSchemano longer erases~standard.jsonSchemafrom its return type
- Added
GeminiImageModel— a Gemini-native image path overai.models.generateContent(newsrc/gemini-image.ts, exported asGeminiImageModel). RequestsresponseModalities: ["TEXT", "IMAGE"](override the list verbatim withoptions.responseModalities), mapsaspectRatio/imageSize/personGenerationonto Gemini'sconfig.imageConfig, and reshapes inline image parts that come back into the sameGeneratedImage[]({ type: "base64", base64, mediaType },image/pngfallback) the Imagen path emits — soai.image()'s envelope is unchanged for callers - Added Token usage is passed through on the Gemini image path instead of hard-zeroed. Whatever
usageMetadataGoogle attaches becomesusage.input/output/total(pluscachedTokens/reasoningTokenswhen reported> 0); only an absent block collapses to zeros. The Imagen path stays a flat zero because Imagen reports no tokens at all. Price these models with{ input, output }rather than{ perImage }, and check the first liveusage— whether these models report tokens is not confirmed here. The mapping is now a sharedapplyGoogleUsageutil used by both the chat model and the image model, so one rule decides what a Gemini token report means package-wide - Added A response with no image part is never a silent empty success: a blocked prompt (
promptFeedback.blockReason) or a safety/policyfinishReason(SAFETY,IMAGE_SAFETY,PROHIBITED_CONTENT,IMAGE_PROHIBITED_CONTENT,RECITATION,IMAGE_RECITATION,BLOCKLIST,SPII) throwsContentFilterErrorcarrying the reason; a text-only answer throwsProviderErrorquoting the text the model returned; anything else throwsProviderErrornaming the part count and finish reason - Changed
@google/genaimoves from^2.4.0to^2.17.1(2.17.1 is what installs today). The Gemini image path does not depend on the bump —models.generateContentexists in both — but the older range predates the deprecation notice above and predatesai.interactions, so staying on it meant documenting an SDK surface the package could not reach. The 11 suites / 149 specs in this package pass unchanged on 2.17.1. Note this re-resolved the whole workspace lockfile, not just this package's dependency - Changed
GoogleSDK.image()returnsGeminiImageModelfor agemini-id. This is routing, not validation — no id is rejected locally: an id matching neither family takes thegenerateImagesroute, the only route that existed before, so every id that reached Google before still reaches Google the same way and still fails (or succeeds) at the provider - Fixed
google.image({ name: "gemini-…" })no longer hits the endpoint that 404s it.ai.models.generateImagesroutes to{model}:predict(generateImages→generateImagesInternal→formatMap('{model}:predict', …)in@google/genai's bundle), which does not serve the Gemini image models — the call came back404 models/… is not found for API version v1beta, or is not supported for predict.GoogleSDK.image()now picks the transport from the id: agemini-id (with an optionalmodels/resource prefix) gets the newgenerateContentimplementation, everything else keepsGoogleImageModel/generateImages. Scope of the proof: two levels. Measured here — on the new transport such an id got as far as a quota error (HTTP 429) instead of the 404, which establishes that the endpoint accepts the id. Reported by the maintainer — once billing was enabled on the project, the path returned an image end-to-end from an application running a locally linked build of this package. No test in this package calls Google; the suite proves the request shape and the error mapping, not the round trip - Deprecated **Google has deprecated
generateImages, the transport theimagen-*path still uses.** Verbatim from the@google/genairuntime warning: *"The generateImages method is deprecated and will be removed in the next major release (not before Jan. 1 2027). Please use the generateContent method with image models instead. See https://ai.google.dev/gemini-api/docs/deprecations#imagen-models and https://docs.cloud.google.com/gemini-enterprise-agent-platform/models/capabilities/image-generation#generate-images"* (editImagecarries the same notice.) Nothing breaks today and the Imagen path is unchanged, but it is on a clock: new image work should prefer agemini-id. The warning is emitted by@google/genai≥ 2.17; with the bump below, this package now prints it whenever theimagen-*path is used
- Changed
openaimoves from^6.34.0to^7.4.0. The runtime was unaffected: the full suite — 13 files / 208 tests — passed on 7.4.0 *before* any of the type fixes below were made, so nothing about the wire shape this adapter sends or the responses it reads changed across the major. Every fix in this release is a compile-time one. - Changed
OpenAI.Images.ImageGenerateParamsBaseis no longer reachable upstream — openai 7 split image generation intoImageGenerateParamsNonStreaming/ImageGenerateParamsStreamingand stopped re-exporting the sharedBaseinterface from theImagesnamespace.image.tsnow sources itsquality/output_format/backgroundvalue types fromImageGenerateParamsNonStreaming, which is what the request body was already typed as and what the non-streamingimages.generateoverload accepts. The three fields are inherited fromBaseunchanged, so the accepted values are identical. - Changed
ChatCompletionToolbecame a union (ChatCompletionFunctionTool | ChatCompletionCustomTool) now that Chat Completions carries custom tools..functionis no longer reachable without thetypediscriminant, so the tool-conversion specs narrow ontype === "function"and throw on anything else — a custom-tool regression fails loudly rather than silently skipping the assertion it used to make. - Fixed
OpenAISDKConfig.provideris a usablestringagain. openai 7 added its ownprovider?: Providerkey toClientOptions— an opaque branded object minted bycreateProvider()— and our intersection collapsed the field toProvider & string, a type no string literal can inhabit. The config now omits the upstream key (Omit<ClientOptions, "provider">) before declaring its own label. No behavior change: the constructor already peeledprovideroff and never forwarded it to the OpenAI client.
- Fixed The RabbitMQ driver's lazy
amqplibloader was not idempotent under concurrent callers. It cached the resolved module in a module-level binding but nothing guarded the load itself, so two loads could be in flight at the same time and the last one to settle won the binding. The eager, unawaitedloadAmqplibModule()call at module scope was one of those callers by construction — it started a load nobody was waiting on, which then raced the awaited call fromconnect(). That eager call has been removed:connect()already awaits the loader, so it bought nothing but the race. The loader now memoizes the in-flight promise itself, so the first caller starts theimport()and every later caller awaits that same one - Fixed No user-visible misbehaviour is known in production — both racing paths resolve the same real
amqplib, so whichever won, callers got the module they expected. The observable damage was in test isolation: when a test was aborted mid-await import(...), the racing loads could leave the binding holding the realamqplibwhile the test file'svi.mock("amqplib")was still active, so every later test in that file silently bypassed the mock and opened a real socket. Proven by instrumentation — the driver held a liveChannelModelon::1:5672whileimport("amqplib")inside the same test still returned the mock, which is how a green test could be green for the wrong reason - Fixed Verified by a timeout sweep, not by a passing suite. The full suite passed both before and after (13 files / 137 tests), because the fault only surfaces when the first test is starved of time. Running
tests/connect-to-broker.test.tsat--testTimeout=3000and4000previously timed out the first test *and* tookwraps a connection failure with the driver namedown with it, failing in ~50 ms withpromise resolved "Broker{…}" instead of rejecting— the mock was gone. With the loader fixed, that test passes at every timeout even while the first test still times out: starving one test can no longer poison the next - Fixed The first test also paid a cold-transform cost inside its own timed body, since
connectToBrokerdynamically imports the driver, which pulls in@warlock.js/sealand@warlock.js/loggeras raw TS source. That work moved to abeforeAllwarm-up. This is a test-timing change only and carries none of the correctness weight above — the loader fix stands on its own without it
4.14.0
- Added
teardownTest()— the other half of the pair.setupTesthas shipped without a counterpart since it was introduced: there was no supported way to close the framework a test file brought up, and the only "reset" available was a module flag that proved nothing about whether ports, sockets, pools or timers had actually closed - Changed BREAKING — an explicit
setupTest({ connectors })now wins overtests.connectorsconfig. The order wasconfig > parameter > true; it is nowexplicit parameter > config > true - Changed BREAKING — a conflicting
setupTestcall rejects instead of being ignored. While a setup is starting or ready, a call with *different* effective options now rejects with an error naming both the active and the requested selection. The same options remain a no-op, and concurrent identical calls share one startup - Changed Lifecycle state is now scoped to the worker runtime instead of the module.
isSetupCompletewas a module-level variable, and Vitest rebuilds the setup module's registry between test files while the worker process or thread keeps running — so the flag reset in exactly the situation where live DB connections, pools and timers survive - Fixed A stranded setup no longer exhausts the heap. A lifecycle left in the
startingstate sentteardownTest's wait-then-re-enter path into unbounded recursion —FATAL ERROR: JavaScript heap out of memoryat 4 GB, killing the worker with 26 tests in that run never executed. It was found while proving the state machine, not reported by a user, and it would have shipped
- Changed
openai.image({ name }),openai.speech({ name })andopenai.transcribe({ name })no longer reject an unknown model id at construction — the id is forwarded to OpenAI as given, so an id OpenAI does not serve now fails as a typed provider error instead of a localInvalidRequestError. - Removed BREAKING —
isOpenAIImageModel()andOPENAI_IMAGE_MODEL_PREFIXESare no longer exported; theknown-image-modelsmodule is deleted. Nothing in the adapter read them once the construction-time gate went away, so they were a public list of model ids that enforced nothing and went stale on OpenAI's release schedule, not this package's. Import them from nowhere — branch on your own id list if you need one. - Removed BREAKING —
isOpenAISpeechModel()andisOpenAITranscriptionModel()are no longer exported either, for the same reason. Once their construction-time gates went away nothing in the adapter read them, leaving two more public model-id lists that enforced nothing. No@warlock.js/ai-*adapter validates a model id locally, so the package no longer ships a helper that implies otherwise — branch on your own id list if you need one.
- Changed
google.image({ name })no longer rejects a non-imagen-*model id at construction — the id is passed through toai.models.generateImagesas given, so an id Google does not serve now fails as a typed provider error instead of a localInvalidRequestError - Removed BREAKING —
isGoogleImageModel()andGOOGLE_IMAGE_MODEL_PREFIXESare gone from the public API. Both were dropped from the package entrypoint and the module deleted; importing either from@warlock.js/ai-googleis now a compile error. With the construction-time guard gone (below) they enforced nothing and only invited callers to re-implement a model allow-list the framework does not own — a model id is the provider's to rule on, so there is nothing left for a local list to say. Callers that branched on the Imagen family should match on the id themselves (name.startsWith("imagen-")) or, better, stop branching and let the provider answer
4.13.0
- Added
build.singleBundle— one file you can run withnode dist/app.js. The default build keeps dependencies as realimportspecifiers resolved fromnode_modules, which is right when you deploy the folder. Producing a single self-contained file previously meant knowing to setpackages: "bundle"andsplitting: false, and it still did not work - Added
@warlock.js/core/testsand@warlock.js/core/viteare real subpaths, with their own build entries andexportskeys — the first version in which those helpers are addressable at all./testscarries the 13 documented test helpers;/vitecarrieslowerStage3Decorators - Fixed
setupTest()no longer crashes in a project that has nosrc/config/tests.ts.config.get("tests")resolves an absent key tonull, and the result was dereferenced — so the very pathwarlock add testgenerates threwCannot read properties of null (reading 'connectors')before running a single test.setupTest()with no arguments at all threw one step earlier still, on a destructured parameter with no default - Fixed The request helpers send falsy JSON bodies.
testPost,testPutandtestPatchusedbody ? JSON.stringify(body) : undefined, sofalse,0,""andnull— all legal JSON documents — were sent as no body at all. Only an omitted argument now means "no body" - Fixed Shutdown survives a throwing log channel. A connector whose
shutdown()failed was reported throughlog.error(...)from inside the catch block — andLogger.log()hands each entry tochannel.log()with no isolation, so a channel that throws synchronously (a misconfigured transport, an unserialisable payload) made that report reject. The rejection escapedshutdown()entirely, and the consequences went well past a missing log line:log.flush()never ran, so every buffered entry from the whole run was lost; the remaining connectors were never torn down; andprocess.exit(0)— the linegracefulShutdownruns onceshutdown()resolves — was never reached, leaving the process alive on the handles those connectors still held - Fixed A test server that fails to start no longer leaves half of itself running.
startHttpTestServer()publishes the resolved port before the late connector phase and setsisServerRunningonly on its last line, so a failure in between left live early-phase connectors and a published port pointing at a server that never came up — whilestopHttpTestServer()inglobalTeardownreported *"No server to stop"* and walked away from them. Startup now unwinds what it started, always withdraws the port and resets its state. ⚠ The error you get back is unchanged — it always was. Startup had nocatchat all, so the original failure already propagated correctly; what was missing was the cleanup, and the newcatchexists only to run it. A failure *during* that cleanup is reported and never substituted for the cause, which is the one propagation guarantee the wrapper had to be careful not to break - Fixed
startHttpTestServer({ port: 0 })is refused with an instruction instead of half-working.0is the OS's "pick a free one" idiom, and the test server cannot honour it: the preflight would bind some unrelated ephemeral port and pass without proving anything, and nothing publishable exists afterwards —HttpConnector.start()records the port it asked for, not the one Fastify bound. Accepting it silently meantgetTestServerUrl()resolved0through its own config fallback and every worker request went tohttp://host:0, where nothing listens. The error names the fix: pass an explicit port, or sethttp.port - Fixed ⚠ BREAKING — the package entry no longer re-exports the CLI, the dev server, the test helpers or the Vite integration. Five
export *lines are gone from@warlock.js/core's root:./cli,./dev-server/files-orchestrator,./dev-server/health-checker,./testsand./vite - Fixed ⚠ BREAKING — the CORS allow-list in
http.corsnow actually applies. The framework's defaults were spread after your configuration, so{ origin: "*", methods: "*" }overwrote whatever you set.http.corshas never had any effect, in any release up to 4.12.0 — an app that configured an allow-list still answered every origin. Your configuration now wins - Fixed ⚠ BREAKING —
http.bodyLimitno longer defaults to 200 GB. An app that configures nothing now gets Fastify's own 1 MB limit. The previous default did not merely allow large bodies, it replaced a protection Fastify provides: an unauthenticated endpoint accepted a 5 MB body and ran application logic on it where bare Fastify would have answered413 - Fixed ⚠ BREAKING —
http.trustProxynow defaults tofalse.request.ipwas derived from the client-suppliedX-Forwarded-Forheader by default, and@fastify/rate-limitkeys its buckets onrequest.ip— so a client sending a differentX-Forwarded-Foron each request got a fresh rate-limit bucket every time. Any deployment not behind a proxy that strips the header had bypassable rate limiting, and the same applied to per-IP lockouts and audit logs - Fixed Per-route
serverOptionsare no longer discarded by the dev server.scanDevServerregisters wildcard routes and dispatches per request, so it had no per-route registration slot and droppedserverOptionsentirely. A route declaringserverOptions.onRequest— the documented way to run before body parsing — worked in production and silently never ran in dev, which is the only mode most teams run.route.rateLimitwas dropped the same way, since it rides in the same options object - Fixed The dev server no longer rebuilds its entire route registry on every request. A comment claimed the registry was initialised "once" and pointed at a
rebuildRouteRegistryfunction that does not exist; the code sat inside the per-request handler, re-registering every route on every hit — androuter.any()routes expand into seven registrations each. It is now built once and rebuilt when the route table changes - Fixed The dev dispatcher logs through the framework logger instead of
console.log(error), with the request method and url attached - Fixed A bundled production build no longer succeeds and then dies at startup. Setting
packages: "bundle"produced a clean build whose process failed immediately withError: Dynamic require of "node:assert" is not supported. Warlock's output is an ES module; bundled CommonJS dependencies callrequire(...)and read__dirnameto locate their own assets, and neither exists in an ES module, so the bundler substituted a stub that throws. The only fix available to an application author was to hand-write an esbuildbannerrecreatingrequireviacreateRequire(import.meta.url)— esbuild internals no app should need to know
4.12.0 August 11, 2026
@warlock.js/core adds warlock migrate --pending — what will run next, in execution order, with exit codes you can gate a deploy on — and stops two CLI flags doing the opposite of what they say: migrate --rollback=false dropped every table, and generate.module --force=false overwrote your files, because boolean options were parsed as raw strings and "false" is truthy. @warlock.js/auth closes a token-expiry hole: an unparseable expiresIn minted a JWT with no exp claim, such a token was then accepted forever, and the rows were never purged — tokens are now rejected at issue and at verification, and auth:purge-never-expiring finds and revokes the ones already in your database. @warlock.js/core also fixes Image and renderReact racing their own optional-dependency imports, where a constructor could run before sharp or react had loaded and fail with not a function.
- Added
warlock migrate --pending— what will run next, in the order it will run.migratecould report what had already run (--list) and what files existed on disk (--all), but not the one thing an operator asks before a schema change against a live database. The pending set was already computed on every migrate run; it simply had no read-only exit - Changed
migrate's preload block no longer declaresenv: true. The flag has done nothing since env began loading for every command that declares a preload block; it was decoration, and the test suite now asserts its absence so it is not re-added by someone reading the still-deprecated type - Changed The package now declares its own test runner and a
testscript.@warlock.js/coreshipped a maintainedvitest.config.ts— aliasing eight sibling packages to their sources — with nodevDependencieskey at all and no way to invoke it. Its suite was reachable only by knowing to typenpx vitest, which resolves whatever happens to exist in the tree rather than anything the manifest asked for. The runner is pinned to an exact version, not a range: it moved from 4.1.8 to 4.1.10 mid-development on an unrelated install, silently, and a suite whose runner can change underneath it proves less than it appears to - Fixed A build artifact that names an entry point it does not contain is now refused before it can be packed. An interrupted build leaves a directory that looks finished —
package.json,README,CHANGELOG,bin/,skills/— and holds no compiled code at all. Nineteen existed in this tree at once, and nothing in the release path noticed: the only related guard compares modification times, so a hollow directory with a freshly written manifest is *newer than source* and passes, and it runs solely on the artifact-reuse path, which is not how the hollow directories were produced - Fixed The production acceptance gate no longer inherits the environment it is supposed to be testing.
run-pnpm-acceptance.mjsspawned every child withenv: { ...process.env }and set noNODE_ENV. It exercised the production path only because the shell it was written in happened to carryNODE_ENV=production; on a clean checkout, a new contributor's machine, or CI, the same gate boots the app in development — and does not fail, it passes while testing something other than the thing it is named after. That is the worst outcome available to a gate, and it sat underneath the proof for 4.11.0's headline fix - Fixed
warlock migrate --rollback=falseno longer drops every table. CLI options were parsed as raw strings and nothing ever coerced them:--rollback=falsereached the action as the string"false",if (rollback)saw a truthy value, and the run rolled back *everything*. The declaredtype: "boolean"on the option was decorative — used only to render help. The same shape existed on every boolean option, includingwarlock drop.tables --force=false, where it turned a confirmation prompt into an unattended drop - Fixed
warlock generate.module users --force=falseno longer overwrites your files. The coercion above is opt-in by design — it applies only to options a command declarestype: "boolean", so a string option whose value is genuinely the wordfalsesurvives. The generate family andaddnever carried that declaration, so the fix reached none of them and both faces of the defect stayed live on the commands most likely to be run against existing source - Fixed
new Image(...)no longer fails depending on how soon you call it. TheImagemodule firedimport("sharp")at load time without awaiting it, and the constructor only checked whether that import had *failed* — never whether it was still in flight. Constructing an image in the first tick after importing the package therefore ran with an undefined sharp function and died withTypeError: sharpFn is not a function; the exact same code passed if something had awaited a timer first. Anything that builds an image during boot — a startup thumbnail job, a module-level warm-up — hit it, and it presented as a mysterious "works locally, breaks in prod" timing bug rather than as a missing dependency - Fixed A sharp that is installed but will not load no longer reports itself as "not installed". The resolution above swallowed every failure into a single outcome, so the most common real-world sharp problem — the package present but its native binary built for another platform — arrived as
sharp is not installed.plus instructions to runnpm install sharp, which cannot fix it. sharp throws its own long, actionable error naming the runtime, the failing.nodefile and the exact install flags to use; that text was discarded and replaced with a different, wrong cause - Fixed
renderReact()no longer renders against modules that have not loaded yet. The same defect as the two above, in a second module, found by looking for the pattern rather than by a bug report.react/index.tsfiredimport("react")andimport("react-dom/server")at load time without awaiting either, and tracked them with a three-state flag that the guard only tested for one state:if (moduleExists === false). While the imports were in flight the flag wasnull, which is notfalse, so the guard passed and the synchronousrenderReactreadcreateElementoffundefined. With two sequential dynamic imports the window is wider than the image module's, and it is open during exactly the work a server does at boot — rendering a page or an email template from a module-level warm-up - Fixed A broken
react-dom/serverno longer reports itself asreact is not installed. The two packages were loaded in onetryand collapsed into one flag, so any failure of either was attributed to react. The specifiers are now resolved and reported separately, and the message names the one that actually failed —Failed to load "react-dom/server": …— because sending an operator to reinstall react when react is fine costs them the debugging session. Absence is distinguished from breakage the same way as for sharp:MODULE_NOT_FOUNDand a message naming the specifier exactly, quoted, which is also what stops'react-dom'from satisfying a check for'react'. Everything else surfaces the original error, inlined and chained ascause. Areact-domwhose./serversubpath is missing fromexportsraisesERR_PACKAGE_PATH_NOT_EXPORTED, so it correctly reports as an incompatible install rather than an absent one
- Added
warlock auth.purge-never-expiring— remediation for rows written by theexpiresIndefect below. Finds every access- and refresh-token row that can never retire itself, on two independent signals: anexpires_atthat is missing or unparseable, and a persisted token carrying noexpclaim. Reportsid,user_id,user_typeandexpires_atper row (never the token string — it is a live credential until the command removes it), then deletes them. Pass--dry-runto report without deleting. - Changed Potentially breaking: a JWT with no
expclaim is rejected byjwt.verify/jwt.verifyRefreshToken. No supported configuration produces one: an app that wants a token that effectively never expires setsexpiresIn: NO_EXPIRATION("100y"), which mints a realexpabout a century out (ms("100y")⇒3155760000000;exp - iat⇒3155760000seconds). "No deadline" and "a distant deadline" are different things, and only the second was ever asked for. If you sign tokens with your own signer and feed them to this package's verifier, they must carryexp. - Changed Potentially breaking:
RefreshToken.isExpirednow answerstruefor a missing or unparseableexpires_at; it previously answeredfalse("no expiry recorded ⇒ never expires"). That reading handed an unlimited life to precisely the malformed rows.expires_atisrequiredin the schema — a row that cannot say when it dies is malformed, not immortal.AccessToken.isExpiredis new and fails closed the same way. - Changed Potentially breaking: an invalid
accessToken.expiresIn/refreshToken.expiresInnow throws on token issue instead of producing a token with a wrong or absent expiry. Valid configuration is unaffected —"1h","7d","30 days",NO_EXPIRATION("100y"), the1haccess default when the key is absent, and the7drefresh default all behave exactly as before. An empty string (e.g.env("JWT_TTL")with the variable unset) now throws rather than falling back; give the env read an explicit default. - Changed
authConfig.accessToken.expiresInMs()/authConfig.refreshToken.expiresInMs()are the validated accessors token issuers must use; the rawexpiresIn()accessors are unchanged. - Changed Removed the
as ms.StringValuecasts on both call sites. They were what let arbitrary config text compile againstms's template-literal type and reach the signer asundefined. - Changed Declares its own test runner and pins it to an exact version (
vitest@4.1.10). The package is its own repository, so a runner resolved from a workspace root it may not be cloned with is a runner it cannot rely on. The pin is exact rather than a range because the version moved underneath the suite mid-development on an unrelated install — a suite whose runner can change without anyone choosing it proves less than it appears to - Fixed An
expiresInthemspackage cannot parse no longer mints a credential that never expires.accessToken.expiresIn: "30dayz"(or"thirty days", or any truthy-but-unparseable value) madems()returnundefined, which the signer emitted as a JWT with noexpclaim, alongside a token row whoseexpires_atwasInvalid Date. The old guard tested the raw config string for truthiness, so the1hfallback was unreachable in exactly the case it existed for.refreshToken.expiresInhad no fallback at all. - Fixed
expiresIn: "0d"(and any non-positive duration) is rejected too. It is truthy and parses cleanly to0, so it survived any guard that only rejectsundefined— andfast-jwtskips its own validation for0, emitting a token with noexpclaim while the persisted row claims it expired immediately. - Fixed A bare number (
expiresIn: 2592000) is now rejected instead of silently corrupting the expiry.ms*formats* numbers rather than parsing them (2592000⇒"43m"), which then poisonedDate.now() + expiresInintoInvalid Date. Write"30d". - Security A token with no
expclaim is now rejected instead of being accepted forever.fast-jwthas no deadline to check on such a token, so verification simply succeeds — measured againstfast-jwt@6.2.4, a token with noexpverifies unchanged atclockTimestamp+ 100 years. Bothjwt.verifyandjwt.verifyRefreshTokennow require anexpclaim. - Security The persisted
expires_atis now enforced on every request.authMiddlewarepreviously checked only that the access-token row *existed*; a row whose own expiry had passed still opened the gate, because nothing ever asked. The row is now checked against the clock and deleted on rejection.
- Added
listPendingMigrations()— the registered migrations that have not executed, in the order they will execute, mirroringlistExecutedMigrations(). The set was already computed inside the runner on every migrate run;getPendingMigrations()wasprivateand had no read-only exit, so nothing outside could ask "what will run next?" without running it - Changed Declares its own test runner and pins it to an exact version (
vitest@4.1.10). The package is its own repository, so a runner resolved from a workspace root it may not be cloned with is a runner it cannot rely on. The pin is exact rather than a range because the version moved underneath the suite mid-development on an unrelated install — a suite whose runner can change without anyone choosing it proves less than it appears to
- Changed Declares its own test runner and pins it to an exact version (
vitest@4.1.10). The package is its own repository, so a runner resolved from a workspace root it may not be cloned with is a runner it cannot rely on. The pin is exact rather than a range because the version moved underneath the suite mid-development on an unrelated install — a suite whose runner can change without anyone choosing it proves less than it appears to - Changed Adds a
testscript. The package shipped atests/directory with no way to run it, so the suite was reachable only by knowing to typenpx vitest— which resolves whatever happens to exist in the tree rather than anything the manifest asked for
- Changed Declares its own test runner and pins it to an exact version (
vitest@4.1.10). The package is its own repository, so a runner resolved from a workspace root it may not be cloned with is a runner it cannot rely on. The pin is exact rather than a range because the version moved underneath the suite mid-development on an unrelated install — a suite whose runner can change without anyone choosing it proves less than it appears to
- Changed Declares its own test runner and pins it to an exact version (
vitest@4.1.10). The package is its own repository, so a runner resolved from a workspace root it may not be cloned with is a runner it cannot rely on. The pin is exact rather than a range because the version moved underneath the suite mid-development on an unrelated install — a suite whose runner can change without anyone choosing it proves less than it appears to
- Changed Declares its own test runner and pins it to an exact version (
vitest@4.1.10). The package is its own repository, so a runner resolved from a workspace root it may not be cloned with is a runner it cannot rely on. The pin is exact rather than a range because the version moved underneath the suite mid-development on an unrelated install — a suite whose runner can change without anyone choosing it proves less than it appears to
- Changed Declares its own test runner and pins it to an exact version (
vitest@4.1.10). The package is its own repository, so a runner resolved from a workspace root it may not be cloned with is a runner it cannot rely on. The pin is exact rather than a range because the version moved underneath the suite mid-development on an unrelated install — a suite whose runner can change without anyone choosing it proves less than it appears to
- Changed Declares its own test runner and pins it to an exact version (
vitest@4.1.10). The package is its own repository, so a runner resolved from a workspace root it may not be cloned with is a runner it cannot rely on. The pin is exact rather than a range because the version moved underneath the suite mid-development on an unrelated install — a suite whose runner can change without anyone choosing it proves less than it appears to
- Changed Declares its own test runner and pins it to an exact version (
vitest@4.1.10). The package is its own repository, so a runner resolved from a workspace root it may not be cloned with is a runner it cannot rely on. The pin is exact rather than a range because the version moved underneath the suite mid-development on an unrelated install — a suite whose runner can change without anyone choosing it proves less than it appears to
- Changed Declares its own test runner and pins it to an exact version (
vitest@4.1.10). The package is its own repository, so a runner resolved from a workspace root it may not be cloned with is a runner it cannot rely on. The pin is exact rather than a range because the version moved underneath the suite mid-development on an unrelated install — a suite whose runner can change without anyone choosing it proves less than it appears to
- Changed Declares its own test runner and pins it to an exact version (
vitest@4.1.10). The package is its own repository, so a runner resolved from a workspace root it may not be cloned with is a runner it cannot rely on. The pin is exact rather than a range because the version moved underneath the suite mid-development on an unrelated install — a suite whose runner can change without anyone choosing it proves less than it appears to
- Changed Declares its own test runner and pins it to an exact version (
vitest@4.1.10). The package is its own repository, so a runner resolved from a workspace root it may not be cloned with is a runner it cannot rely on. The pin is exact rather than a range because the version moved underneath the suite mid-development on an unrelated install — a suite whose runner can change without anyone choosing it proves less than it appears to
- Changed Declares its own test runner and pins it to an exact version (
vitest@4.1.10). The package is its own repository, so a runner resolved from a workspace root it may not be cloned with is a runner it cannot rely on. The pin is exact rather than a range because the version moved underneath the suite mid-development on an unrelated install — a suite whose runner can change without anyone choosing it proves less than it appears to
- Changed Declares its own test runner and pins it to an exact version (
vitest@4.1.10). The package is its own repository, so a runner resolved from a workspace root it may not be cloned with is a runner it cannot rely on. The pin is exact rather than a range because the version moved underneath the suite mid-development on an unrelated install — a suite whose runner can change without anyone choosing it proves less than it appears to
- Changed Declares its own test runner and pins it to an exact version (
vitest@4.1.10). The package is its own repository, so a runner resolved from a workspace root it may not be cloned with is a runner it cannot rely on. The pin is exact rather than a range because the version moved underneath the suite mid-development on an unrelated install — a suite whose runner can change without anyone choosing it proves less than it appears to
- Changed Declares its own test runner and pins it to an exact version (
vitest@4.1.10). The package is its own repository, so a runner resolved from a workspace root it may not be cloned with is a runner it cannot rely on. The pin is exact rather than a range because the version moved underneath the suite mid-development on an unrelated install — a suite whose runner can change without anyone choosing it proves less than it appears to
- Changed Declares its own test runner and pins it to an exact version (
vitest@4.1.10). The package is its own repository, so a runner resolved from a workspace root it may not be cloned with is a runner it cannot rely on. The pin is exact rather than a range because the version moved underneath the suite mid-development on an unrelated install — a suite whose runner can change without anyone choosing it proves less than it appears to
- Changed Declares its own test runner and pins it to an exact version (
vitest@4.1.10). The package is its own repository, so a runner resolved from a workspace root it may not be cloned with is a runner it cannot rely on. The pin is exact rather than a range because the version moved underneath the suite mid-development on an unrelated install — a suite whose runner can change without anyone choosing it proves less than it appears to
- Changed Declares its own test runner and pins it to an exact version (
vitest@4.1.10). The package is its own repository, so a runner resolved from a workspace root it may not be cloned with is a runner it cannot rely on. The pin is exact rather than a range because the version moved underneath the suite mid-development on an unrelated install — a suite whose runner can change without anyone choosing it proves less than it appears to
- Changed Declares its own test runner and pins it to an exact version (
vitest@4.1.10). The package is its own repository, so a runner resolved from a workspace root it may not be cloned with is a runner it cannot rely on. The pin is exact rather than a range because the version moved underneath the suite mid-development on an unrelated install — a suite whose runner can change without anyone choosing it proves less than it appears to
- Changed Declares its own test runner and pins it to an exact version (
vitest@4.1.10). The package is its own repository, so a runner resolved from a workspace root it may not be cloned with is a runner it cannot rely on. The pin is exact rather than a range because the version moved underneath the suite mid-development on an unrelated install — a suite whose runner can change without anyone choosing it proves less than it appears to
- Changed Declares its own test runner and pins it to an exact version (
vitest@4.1.10). The package is its own repository, so a runner resolved from a workspace root it may not be cloned with is a runner it cannot rely on. The pin is exact rather than a range because the version moved underneath the suite mid-development on an unrelated install — a suite whose runner can change without anyone choosing it proves less than it appears to
- Changed Declares its own test runner and pins it to an exact version (
vitest@4.1.10). The package is its own repository, so a runner resolved from a workspace root it may not be cloned with is a runner it cannot rely on. The pin is exact rather than a range because the version moved underneath the suite mid-development on an unrelated install — a suite whose runner can change without anyone choosing it proves less than it appears to
- Changed Declares its own test runner and pins it to an exact version (
vitest@4.1.10). The package is its own repository, so a runner resolved from a workspace root it may not be cloned with is a runner it cannot rely on. The pin is exact rather than a range because the version moved underneath the suite mid-development on an unrelated install — a suite whose runner can change without anyone choosing it proves less than it appears to
- Changed Declares its own test runner and pins it to an exact version (
vitest@4.1.10). The package is its own repository, so a runner resolved from a workspace root it may not be cloned with is a runner it cannot rely on. The pin is exact rather than a range because the version moved underneath the suite mid-development on an unrelated install — a suite whose runner can change without anyone choosing it proves less than it appears to
4.11.0 August 10, 2026
@warlock.js/core fixes four cases where the framework reported success it had not verified: a production bundle that imported a package your app never declared and so could not boot under pnpm, env() returning its default inside warlock.config.ts, warlock start printing its started banner before the app had booted, and a storage connector that could not start an app shipping no src/config/storage.ts. Also raises @mongez/dotenv to ^1.3.1.
- Added
startHttpTestServer({ port })— run an integration suite on an explicit port, honoured overHTTP_PORTin.env, which the internal bootstrap re-reads and no caller could previously override - Added the test server preflights its port and fails with "stop the dev server" naming the port, instead of a raw
EADDRINUSEfrom inside Fastify - Added
Application.setServedPort()and aportfield on the readiness signal, so a supervisor learns the bound http port from the app rather than re-deriving it from config it may not be able to read - Added
setConfig(name, value)— the write side of the config store, exported separately from the read-onlyconfigaccessor so registering configuration stays a deliberate boot-time act - Changed
@mongez/dotenvis now required at^1.3.1(was^1.2.4). Under the old range a fresh install resolved to 1.3.x while an existing lockfile could stay on 1.2.x, so we could not say which behaviour a given consumer actually had. 1.3.x only changes cases that were previously wrong:env()now consultsprocess.envinstead of returning a default for a key the environment defines,${VAR}interpolation throws naming the key instead of baking the string"undefined"into a value, and numeric coercion no longer corrupts values like0123456789or IDs beyond 2^53. Precedence between.envfiles and injected variables is unchanged - Fixed A production bundle no longer imports a package your app does not declare.
warlock build's generated config loader emittedimport config from "@mongez/config"— one of *core's* dependencies, never the app's. npm and yarn hoist flat so it resolved by accident; under pnpm's strict layout the shipped bundle died at boot withERR_MODULE_NOT_FOUNDfor a package the app had no reason to install. The generator now emitssetConfigfrom@warlock.js/core, which the app does declare, and Node resolves@mongez/configfrom core's own install — correct under pnpm, and portable, unlike baking absolute paths into an artifact meant to be copied between machines - Fixed
env()insidewarlock.config.tsno longer always returns its default. The config module was evaluated *before* any.envfile was read, so a project following the documentedbuild: { outdir: env("BUILD_OUT", "dist") }recipe silently gotdistno matter what the environment said — under every command,devincluded, and underbuildandstartenv was never loaded at all. Env files are now loaded beforewarlock.config.tsis evaluated, for every command - Fixed An application without
src/config/storage.tscan boot again. The storage connector starts unconditionally, on the documented grounds thatstorage.init()falls back to a built-inlocaldriver so file storage works out of the box. That fallback was never implemented:init()resolved the default driver *name* and then found nothing registered under it, so any app without a storage config died at boot withStorage driver "local" is not configured. A built-inlocaldriver rooted atuploadsPath()is now registered before configured drivers — so an app defining its ownlocalstill overrides it, and naming a driver that genuinely does not exist still fails loudly - Fixed
startHttpTestServerno longer breaks a suite that configureshttp.port: 0.0is the OS's "pick a free port for me" idiom, but the guard only checkedtypeof port !== "number", so0fell through: the preflight bound an unrelated ephemeral port and passed without proving anything, and0was then published as the bound port, pointing every request in the suite athttp://host:0. An explicit0now takes the same path as no configured port — no preflight, nothing published, Fastify picks the port - Fixed
warlock startno longer claims success before the app has booted. The startup banner printed inpreAction— before the child process was even spawned — and the failure that followed went only to stderr. Any CI gate or process supervisor that watches stdout for the banner read a 🔴 boot failure as a healthy start, which is how a production app that never booted was recorded as running. The banner now prints only when the application reports a completed boot, and a child that dies before reporting is a failed start: the message goes to both stdout and stderr, and the exit code is forced non-zero even when the process itself exited0 - Deprecated the
envpreloader flag on a CLI command is no longer read — env is loaded for every command that declares a preload block. Setting it is harmless and does nothing; remove it. Dropped at 5.0
4.10.0 August 9, 2026
@warlock.js/core makes response.cookie secure by default — httpOnly, sameSite: "lax", and secure outside development are applied unless you override them. Nothing set them before, so a cookie was readable by any injected script, sent in cleartext, and attached to cross-site requests unless the developer passed three flags on every call; nothing failed when they were missing. @warlock.js/auth documents two things that were previously only findable by reading source: that authMiddleware gates on flat user-type matching and points at @warlock.js/access for permission matrices and who-may-act-on-whom policies, and that auth is bearer-token by design, with cookie sessions and CSRF being app-level work.
- Changed
response.cookieis now secure by default —httpOnly: true,sameSite: "lax", andsecure: trueoutside development are applied unless overridden. Previously nothing set them: a cookie was readable by any injected script, sent in cleartext, and attached to cross-site requests unless the developer knew to pass three flags on every call. Nothing failed when they were missing, so the app worked and was simply insecure. Precedence is framework defaults →http.cookies.options→ the per-call argument, so opting out stays possible and explicit.secureis relaxed only in development, because browsers drop aSecurecookie over plain http
- Changed
protect-routesdocuments two things that were previously only discoverable by reading source: thatauthMiddlewaregates on user type by flat string match and cannot express a permission matrix, role hierarchy, or who-may-act-on-whom — with a worked pointer to@warlock.js/access(gate,can,definePolicy) for exactly that; and that auth is bearer-token by design, with cookie sessions and CSRF being app-level work rather than an omission
4.9.2 August 9, 2026
@warlock.js/cascade fixes migrate:rollback running down() migrations in *apply* order — the rollback list was reversed and then re-sorted ascending, putting it straight back, so any batch with more than one migration could drop a table before dropping the column added to it. @warlock.js/seal fixes v.literal(""), which could never pass because every validator is required by default and "" counts as empty — while .optional() looked like a workaround but silently disabled the literal check entirely; seal also stops returning the rejected input as data on a failed validation, closing a leak where an outbound DTO's internal fields reached callers who didn't branch on isValid, and fixes v.number().toFixed(n), which returned a string its own number rule then rejected. @warlock.js/core stops the generated dev-server loader hook shipping bare esbuild / get-tsconfig imports into your project, which broke warlock dev on pnpm's strict layout.
- Fixed
migrate:rollbackandmigrate:rollback --allrandown()migrations in apply order instead of reverse.getMigrationsToRollbackreversed the executed list and then re-sorted it ascending, which put it straight back into forward order and made the reverse dead code — so a rollback would drop a table before dropping the column added to it, failing withrelation "…" does not exist. Any batch containing more than one migration was affected; single-migration batches hid it because one item has no order to get wrong - Fixed migration ordering now lives in
migration-order.tswith an explicitsortMigrationsForRollback. The descending sort is required, not cosmetic: the executed list is read back ordered bybatch, name, so it is alphabetical rather than chronological and simply *not* re-sorting after the reverse would have produced reverse-alphabetical order — a different wrong answer
- Fixed
v.literal("")could never pass. Every validator is required by default andrequiredrejects anything the empty-value check calls empty — which includes""— so a schema demanding an exact empty string reported "is required" for a field that was present. A literal set containing an empty value now usespresent(the key must exist) instead ofrequired, leaving the literal set to judge the value. Only the empty string was affected;v.literal(0)andv.literal(false)always worked - Fixed
v.literal("").optional()silently disabled the literal check rather than fixing it, accepting"",nulland a missing key alike. The literal rule now runs on empty values (requiresValue: false) while treating absence as the required/present rule's question, so.optional()means optional again and a present value must still match - Fixed a failed validation no longer returns the input it rejected.
objectreturned the raw input — including the unknown keys it had just complained about — whilediscriminatedUnionreturnedundefined; the same call shape had two contracts. Validating an outbound DTO to keep internal fields out of a response, then readingdatawithout branching onisValid, shipped every field the schema existed to exclude.datais nowundefinedwheneverisValidisfalse - Fixed
v.number().toFixed(n)could never produce a valid result — the mutator returnedNumber(value).toFixed(n), a *string*, which the validator's ownnumbertype rule then rejected. It now yields a number (3.14159→3.14), so the method works where it lives. No working code can have depended on the old output, since every such validation failed; for a fixed-point *string*, format at the presentation edge rather than asking a number schema to emit one
- Fixed the dev server's generated
.warlock/loader-hook.mjsno longer ships bareesbuild/get-tsconfigimports. That file is written into the consuming app's directory, so a bare specifier resolves from the app — but both packages are core's own dependencies. npm and yarn hoist flat so it worked by accident; under pnpm's strict layout the dev server died withERR_MODULE_NOT_FOUNDfor a package the app never imported. Each npm specifier is now rewritten at generation time to an absolute path resolved from core's own install, so no consumer has to declare a phantom dependency
4.9.1 August 6, 2026
@warlock.js/cascade fixes a silent data-loss bug in .save({ merge }): a Date written over a column that already held a Date was discarded — the dirty tracker treated any typeof "object" value as mergeable and recursed into the Date, which has no own enumerable properties, so nothing was copied and the column never went dirty. save() returned { success: true, modifiedCount: 0 } and issued no UPDATE. Only plain objects deep-merge now; Date, Map, Set and every other class instance replace.
- Fixed
save({ merge })silently dropped aDatewritten over a column that already held aDate— the dirty tracker's merge treated anythingtypeof "object"as mergeable and recursed into theDate, which has no own enumerable properties, so nothing was copied and the old value survived. The column never went dirty andsave()returned{ success: true, modifiedCount: 0 }without issuing anUPDATE. Only plain objects deep-merge now;Date,Map,Set,RegExpand every other class instance replace, matching whatmodel.dataalready did. Writing into an empty column always worked, so only overwrites were affected
4.9.0 August 6, 2026
@warlock.js/core's dev server now runs supervised — a thin parent respawns the server instead of stacking a process per restart — and restarts itself when warlock.config.ts or .env changes, with keyboard shortcuts (r restart, c clear, q quit, h help, u update-and-restart), crash recovery, Bun lockfile support in update / add, new update --dry-run / --check flags, and an honest offline update check that no longer reports "already up to date" when npm was never reached. @warlock.js/cascade fixes migrations running in filename order instead of chronological order on a fresh database — the sort parsed timestamps with new Date(), which cannot read the framework's own MM-DD-YYYY_HH-MM-SS stamp, so every one collapsed to the alphabetical tiebreaker. The release also fixes build.outDirectory — the name the docs used for several releases while only build.outdir was ever read, so a config written from the documentation was silently ignored. @warlock.js/ai workflow run steps now correctly nest a directly-invoked agent (workflow → agent → tool) instead of producing two disconnected top-level traces, and @warlock.js/ai-openai now defaults reasoning_effort to "none" automatically whenever a reasoning-capable model is called with tools, closing the gap that left tool calls broken on gpt-5 / o-series models unless every call site opted in by hand.
- Added
warlock devkeyboard shortcuts —rrestart,cclear,qquit,hhelp — armed once the server is ready and listed byh. TTY-gated, andCtrl+Ckeeps working while raw mode is held - Added press
uon thewarlock devupdate notice to update every@warlock.js/*dependency, install, and restart the server in place — no Ctrl+C round-trip. Falls back to the printednpx warlock updatecommand when the terminal can't deliver keypresses (CI, piped stdin, supervisors) - Added
warlock devnow runs as a supervised pair — a thin parent that owns the terminal and a disposable worker — so restarting replaces the worker instead of stacking a process per restart, and the supervisor never loads config or connectors - Added
warlock devrestarts automatically whenwarlock.config.tsor any.env*changes, since neither can be hot-reloaded; opt out withdevServer.restartOnConfigChange: falsefor the previous warning - Added Bun support in
warlock updateandwarlock add—bun.lock/bun.lockbare detected and drivebun install/bun add - Added
warlock devrecovers from a crash: a worker that dies after running healthily for 5s is replaced automatically, while one that dies during boot is left alone so its error isn't buried under a reprint. Capped at 3 crashes per minute - Added
warlock update --dry-runreports what would change without touching anything, and--checkdoes the same but exits1when a package is behind — a CI gate for staying current - Changed the dev-server update check remembers npm's answer for 24h in
.warlock/update-check.json, so a day of restarts costs one lookup instead of one per boot; failed lookups are never cached, and the entry is dropped once an update is applied - Fixed
warlock startspawnsprocess.execPathinstead of a barenode, which failed withENOENTwherevernodeis not onPATH(systemd units, cron, slim containers) and could otherwise pick a different Node version than the one running the CLI - Fixed
warlock addno longer carries its own package-manager detection that silently produced an undefined install command when the project had no recognised lockfile — it shares the updater's detection - Fixed
build.outDirectory— the name the docs have used for several releases — is now actually read. Onlybuild.outdirever was, so a config written from the documentation was silently ignored and the bundle still went todist/. Both names now work (outdirwins if you set both) and the docs lead withoutdir - Fixed
warlock updateno longer reports "All @warlock.js packages are already up to date" when it never reached the npm registry — an offline run now says so and changes nothing - Fixed a failed package-manager install during
warlock updateno longer loses the rewrittenpackage.json; the CLI still exits non-zero - Fixed the dev server's update check now uses a 5s abort budget instead of 30s, so a hanging network can't leave a pending request behind a running server
- Fixed migrations ran in filename order instead of chronological order on any fresh database.
SQLGrammar.sort— the comparator that decides execution order across every pending migration — parsedcreatedAtwithnew Date(), which cannot read theMM-DD-YYYY_HH-MM-SSstamp the framework's own generator produces; every timestamp becameNaN, was floored to0, and the alphabetical tiebreaker silently decided the whole ordering. A January 2026 migration would run before a December 2025 one - Fixed
parseCreatedAtnow lives in its own module and backs both migration comparators through a sharedcompareCreatedAt, so the two can no longer drift apart — one of them being wrong was the symptom, two comparators sorting the same data by different rules was the defect
- Added
StepSnapshot.children— reports a workflowrunstep captured from any executable its callback invoked DIRECTLY (agent.execute(...)rather than the declarativeagent:field), via the same ambientRunFramea supervisor/team/orchestrator callback already gets.report.childrennow includes these alongsidestep.agentreports. - Fixed A workflow
runstep that callsagent.execute()directly no longer produces two disconnected top-level traces (one "agent", one "workflow") with the agent missing fromreport.children— it now nests correctly (workflow → agent → tool, usage/cost rolled up) and no longer also self-routes as a separate observed trace. Declarativestep.agentwas already correct; this closes the gap for ad-hoc calls insiderun(self-documented inworkflow/engine.tsas a known limitation).
- Fixed
reasoning_effortnow defaults to"none"automatically on a reasoning-capable model called WITHtoolsand no explicitreasoning.effort— previously this required every call site to opt in (added in 4.8.0), so any agent/model config that didn't know to pass it kept hitting rejected tool calls (empty replies, or a hard 400 on newer model generations —"Function tools with reasoning_effort are not supported ... in /v1/chat/completions"). An explicitreasoning.effortstill overrides the default in either direction; calls with notoolsare unaffected.
4.8.2 July 22, 2026
@warlock.js/ai-panoptic's dashboard gains an "Evaluate system prompt" drawer action — its first write-capable route — plus a round of dashboard reliability fixes (auth-token forwarding, a silent cache-store failure hook) and a @warlock.js/ai peer-dependency / redact() hardening pass.
- Added
DashboardOptions.evaluate— an "Evaluate system prompt" drawer action that grades a trace's last captured system prompt via an LLM judge, editable per-run instructions included; the dashboard's first and only write-capable route (POST .../spans/:spanId/evaluate), off unless configured and gated by the sameauthToken/allowedHostschecks as every other route - Added
evaluateSystemPrompt/extractLastSystemPrompt/findSpanById— the building blocks behind the drawer action, exported for scripting a grade outside the UI - Fixed The dashboard's client-side poll (
GET {basePath}api/aggregate/api/traces) now carries the page's?token=as anAuthorization: Bearerheader on every request — previously, onceauthTokenwas configured the initial page load succeeded (the browser's navigation request carries the query string) but every subsequent 2s poll had no auth attached and 401'd forever, leaving the dashboard stuck on an empty/error state despite loading successfully - Fixed
PanopticConfig.cachefailures (hydrate-on-startup or write-through — bad URL, unreachable Redis, auth failure) are no longer swallowed into total silence: a newPanopticConfig.onErrorhook fires on every failure, defaulting tolog.error("ai-panoptic", "cacheStore", error)when not supplied, so a misconfigured cache driver now surfaces in logs instead of leaving the dashboard permanently empty with zero diagnostic
- Added
judgePromptBody/formatCriteria/JudgeOutcome— the LLM-as-judge building blocksai.prompts().validate()already used internally are now public, so other packages (@warlock.js/ai-panoptic's trace-level system-prompt evaluation) can grade arbitrary prompt text against a model + rubric without a second judging implementation - Fixed
redact()no longer collapses a rawError(or anErrornested in acausechain) to{}—name/message/stackaren't own-enumerable onErrorinstances, so the previousObject.entries()walk saw none of them. This was silently dropping tool/agent errorcausedetail whereverredact()runs it, including@warlock.js/ai-panoptic's tracecausefield (a failed tool'sToolExecutionError.causeshowed as an empty object in the dashboard instead of the underlying thrown error) - Fixed
@warlock.js/ai-openaiandpdf-parsedeclared as optionalpeerDependencies— both are lazilyimport()ed (the skills-catalog embedder probe;ai.rag.loadPdf) but weren't listed in eitherdependenciesorpeerDependencies, so pkgist's bundler vendored their source directly intoai's own build instead of leaving them external (the same split-brain class of bug ascore's missing@warlock.js/aipeerDependency, fixed in 4.8.1). For@warlock.js/ai-openaispecifically this meant the skills-catalog embedder-installed probe always resolved against the vendored copy bundled intoai, so it reported an embedder provider as "installed" even when the app never installed@warlock.js/ai-openaiitself
4.8.1 July 21, 2026
Fixes a split-brain bug where @warlock.js/core's bundler vendored its own disconnected copy of @warlock.js/ai (and the same gap for @warlock.js/access / @warlock.js/notifications) because they weren't declared as peer dependencies — the vendored copy's config never reached listeners (e.g. ai-panoptic's dashboard) registered against the real installed package. Also logs previously-swallowed onConfigApplied listener errors in @warlock.js/ai.
- Fixed
@warlock.js/ai,@warlock.js/access, and@warlock.js/notificationsdeclared as optionalpeerDependencies(matching the existing@warlock.js/heraldpattern) so pkgist's bundler leaves them external instead of vendoring their source into core's own build — a vendored@warlock.js/aicopy was a disconnected module instance whose config listeners (e.g.ai-panoptic's dashboard wiring) never receivedai.config(...)calls routed through the real, separately-installed package
- Fixed
setAIConfig'sonConfigAppliedlistener notification no longer swallows a misbehaving listener's exception silently — it's now logged vialog.error("ai", "configListener", error)
4.8.0 July 19, 2026
A new reasoning: { effort: "none" } level in @warlock.js/ai runs a reasoning model without reasoning, explicitly — @warlock.js/ai-openai emits reasoning_effort: "none" so OpenAI gpt-5 / o-series models accept function tools instead of returning empty replies, and the budget-based adapters (@warlock.js/ai-anthropic, @warlock.js/ai-google, @warlock.js/ai-ollama) map it to reasoning-off.
- Added
reasoning: { effort: "none" }now emitsreasoning_effort: "none"on the wire — unblocks function tools on gpt-5 / o-series reasoning models, which otherwise reject tools on Chat Completions while reasoning is active and return empty replies.
- Added
reasoning: { effort: "none" }— a neutral "run without reasoning, explicitly" level onReasoningEffort; OpenAI emitsreasoning_effort: "none"so gpt-5 / o-series accept function tools, and budget-based adapters (Anthropic / Bedrock / Google / Ollama) disable thinking.
- Changed
reasoning: { effort: "none" }disables extended thinking (emits nothinkingblock) — the neutral "run without reasoning" level, consistent across adapters.
- Changed
reasoning: { effort: "none" }maps tothinkingBudget: 0— Gemini's native reasoning-off switch, the neutral "run without reasoning" level.
- Changed
reasoning: { effort: "none" }maps tothink: false— the neutral "run without reasoning" level, consistent across adapters.
4.7.0 July 6, 2026
@warlock.js/ai gains the prompt compiler — systemPrompt().refined({ model, criteria, store }) lazily rewrites human-authored prompt text into a model-optimized version, pinned like a lockfile with machine-enforced placeholder parity and an always-safe fallback to the original — plus ai.prompts.validate({ criteria }) to grade a prompt against your own rules. @warlock.js/fs gains an ergonomic async fs facade — fs.files.* / fs.dirs.* grouping, lazy File / Directory handles, read-modify-write helpers (edit / editJson / mergeJson), recursive walk, and zero-dependency schema-validated JSON. @warlock.js/cascade lands a driver-correctness pass — a dozen real Postgres/MongoDB driver fixes (multi-row findAndUpdate, where-scoped update / unset / deleteOne, pivot detach, MongoDB with() eager-loading, per-subclass global scopes) plus new lockForUpdate({ skipLocked }) row locking (FOR UPDATE SKIP LOCKED).
- Added
systemPrompt().refined({ model, criteria, store })— the prompt compiler. Humans keep writing human prompt text; the refined wrapper lazily rewrites it into a model-optimized version on first agent use and pins the result like a lockfile (re-compiled only when the source text, refiner model,criteria, or recipe version change — never silently).await refined.refine()returns the compiled template string (placeholders intact — routes / previews / warmup / CI; throwsPromptRefinementErroron failure) andawait refined.refinePrompt()returns a composable prompt withmeta.refinedFrom/meta.refinerModelprovenance (register it todifforiginal vs refined). Placeholder parity is machine-enforced (one repair re-ask, then rejected); the lazy agent path never throws — it warns once and serves the original. - Added
ai.prompts.validate({ criteria })— validate a prompt against your own rules. Passcriteria(a string or a list of short rules) and, when ajudgemodel is supplied, it replaces the built-in quality rubric so the judge'sscore/issuesreflect your criteria (a failed rule is named inissues). Advisory only — never flips the deterministicok; folded into thejudgeCachekey so different rules re-run.
- Added
fsshorthand facade — an async, ergonomic surface over the primitives:fs.files.*(file ops),fs.dirs.*(directory ops), lazyfs.file(path)/fs.dir(path)handles (File/Directoryclasses), andfs.exists(path)(type-agnostic). Delegates to the existing*Asyncprimitives; synchronous callers keep using the bare primitives (thebare = sync/*Async = asynccharter is unchanged) - Added New file ops on the facade:
append/prepend/appendLine/appendJsonLine(NDJSON),size,isEmpty,ensure(create-if-missing, never truncates),touch,edit(read → transform → write),editJson,mergeJson(shallow, or{ deep: true }),ensureJson(get-or-create),checksumMatches,readLines(streaming async iterator), and an EXDEV-safemove(creates the destination parent, falls back to copy+unlink across devices) - Added New directory ops on the facade:
empty(emptyDir),size(recursive byte sum),count,isEmpty,walk(constant-memory async iterator of{ path, name, type }), arecursiveoption onlist/listFiles/listDirs, andhash(stable directory fingerprint) - Added
fs.files.getJson(path, { schema })— validate parsed JSON against any Standard Schema validator (seal / zod / valibot) with zero dependency (calls the schema's own~standard.validate); throwsJsonSchemaValidationErroron failure.{ default }returns a fallback when the file is missing - Added
File/Directoryhandles are lazy (no IO in the constructor) and immutable (copy/move/rename/copyTo/moveToreturn a NEW handle); pure-path helpersname/basename/extension/parent()and childfile(...)/dir(...);Directory.listFiles()/listDirs()returnFile[]/Directory[] - Added
fs.hashnamespace —fs.hash.string/fs.hash.buffer(sync, pure/in-memory) andfs.hash.file/fs.hash.dir(async, read from disk) - Added
fs.files.get()is overloaded: a text read returnsstring(no cast); pass{ encoding: null }for aBuffer
- Added
lockForUpdate({ skipLocked?, noWait? })— row locking on SELECT (FOR UPDATE [SKIP LOCKED | NOWAIT]), the concurrent job-queue claim shape; Postgres-only, the MongoDB driver throws - Added
DatabaseDriverContract.supportsSqlSerialization— capability flag (defaulttrue);falseroutes the MigrationRunner through direct migration-driver execution - Fixed Postgres model-level
sum/avg/min/max/distinct/countDistinct/pluck/valueno longer return0/undefined— the hydration callback is reset before reading, matching MongoDB - Fixed Postgres
Model.findAndUpdate/Model.atomicnow update every matching row instead of one arbitrary row (a hiddenLIMIT 1; MongoDB was already multi-row) - Fixed Postgres query-builder
update()/unset()now honor the chainedwherefilter — previously they updated the whole table - Fixed Postgres query-builder
deleteOne()deletes exactly one row — the internallimit(1)was silently ignored, deleting every matching row - Fixed Postgres pivot
detach(ids)(andsync/toggle) works — the driver translates Mongo-style filter operators ($in,$nin,$eq,$ne,$gt,$gte,$lt,$lte) instead of binding the operator object literally - Fixed CHECK constraints are no longer silently dropped on the MigrationRunner SQL path — the Postgres serializer emits
ADD CONSTRAINT ... CHECKforthis.check(...)and column.check(...) - Fixed MongoDB
with()eager loading is no longer a silent no-op —get()runs the relation loader, same wiring as Postgres - Fixed MongoDB pipelines order
$matchbefore$project(SQL semantics), soselect()beforewhere()no longer strips the filter column and returns[]— fixes pivotattachde-duplication andsync/toggledeltas - Fixed The MigrationRunner works on MongoDB — migrations execute directly through the migration driver;
exportSQLstays SQL-only with a clear unsupported error - Fixed MongoDB
dropIndex(table, name)honors the literal index name — the string form is no longer rewritten to<name>_1(the columns-array form keeps the convention name) - Fixed
addGlobalScope/addLocalScoperegister per-subclass — a scope added on one model (e.g. a soft-deletenotDeleted) no longer leaks onto every other model
- Added Non-interactive scaffolding —
create-warlock <name> --yes(with--db,--pm,--features,--ai,--git,--jwt) scaffolds the entire app in a single command, no prompts - Added
--db=none/--no-dband a None option in the database prompt — scaffold with no database: the driver, its package, andsrc/config/database.tsare all skipped - Changed Starter models drop the baked-in
globalColumnsSchemaaudit columns (createdBy/updatedBy/deletedBy/isActive) — global columns are left to the developer
4.6.1 July 1, 2026
A production-hardening patch across @warlock.js/core, @warlock.js/logger, and @warlock.js/cascade — a fatal boot error now fails loudly instead of exiting 0, native Postgres array columns (TEXT[] / JSONB[]) work with no configuration, and a nested transaction() joins the active transaction instead of opening an isolated one that can't see its writes.
- Fixed Native Postgres array columns (
TEXT[]/JSONB[], fromarrayText()/arrayJson()) are now auto-detected by introspecting the schema on connect and bound as raw arrays — no more "malformed array literal" on insert and no need to hand-listnativeArrayColumns(which stays as an optional per-connection override, now consulted per-table) - Fixed
transaction()now flat-nests: a nestedtransaction()joins the active one (same session, sees its uncommitted writes) instead of opening a second, independent transaction — fixes phantom foreign-key violations when a service that opens its own transaction is called inside an outer one (e.g. a seeder creating a row, then a service inserting a child that references it). MongoDB joins too, replacing its "nested not supported" throw
- Fixed a fatal
uncaughtExceptionat production boot (e.g. a config file that throws) is no longer swallowed into a silentexit 0— bootstrap now wires the crash handler to exit non-zero in production sowarlock startsurfaces the failure; the dev server still logs-and-continues for HMR
- Changed
captureAnyUnhandledRejection()now exits the process non-zero after anuncaughtException(and prints the stack toconsole.errorwhen no terminal channel is configured) so a fatal error at boot is never silently swallowed into a cleanexit 0— opt out with{ exitOnUncaughtException: false }where the process recovers on its own (e.g. a dev server using HMR).unhandledRejectionis unchanged (logged aterror, never exits).
4.6.0 July 1, 2026
The AI framework closes every remaining capability gap. Output modalities land — ai.image(), ai.speech(), and ai.transcribe(), plus the new @warlock.js/ai-live package for ai.realtime() duplex voice + ai.video(). RAG gains first-party vector stores and document loaders (ai.rag.pgVectorStore on Postgres/pgvector, ai.rag.loadWeb / loadPdf / loadHtml / loadText). Durable mid-run crash-resume comes to agents and planners (agent.resume() / planner.resume() via an opt-in durable store). And provider breadth doubles with four new OpenAI-compatible adapters — @warlock.js/ai-mistral, ai-groq, ai-deepseek, and ai-xai.
- Added
ai.image(params)— image generation, the first verb of the output-modality track (Theme I). Wraps anImageModelContractin the uniform never-throws{ data, error, usage, report }envelope, with cost-truth (per-token forgpt-image, per-image for DALL·E / Imagen) folded into the sameUsage.costrollup and atype: "image"report routed to observers. Ships on the OpenAI + Google adapters. - Added
SDKAdapterContract.image?(config)— the image-model capability seam, mirroringembedder?(). AddsImageModelContract,GeneratedImage(discriminatedbase64|url),ImageModelPricing, andImageGenerationOptions. - Added
MockSDK().image(...)+MockImageModel— deterministic image doubles (scriptable responses, recorded calls, pricing) for tests. - Added
ai.speech(params)+ai.transcribe(params)— text-to-speech and speech-to-text, the audio verbs of the modality track. Same uniform never-throws envelope + cost-truth (per-character / per-minute / per-token). NewSpeechModelContract/TranscriptionModelContractonSDKAdapterContract.speech?()/transcribe?(), plusMockSpeechModel/MockTranscriptionModel. - Added
ai.audioFromFile(path)/ai.audioFromBuffer(bytes, mediaType)/ai.audioMediaTypeForFilename(name)— non-AI utilities that package audio (WhatsApp.ogg/.opus, iOS.m4a, …) into theAudioInputshapeai.transcribeconsumes. - Added
ai.rag.pgVectorStore({ client })— a Postgres + pgvector vector store satisfyingVectorStoreContract(upsert / query / removeNamespace), with anensureSchema()DDL helper and a lazypgoptional peer. - Added
ai.rag.loadText/loadHtml/loadWeb/loadPdf— document loaders producingRagDocuments for.index().loadWebis SSRF-safe (routes throughguardedFetch/OutboundPolicy);loadPdfuses a lazypdf-parseoptional peer. - Added Durable mid-run crash-resume — opt-in
durable: { store, deleteOnComplete? }onai.agent/ai.plannerwith a stablerunId+agent.resume(runId)/planner.resume(runId). Per-trip (agent) / per-node (planner) checkpoints reuseai.snapshot.{memory,pg,redis}; drift detection viaAgentDriftError/PlannerDriftError(bypass with{ force: true }); completed work never re-runs its tools and usage is never double-counted. - Added **
ai.rag.*namespace** now also carrieschunk,cacheVectorStore,pgVectorStore,loadText/loadHtml/loadWeb/loadPdf,bm25Rank,reciprocalRankFusion,hybridRank,multiQuery(previously standalone-only exports), forai.*-namespace consistency.
- Added First release — the live & generative rich-media add-on for
@warlock.js/ai, kept in its own package so core text/image/speech stay dependency-light. A side-effect import (import "@warlock.js/ai-live") mountsai.video+ai.realtimeonto the sharedAifacade. - Added
ai.video(params)— text-to-video (Sora / Veo / Kling-class); the provider's async submit→poll job hidden behind the uniform never-throws{ data, error, usage, report }envelope, with per-second cost-truth folded intoUsage.costand atype: "video"report routed to observers. - Added
ai.realtime(options)— a stateful duplex voice session over a pluggableRealtimeTransport:sendAudio/sendText/events()out,close()→RealtimeReportfor the cost/observability surfaces. - Added Contracts —
VideoModelContract,GeneratedVideo,VideoModelPricing,VideoOptions;RealtimeSession,RealtimeTransport,RealtimeConnection,RealtimeEvent,RealtimeReport,RealtimeOptions. - Added Mocks —
MockVideoModel+MockRealtimeTransportfor deterministic, HTTP- and socket-free tests (scripted responses / event streams, recorded calls).
- Added
openai.image({ name })— image generation for thegpt-image-*(token-metered) anddall-e-*(per-image) families, for use withai.image(). A non-image model id is rejected at construction. - Added PDF + audio input.
pdfandaudiocontent parts now map to OpenAIfile(base64file_data) andinput_audio(wav/mp3) parts — opt in withmodel({ pdf: true })/{ audio: true }. A remote-URL pdf/audio source raises a typedInvalidRequestErrorup front. - Added
openai.speech({ name })— text-to-speech for thetts-1/tts-1-hd/gpt-4o-mini-ttsfamilies (audio.speech.create), for use withai.speech(). - Added
openai.transcribe({ name })— speech-to-text for thewhisper-1/gpt-4o-transcribefamilies (audio.transcriptions.create), for use withai.transcribe().whisper-1defaults toverbose_json(duration + segments); a non-TTS/STT model id is rejected at construction. - Fixed Non-text content parts are no longer coerced to
image_url. The message mapper now branches per modality (image →image_url, pdf →file, audio →input_audio) instead of forcing every attachment through the image path.
- Added First release.
MistralSDK— a thin wrapper over@warlock.js/ai-openaithat points one internalOpenAISDKat Mistral's OpenAI-compatible endpoint (https://api.mistral.ai/v1) withprovider: "mistral", delegating transport, streaming, tool calls, structured output, error wrapping, and token accounting to the battle-tested adapter. Exposes.model(),.embedder()(mistral-embed), and.count(). - Added Mistral-aware capability inference —
visionis auto-set for thepixtralfamily and recent multimodal generations (mistral-large,mistral-medium,ministral-3);reasoningfor themagistralfamily and the hybridmistral-smallgeneration. An explicitvision/reasoningon.model()always wins. Exported asinferVisionCapability/inferReasoningCapability, with the-latestaliases grouped underMISTRAL_MODELS. - Added Default pricing registry (
MISTRAL_DEFAULT_PRICING, USD per 1,000,000 tokens) merged under any caller-suppliedpricingso cost truth works out of the box; per-model > SDK-level > default >undefined. Noimage()— Mistral has no OpenAI-compatible image endpoint.
- Added
google.image({ name })— Imagen (imagen-*) image generation for use withai.image(). Per-image-metered; when every candidate is safety-filtered the run surfaces a typedContentFilterError. A non-Imagen model id is rejected at construction. - Fixed PDF + audio input are now explicitly mapped and tested. The content-part mapper documents and proves that
pdf/audioparts route to GeminiinlineData(thepdf/audiocapabilities the adapter advertises are backed by a real mapper, not an accident of the image path), and the remote-URL rejection now names the actual modality instead of always saying "images".
- Added release-hygiene tests: version↔changelog invariant + generator-stub import check
- Added
router.routeCount()exposes the number of registered routes as a boot/readiness signal - Added
health.addRoutesRegisteredCheck(getRouteCount)registers a readiness check that reports not-ready when a booted HTTP app has zero routes - Added seeders now receive a
{ track }context —track(model),track(models[]), andtrack(table, id)register created records (each call returns its argument so it can be chained inline);recordsCreatedis auto-derived from the track count - Added
seed_recordstable (created via the newSeedRecordsTableMigration) records every tracked seed reference within the same transaction the seed runs in; only the last run's refs are kept per seeder - Added
warlock seed --drop [name]undoes a seed: deletes its tracked records in reverse run/insertion order inside a transaction, then resets the matching seeds-log rows soonce: trueseeds re-run; scope to one seeder with--drop=<name> - Added
Seeder.dependsOnis now resolved — seeders are topologically sorted so dependencies run before dependents, layered over the numericordertie-break; throwsUnknownSeederDependencyErrorfor a missing dependency andSeederDependencyCycleErrorfor a cycle - Added seeders receive an injectable clock and a meaningful batch size —
run({ track, now, batchSize });now()(default() => new Date()) drives both seed data and the seeds-log timestamps so historical/back-fill runs are deterministic, andbatchSizesurfaces the seeder's ownbatchSizeforModel.createMany(rows, { batchSize }) - Added repository-level aggregation —
aggregate(),sum(),avg(),min(),max(), andgroupBy()onRepositoryManager, each reusingfilterBy(and its operator-injection guard),where, and scopes before the aggregate, exactly likecount() - Added
warlock doctor— a read-only diagnostics command that runs routes / config / connectors / optional-peers / health / release-hygiene checks and prints a pass/warn/fail report (exits non-zero on any failure, never opens a DB/cache/socket connection) - Added
warlock routes— a read-only command that lists the registered HTTP routes as a verb-colored table (method / path / name / action / middleware-count / source); filter with--method/--path/--name, or emit the normalized rows as JSON with--json. Boots app code to register routes but starts no connectors - Changed
Seeder.runnow receives aSeedContext(run(ctx)) — backward compatible, an existing zero-argrun()keeps working unchanged - Fixed route-module load/registration failures are no longer swallowed: a route file that throws on import or registration now surfaces loudly instead of silently 404'ing the whole surface
- Fixed
ModuleLoader.loadModulerethrows after logging (wrapped in a newModuleLoadErrorcarrying the failing file + cause), so a broken module aborts boot and is caught loudly by the HMR batch-reload handler in dev - Fixed
ModuleLoader.loadAllaggregates per-file failures and throws anAggregateErrorat the end, so one broken module no longer hides the others - Fixed
router.withSourceFilerethrows the callback error after logging instead of consuming it with a bareconsole.log(thetry/finallysource-file stack cleanup is preserved)
- Added Fast bulk
Model.createMany(data, options?: { batchSize?; bulk? })— both paths chunk bybatchSize(default 500);bulk: trueroutes each chunk to the driver's native multi-rowinsertManyfor 10–100× throughput (skips per-row hooks/events; default path preserves them) - Added
IdGeneratorContract.generateNextIds({ table, count })— reserve a contiguous block of auto-increment ids in a SINGLE atomic op (MongoDB).Model.createMany(default + bulk) now reserves one id block per chunk instead of one counter round-trip per row; engages only for fixed-increment, auto-generated, id-less rows (random-increment or caller-supplied-id rows fall back to per-row generation) - Added
QueryBuilder.groupByDate(column, unit, aggregates?)— portable date-bucketedGROUP BY(day/week/month/year) across Postgresdate_truncand MongoDB$dateTrunc - Added
$agg.sum(expr)now also accepts a typed column expression ($expr.mul/$expr.add/$expr.sub/$expr.div/$expr.col/$expr.lit) so you can sumprice * quantity; bare-string payload is unchanged. Added$agg.sumRaw(expression)raw escape hatch (PostgresSUM(<raw>); throws on MongoDB) - Added Column-expression DSL grouped under a single
$exprobject (mirroring$agg) —$expr.col/$expr.lit/$expr.mul/$expr.add/$expr.sub/$expr.div/$expr.raw— plusisColumnExpression/toColumnExpressionand theColumnExpression/ColumnExpressionInputtypes - Added MongoDB id counter (
MasterMind) now has a lazily-ensured unique index on{ collection: 1 }plus a bounded retry on duplicate-key (E11000), closing the cold-start race where two concurrent first inserts into a new collection could reserve overlapping ids/blocks - Added
$agg.countDistinct(field)— a cross-driver grouped distinct-count aggregate (PostgresCOUNT(DISTINCT col); MongoDB$addToSetin$groupfinalized with$sizein the renaming$project) - Added
Model.raw<T>(sql, params)— typed, transaction-aware raw query that auto-joins the activetransaction()scope and returnsRawQueryResult<T> - Added
DataSource.raw<T>(sql, params)— thin transaction-aware passthrough todriver.query - Added Postgres connection option
nativeArrayColumns— opt out listed columns (JSONB[]/TEXT[]/…) from JSON-text encoding so genuine native-array columns keep their{...}literal form - Changed
DriverContract.query<T>()is now typedPromise<RawQueryResult<T>>(newrows+rowCountresult type) instead ofPromise<any> - Fixed Postgres
json/jsonbcolumns no longer corrupt: object-arrays, string-arrays, mixed arrays, empty[](previously stored as{}), and plain objects are now JSON-encoded before binding instead of falling through to a Postgres array literal; the same encoding is applied on the UPDATE$setpath. The pgvector all-number array form is preserved. - Fixed Insert no longer overwrites a caller-supplied
createdAt— a backdated value (imports/migrations) is now honored, mirroring the upsert guard, whileupdatedAtis always stamped at persist time - Fixed Insert validation now whitelists the system columns (
id/_id/timestamps/deletedAt) like the update path, so a backdatedcreatedAtsurvives strictstrip/failmode instead of being dropped before reaching the writer - Fixed Corrected the MongoDB id-generator docs that falsely claimed the counter write "participates in active transactions" — it is a standalone, immediately-durable write (no transaction session is attached), so a rolled-back insert leaves the consumed id as a gap, exactly like SQL
SERIAL
- Added First release. DeepSeek adapter for
@warlock.js/ai— a thin wrapper over@warlock.js/ai-openai'sOpenAISDKpinned tohttps://api.deepseek.com(provider: "deepseek"), so all wire behavior (streaming, tool calls, structured output, error wrapping) is inherited unchanged. - Added
DeepSeekSDK—.model()/.embedder()/.image()/.count()delegated to the wrapped client.baseURLandproviderare optional (default to DeepSeek's endpoint / label); every otheropenaiClientOptionsvalue is forwarded verbatim. - Added DeepSeek-specific capability inference (
inferReasoningCapability/inferVisionCapability) —reasoningis auto-truefordeepseek-reasonerand the*-protier, auto-falsefordeepseek-chat/*-flash;visionisfalsefor every id (no documented vision surface). An explicitreasoning/visionper model always wins. - Added Built-in DeepSeek pricing defaults (USD per 1M tokens) for
deepseek-chat,deepseek-reasoner,deepseek-v4-flash,deepseek-v4-pro, sousage.costis computed out of the box; overridable per model or per SDK. - Added
DEEPSEEK_CHAT_MODELS— informational list of the documented chat model ids.
- Added First release. xAI Grok adapter for
@warlock.js/ai— a thin wrapper over@warlock.js/ai-openai'sOpenAISDKpinned tohttps://api.x.ai/v1(provider: "xai"), so all wire behavior (streaming, tool calls, structured output, error wrapping) is inherited unchanged. - Added
XaiSDK—.model()/.embedder()/.image()/.count()delegated to the wrapped client.baseURLandproviderare optional (default to xAI's endpoint / label); every otheropenaiClientOptionsvalue is forwarded verbatim. - Added xAI-specific capability inference (
inferVisionCapability/inferReasoningCapability, exported alongsideXAI_VISION_MODEL_PREFIXES/XAI_REASONING_MODEL_PREFIXES) — Grok ids don't match OpenAI'sgpt-*/o*prefixes, sovisionis auto-trueforgrok-4/grok-2-visionandreasoningforgrok-4/grok-3-mini. An explicitvision/reasoningper model always wins. - Added
XAI_CHAT_MODELS— convenience list of current public Grok chat ids (grok-4,grok-3,grok-3-mini,grok-2-vision,grok-2). - Added Optional per-model pricing registry — resolution at
model()time is per-modelpricing> SDK registry >undefined.
- Added First release.
GroqSDK— a thin wrapper over@warlock.js/ai-openaithat points one internalOpenAISDKat Groq's OpenAI-compatible endpoint (https://api.groq.com/openai/v1) withprovider: "groq", delegating transport, streaming, structured output, error wrapping, and token accounting to the battle-tested adapter. Serves Groq-hosted open models (llama-3.3-70b-versatile,llama-3.1-8b-instant,openai/gpt-oss-*,deepseek-r1-distill-llama-70b) on LPU hardware via.model()and.count().GROQ_BASE_URL/GROQ_PROVIDER/GROQ_KNOWN_MODELSexported. - Added Groq-aware capability inference — because Groq ids are upstream open-weight names, not OpenAI's, the wrapper carries its own lists:
visionis auto-set forgpt-oss/llama-4/llama-3.2-*-vision;reasoningforgpt-oss/deepseek-r1/qwq/qwen3. An explicitvision/reasoning/structuredOutputalways wins. Exported asinferVisionCapability/inferReasoningCapability. - Added Default pricing registry (USD per 1,000,000 tokens) for the known Groq models as the final fallback; resolution per-model > SDK-level
pricing[name]> built-in default >undefined. No embeddings endpoint on Groq (.embedder()is delegated for symmetry but calls fail upstream) and noimage().
- Fixed Warn in development when jobs are registered but
start()is never called — a one-shot deferred check logsN job(s) registered but scheduler.start() was never called, is suppressed oncestart()runs or in production (NODE_ENV=production), and is unref'd so it never holds the process open.
4.5.0 July 1, 2026
The biggest AI release yet — the agent-platform, prompt-unification, and team-identity milestones plus a security-hardening pass, consolidated into one release. @warlock.js/ai grows from a primitive ladder into a full agent platform: ai.rag(), ai.team(), ai.skills(), a unified ai.prompts registry (ai.prompt is now a facade over it), ai.dataset() / ai.vcr(), a planner that runs DAGs and can pause for approval, a generic observe seam, the former ai-human + ai-guard satellites folded into core, and a security pass (SSRF-safe outbound I/O, redaction, attachment policy, SSE serving, streaming structured output). @warlock.js/ai-panoptic becomes batteries-included with a redesigned, auth-gated local dashboard (group-by-type, cost heatmap, timeline, search / filter, restart-persistent store) and first-class team-type traces. The first releases of @warlock.js/ai-tools (ready-made tools + MCP) and @warlock.js/ai-workspace (a policy-jailed coding workspace), plus a broad @warlock.js/core hardening pass.
- Added
ai.rag(config)— retrieval-augmented generation in core: a chunk → embed → retrieve → cite pipeline that reuses your existing embedder and cache, with zero new dependencies. Includes hybrid retrieval (dense + BM25 reciprocal-rank fusion), keyword / LLM rerankers, and multi-query expansion. - Added
ai.team(config)— manager-led multi-agent teams: thin sugar overai.supervisorfor the review-then-fix and test-then-fix shapes. - Added
ai.skills(config)— runtime agent skills with progressive disclosure: a cheap always-injected catalog plus an on-demandloadSkilltool. Adds askillsoption onai.agent. - Added
ai.streamObject(...)— structured-output streaming: partial-object snapshots as tokens arrive, with a strict final parse against the response schema. - Added
ai.serve(executable, options)— serve any agent / workflow / supervisor as an SSE HTTP endpoint. - Added Multimodal attachments —
ContentPartgainspdfandaudiovariants alongside text / image, resolved to provider-ready parts (PDF wired on the Anthropic and Bedrock adapters). - Added Planner DAG execution, re-planning, and plan-only approval — run independent steps concurrently, revise the plan when a step fails, or return a plan for approval before it executes.
- Added Generic
Observerseam — route any flow's run report to pluggable observers (e.g.@warlock.js/ai-panoptic) without coupling core to a backend. - Added
ai.prompts+ai.prompt— a process-wide registry of named, versionedsystemPrompt(...)builders (resolved byname@version/name@tag) withdefine/tag/diff/export/importand a unifiedvalidate(deterministic missing-placeholder check plus an optional Nova-safe LLM-judge);ai.promptis a thin facade over it. - Added
SystemPromptContractidentity + provenance —.meta({ name, version, description, required })(a name auto-registers inai.prompts),.merge(...blocks)/.merge(contract)/.merge(name, { fromVersion }), and deterministicmeta.composedFromlabels. - Added
ai.dataset(options)— filterable, shardable evaluation case sets that feedagent.eval, with baseline / regression detection and CI reporters. - Added
ai.vcr(model, options)— record / replay any model against an on-disk cassette for deterministic, offline tests, withrecordRequestmodes andredactRequest/redactResponse/redactErrorhooks. - Added
ai.agent.judge(config)— judge-safe agent preset (alsoai.agent({ judge: true })): lenient JSON parsing, bounded repair re-asks, and never-throw verdicts on Nova-class models. - Added Human-in-the-loop approval now ships in core (
ai.human.*, formerly@warlock.js/ai-human) — a tool-approval gate plus durable interrupt / resume. - Added Content guardrails now ship in core (
ai.guardrail.*, formerly@warlock.js/ai-guard) — PII / topic / injection / moderation detectors. - Added Orchestrator
sessionLock— per-session turn serialization (default in-process mutex keyed bysessionId, pluggable distributed lock) so concurrent same-session turns can't lose a checkpoint update. - Added Sub-agent trace nesting — a supervisor / team / orchestrator callback that calls
agent.execute()directly now nestscallback → agent → toolwith rolled-up usage / cost. - Added
AgentReport.systemPrompt— the resolved system prompt sent to the model is now recorded on the agent report. - Changed
ai.teamruns reporttype: "team"— a first-classReportType(was"supervisor") so observers distinguish team runs on the wire. - Changed Deterministic parallel workflow state merge — parallel children merge into the parent in declaration order (last-declared wins on a conflicting key) instead of completion order; an optional per-step
mergeStatereducer overrides it. - Changed Safer batch / RAG defaults —
ai.batchwarns once on a large unbounded run (pass an explicitconcurrencyor"unbounded");ai.ragacceptslimits(maxDocuments/maxChunks/maxBytes) that fail before any embedding spend. - Fixed Cancellation propagates through composite tools — a cancelled outer agent now aborts a nested agent / workflow / supervisor invoked via
.asTool()(the run signal threads into the nestedexecute). - Fixed Observer / event-handler errors are surfaced, not swallowed — a throwing observer or
onhandler stays isolated (never crashes the run) but is now warned once / routed to a hook instead of disappearing silently. - Fixed
budget({ maxCostUSD })fail-open closed — a cost cap with no matching model pricing now warns once (naming the model) instead of silently never tripping. - Security Shared
OutboundPolicy+redact()— one SSRF-safe outbound-fetch guard (scheme + host allowlist, post-DNS private-IP deny, max-bytes, timeout, injectable fetch) and one redaction utility, consumed across attachments, URL skills, VCR, and the error path. - Security Attachment trust boundary (
AttachmentPolicy) — remote-text attachment fetch is default-deny (opt in with a policy), local reads honor anallowedRootssandbox, and bare-string local paths warn (staged deprecation). - Security URL skill sources hardened — the manifest fetch runs through
OutboundPolicyand every record is runtime-validated before it enters model context; adds cache-TTL controls. - Security Guardrail coverage documented — input detectors inspect text only; non-text attachment content needs an attachment-level policy.
- Added Zero-setup local dashboard —
dashboard(store, options)serves a loopback-only mini-Langfuse (default127.0.0.1:4319) with no Docker and no account: light / dark / system theme, a two-pane drawer (nested call tree / detail), a metadata panel, colour-coded Title-case type labels, arrow-coded token rollups (↓input · ↑output · total), per-node rollup cost, and the Warlock logo. - Added Dashboard search / filter / grouping — client-side free-text search, status / type / session / prompt filter chips, an errors-only toggle, and group-by Session / Prompt / Type with a per-type aggregate-stats panel (count, failure rate, p50 / p95 latency, tokens, cost).
- Added Cost heatmap, timeline view, and deep-links — each node carries a cost-tinted accent; the drawer toggles between the call tree and a Gantt timeline (critical path highlighted); the open trace + span are reflected in the URL hash for shareable views.
- Added Prompt-version linkage — agent spans stamp
agent.promptName/agent.promptVersion; the dashboard filters and groups by the resolvedname@versionkey. - Added Cache-backed persistent trace store —
createCacheTraceStore(cache, options)persists traces through any@warlock.js/cachedriver, serves reads from an in-memory mirror, and re-hydrates onready()so traces survive a restart;ai.config({ panoptic: { cache } })wires it. - Added Declarative
ai.config({ panoptic })— configure panoptic once; it registers on core's observer registry and starts the dashboard. Per-flowobserve+observeAllopt flows into observation. - Added
ContentCaptureOptions.fullHistory— capture the agent's complete message history on the span; agent content is emitted as a[system, user]chat array; Langfuse gets trace-levelinput/output. - Added
onErrorhook — handle isolated exporter failures onpanoptic()/createCollector(). - Added Pure trace-list helpers exported from the package root (
filterTraces/groupBySession/groupByPrompt/groupByType/aggregateByType/rollupCost/ …) so your own views mirror the dashboard's rules. - Changed Title-case status & type labels across rows, drawer, chips, and group headers (underlying filter keys stay lowercase);
teamis a first-class dashboard type; type chips show only present types; drawer metadata keys are humanized; the group-by toggles became a singleGroupdropdown. - Fixed Isolated exporter failures no longer fail silently — a failing exporter still never crashes the run, but now warns once (or calls
onError). - Security Dashboard hardening — bearer-token auth (
authToken, required when binding off-loopback), aHost-header allowlist (DNS-rebinding guard), and security response headers (nosniff/X-Frame-Options: DENY/ locked-down CSP) on every response. - Security Error redaction — captured error
message/stackare scrubbed of secrets (Bearer tokens, API keys) and a retainedcauseis deep-redacted (auth / cookie headers stripped) before a trace is stored or exported.
- Added **Five ready-made agent tools, attached to the shared
aiobject underai.tools.*via adeclare module "@warlock.js/ai"augmentation, so a bareimport "@warlock.js/ai-tools"makes them available and statically typed. Each returns aToolContractthat drops straight intoai.agent({ tools: [...] }): -ai.tools.webSearch(options)(web_search) — web search via a chosen provider (tavily/brave/serpapi) over the globalfetch; the API key falls back toTAVILY_API_KEY/BRAVE_API_KEY/SERPAPI_API_KEY;maxResultsis clamped per call. -ai.tools.fetchUrl(options?)(fetch_url) — fetch a URL and return its content as readability-extractedtext(default), rawhtml, ormarkdown, with a host allowlist (SSRF guardrail), a byte cap (truncatedflag), and a request timeout. -ai.tools.http(options?)(http_request) — a guarded HTTP/REST client: method + host allowlists enforced before the network call, optionalbaseUrljoin, static-header merge, byte cap, and timeout; JSON-parses a JSON response body. -ai.tools.calculator(options?)** (calculator) — a SAFE arithmetic evaluator (+ - * / % ^, unary signs, parentheses, decimal/scientific literals) implemented with a shunting-yard pass — it never callseval/Function. -ai.tools.dateTime(options?)(date_time) — clock/calendar operations:now/add/diff/formatover ISO-8601 instants, with millisecond-based units and IANA time-zone rendering.diffacceptsfromas an alias for the start instant (isowins when both are set). - Added MCP client —
ai.mcp(server, options?)(Direction A). Connects to an external MCP server over a stdio (node:child_process+node:readline, no dependency) or Streamable HTTP transport, runs theinitializehandshake +tools/list, and adapts each remote tool into a nativeToolContract(its JSON Schema wrapped as a Standard Schema,tools/callasexecute). SupportsnamePrefix,filter, and a per-calltimeoutMs; anisErrorresult surfaces as{ error }data. - Added MCP server —
ai.mcp.serve(source, options)(Direction B). Exposes a built agent / supervisor / orchestrator (or a rawToolContract[]) AS an MCP server:tools/listemits each tool'sinputSchemaviaextractJsonSchemaat the configuredschemaTarget(defaultdraft-2020-12), andtools/callroutes tocontract.invoke(), mappingdatato a text content block anderrorto anisError: trueresult. The stdio transport is auto-pumped overprocess.stdin/process.stdout; the pure protocol core is also exported ascreateServeHandlerfor a host's own HTTP wiring. - Added Typed error classes —
WebToolError,HttpPolicyError,CalculatorError,DateTimeError, andMcpTransportError, each extending the@warlock.js/aiAIErrorbase with atypediscriminator. Every tool follows the errors-as-data contract: failures are thrown insideexecute, wrapped bytool(), and reach the model as{ error }so the agent self-corrects instead of crashing the run. - Added Optional peers, lazily imported. Heavy dependencies — a search provider (
@tavily/core), the readability scraper (@mozilla/readability+jsdom), the MCP SDK (@modelcontextprotocol/sdk), and the JSON-Schema validator (ajv) — are optional peers,import()ed only when the relevant path runs, surfacing a curatednpm installstring when absent rather than crashing at import time. The only required runtime peer is@warlock.js/ai; everything else is Node built-ins + the globalfetch(Node 18+).
- Added
ai.workspace(policy)— the workspace verb, registered on the sharedaiobject via adeclare module "@warlock.js/ai"augmentation + a runtime side-effect on import (no edit to@warlock.js/ai's own source). Exported as theworkspacefactory;WorkspaceCapableAiis the typed view consumers castaithrough. - Added Policy jail — every path is
realpath-resolved and must sit undercwd(or anallowPathsroot);denyPathsglobs are blocked even insidecwd; the shell allow/deny list gates each command's leading executable basename (deny wins, fail-closed); per-command timeout + output byte cap; andprocess.envis never inherited wholesale (opt-inshell.inheritEnv, plus explicitshell.env). - Added Seven agent-facing tools under
ws.tools.*, each aToolContractbuilt on the coretool()factory:read_file,edit_file,write_file,run_shell,run_tests,grep,glob. Each factory takes an optional{ name }(andrun_testsa{ command }) override. - Added
tools.all()— every tool in canonical order — andtools.pick(...names)— a least-privilege subset (e.g.pick("readFile", "grep", "glob")for a reviewer). - Added Direct programmatic methods sharing the same policy seam:
readFile/writeFile/editFile/exec/grep/glob/exists/mkdir/remove. - Added
readonly()— a projection that vends only the read/grep/glob tools and rejects every mutating direct method with aWorkspacePolicyError. - Added
scope(subdir)— a sub-jailed workspace rooted atsubdir(narrowedcwd, same sub-policies and backend selection). - Added Read-before-edit guard —
read_file/readFilereturn a SHA-256 contenthash(via@warlock.js/fshashString);edit_filerequires an exact, uniqueoldString(orreplaceAll) and rejects a mismatchedexpectHashas stale. - Added Backends —
createLocalBackend(default"local";@warlock.js/fsfor IO +node:child_processfor the shell) andcreateMockBackend(in-memoryMap+ scriptedexec, for hermetic disk-free tests), behind theWorkspaceBackendcontract. - Added Policy engine seam exports:
resolveInJail,isCommandAllowed,buildEnv, and theResolvedPathtype. - Added Ops layer export
createOps— the single policy-enforced operation layer both the tools and the direct methods funnel through. - Added Typed errors
WorkspacePolicyError(type: "path-escape" | "denied-command") andWorkspaceEditError(type: "not-found" | "not-unique" | "stale-hash"), both extending the@warlock.js/aiAIErrorbase (codeTOOL_EXEC_FAILED) so failures surface to the agent as tool-error data, never thrown run-killers. - Added Public type surface re-exported from the barrel:
Workspace,WorkspaceTools,WorkspacePolicy,WorkspaceShellPolicy,WorkspaceReadPolicy,WorkspaceBackendType,WorkspaceToolName,WorkspaceOps,WorkspaceBackend(+WorkspaceBackendExecOptions/WorkspaceBackendExecResult), and the per-tool IO shapes (ReadFileInput/Result,EditFileInput/Result,WriteFileInput/Result,RunShellInput/Result,RunTestsInput,GrepInput/Match/Result,GlobInput/Result). - Added
scripts/generate-llms.mjsand the generatedllms.txt/llms-full.txtprojections ofskills/. - Added
skills/use-a-workspace/SKILL.md— building a jailed workspace and operating it (policy, the seven tools, the direct methods,readonly/scope). - Added
skills/build-loop-agent/SKILL.md— wiringws.tools.all()into a coding agent that reads → edits → runs tests until green.
- Changed dev-server update notice now fires immediately on
warlock dev— the check is spawned in parallel with server startup instead of awaiting it, so the notice surfaces as soon as npm responds - Changed raised the update check's npm registry timeout from 2.5s to 30s, so a slow connection no longer drops the notice
- Changed repository lifecycle hooks (
onCreating/onCreate/onUpdating/onSaving/onDeleting/ …) now run oncreate/update/delete— they were defined but never invoked - Changed
repository.list()/all()now honor thesortBy,sortDirection, andpurgeCacheoptions — previously accepted but silently ignored - Fixed
response.sendFile({ filename })andresponse.download()no longer 500 on non-ASCII file names — theContent-Dispositionheader is now RFC 6266-encoded (a sanitized ASCIIfilenamefallback plus an RFC 5987filename*=UTF-8''…), so an Arabic / emoji / UTF-8 download name streams correctly instead of throwing Node'sERR_INVALID_CHAR - Fixed local storage paths are contained to their disk root —
../traversal segments and absolute paths can no longer escape the configured directory - Fixed
storage.putFromUrladds SSRF guards — private / loopback / link-local hosts are rejected and the fetched body is size-capped - Fixed S3 / R2 / DigitalOcean Spaces
url()no longer produces a malformed double-host URL whenurlPrefixis set - Fixed cloud
deleteDirectorypaginates via the list continuation cursor instead of re-listing the first page - Fixed local-storage metadata cache is invalidated on write / delete — it was serving a stale size / modified-time
- Fixed the cloud driver no longer reports a misleading "SDK not installed" error when the AWS SDK is in fact present (driver load race)
- Fixed repository
countCached/countActiveCachednow cache and return correctly — anullcache miss was being returned as the count - Fixed repository
firstCached/lastCachedno longer fetch and cache the entire table to return a single row - Fixed repository boolean filters no longer coerce
false/0totrue - Fixed repository cache keys are now order-independent (stable key serialization)
- Fixed router groups restore prefix / name / middleware state via
try/finallyeven when the group callback throws - Fixed
router.any()/allroutes now match every HTTP verb under the dev server — they previously matched only GET and POST, diverging from production - Fixed the HTTP concurrency limiter releases its slot on every response path (
noContent, redirect, file, buffer) — a throwing or non-sendhandler no longer leaks a permit and permanently 429s the route - Fixed the cached-response middleware replays a hit through
response.replay()instead of re-sending an already-sent reply, preserving status and content-type - Fixed
onSentcache writes in the idempotency and cache middleware are error-handled — a cache-backend failure no longer surfaces as an unhandled rejection - Fixed
X-Forwarded-Foris parsed to its first hop, so IP-filter / rate-limit / idempotency scoping cannot be spoofed with extra header hops - Fixed the
maintenancemiddleware allowlist matches request paths that carry a query string - Fixed use-cases run their
aftermiddleware and broadcast for avoidhandler, and a failed history write no longer fails an otherwise-successful call - Fixed the use-case retry counter reports the correct count on total failure
- Fixed the socket connector no longer double-closes the shared HTTP server during graceful shutdown
- Fixed the cache connector disconnects its drivers on shutdown — an open Redis connection was left dangling
- Fixed generator stubs import
v/Inferfrom@warlock.js/seal(core never re-exported them), so generated models compile and run - Fixed
warlock devhot-reloads when a file is emptied or saved with no trailing newline — a stale no-op-change check was silently dropping those saves before they reached HMR - Removed presigned-upload
maxSizeoption — a presigned PUT URL cannot enforce a size cap, so the option was a false guarantee
- Fixed All upstream
ClientOptionsnow reach the Anthropic client. The SDK constructor peels off the framework-onlyprovider/pricingkeys and forwards the rest (timeout,maxRetries,defaultHeaders, customfetch,baseURL, …) verbatim, instead of dropping everything butapiKey/baseURL.
- Fixed All upstream
ClientOptionsnow reach the OpenAI client. The SDK constructor peels off the framework-onlyprovider/pricingkeys and forwards the rest (timeout,maxRetries,defaultHeaders, customfetch,organization,project, …) verbatim, instead of dropping everything butapiKey/baseURL.
4.4.0 June 21, 2026
@warlock.js/core completes the production lifecycle — a booted hook, a shutdown hook, graceful HTTP draining, and built-in /health + /ready endpoints for zero-downtime deploys. Plus AI fixes across @warlock.js/ai, @warlock.js/ai-openai, and @warlock.js/ai-panoptic (opt-in content capture, corrected Langfuse token accounting).
- Added
Application.onceBooted(cb)— run a callback once the app is fully booted (fires immediately if already booted) - Added
Application.whenBooted()— promise that resolves with the boot context when the app is fully booted - Added
Application.isBooted— whether the app has finished booting - Added
Application.onShutdown(cb)— run teardown once on shutdown, before connectors stop (mirror ofonceBooted) - Added
Application.isShuttingDown— whether shutdown has begun - Added built-in
/health(liveness) and/ready(readiness) endpoints with ahealthcheck registry (health.addCheck) - Added graceful HTTP shutdown — drains in-flight requests on shutdown, bounded by
http.gracefulShutdown.timeout - Added
http.health.*config to toggle or rename the health endpoints - Fixed connector shutdown no longer reverses the connector list in place (could corrupt order on a repeated shutdown)
- Fixed Planner: OpenAI strict structured-output
400. The generated plan schema now lists every property inrequiredand dropsminItems/maxItems, soai.planner()no longer fails against OpenAI strictjson_schemamode. - Fixed Report / result types no longer collapse to
neverunder strict TypeScript. The narrowing report / result types now override the discriminant viaOmit<…>instead of intersection. Type-only — no runtime change.
- Added Opt-in content capture.
panoptic({ captureContent, redactContent })copies the agent prompt / response and each tool's args / result onto spans, surfaced by the console (io), file, OTel (gen_ai.prompt/gen_ai.completion), and Langfuse exporters. Off by default; aContentRedactormasks each value. - Fixed Langfuse token accounting — generations now meter their own usage (rolled-up minus children) and the root execution is metered, so the trace total no longer double-counts nested spans.
- Fixed Strict structured-output compatibility check is now recursive. A schema that omits a
requiredproperty anywhere in the tree degrades to loosejson_objectinstead of400-ing; client-side validation still enforces the full shape.
- Changed Documented
model.uuid— the accessor returns the model's primary id asstring(wheremodel.idisstring | number); the name is historical and performs no UUID validation.
4.3.0 June 21, 2026
Self-update tooling — @warlock.js/core gains the warlock update command and a new-release notice in warlock dev. @warlock.js/ai caps the primitive ladder with ai.orchestrator(), ai.planner(), and ai.memory(), plus a cost-truth pass across every provider. The new @warlock.js/ai-panoptic package adds observability. ⚠ Breaking: snapshot persistence moves from a CacheDriver to the dedicated SnapshotStore.
- Added
warlock update— update every@warlock.js/*package in package.json to its latest version (operator preserved), then run the detected package manager's install - Added dev-server update notice —
warlock devchecks npm on start and prints a one-line notice when a newer@warlock.js/coreis published - Added
devServer.checkForUpdatesconfig flag (defaulttrue) to toggle the dev-server update notice - Added
fetchLatestVersion()andisNewerVersion()registry/version utilities - Fixed
warlock dev --skip-typingsand--skip-healthlong-form flags now work (were silently ignored)
- ⚠ BREAKING Supervisor + workflow snapshot persistence moved from
CacheDriverto the dedicatedSnapshotStorecontract. The per-primitive fallback is nowai.config({ defaultSnapshotStore }). Migration: replacesnapshotStore: cache.driver("redis", { client })withsnapshotStore: ai.snapshot.redis({ client })(andai.snapshot.{memory,pg}for the other tiers). - Added
ai.orchestrator()— stateful session manager over a supervisor: durable session / history / context, drift detection, history compaction, resume, and a command surface (orchestrator.asTool(), a 3-tier event surface, andOrchestratorContract/ config / error types). - Added
ai.checkpoint.{memory,pg,redis}()andai.snapshot.{memory,pg,redis}()— durable orchestrator-session and supervisor / workflow run stores, with matchingdefaultCheckpointStore/defaultSnapshotStoreconfig fields. - Added
ai.memory()— agent-memory store with four tiers: working (in-run scratch), semantic (durable facts), episodic (durable, recency-blended events), and procedural (durable, reinforcement-blended how-tos). Wired into the orchestrator via amemory?field. - Added
ai.planner()— an LLM generates an ordered plan over your registered capabilities, then executes it step-by-step. - Added
ai.spawnSubAgent()— one-shot delegation to a fresh single-use agent with an optional per-task budget; usable from a planner step, a tool, or a workflow. - Added Cost-truth contract surface across all five adapters —
Usage.reasoningTokens, per-channelModelPricing, andModelCallOptions.{reasoning, cacheControl}(ignored by adapters that lack the capability). - Added DX helpers —
ai.router(),ai.fanOut(),ai.batch(),ai.fallbackModel(),ai.mockRouter(),agent.eval()+ built-inai.eval.*scorers, Vitest matchers (registerAiMatchers()), supervisor-level middleware, andai.systemPrompt.fromFile(path). - Added Executables passed in an agent's
tools: [...]are auto-adapted into tools (workflows / supervisors / orchestrators compose directly via.asTool()).
- Added
panoptic()— the one-call subscriber factory: builds a collector, registers exporters, and feeds traces viaattach(),middleware(), orcollect(). - Added Exporters —
consoleExporter(),fileExporter()(JSON-Lines),otelExporter()(GenAI semantic conventions), andlangfuseExporter().@opentelemetry/*andlangfuseare optional peers, lazily imported. - Added
createInMemoryTraceStore()— a queryable in-memory trace store (query/aggregateby runId, sessionId, status, time window; optionalcapacityFIFO cap) that doubles as an exporter. - Added Vendor-neutral trace contracts (
Trace/TraceSpan/CollectorContract/ExporterContract) derived 1:1 from the coreBaseReporttree, plus skills for observing, exporting, and querying traces. - Fixed Failed root runs now carry their error in every export (threaded from the result envelope onto the root span).
- Fixed Per-span exporters now receive every span (the collector walks the finalized tree).
- Fixed
panoptic().middleware()now works on a supervisor (the middleware declares asupervisorhook map).
- Added Usage accounting —
usage.cacheWriteTokensis populated from Anthropic'scache_creation_input_tokens(alongsidecachedTokens);reasoningTokensis left unset because Anthropic bills thinking insideoutput_tokens. - Added Extended thinking —
ModelCallOptions.reasoningmaps to Anthropic'sthinkingbudget (reasoning.effort→ a tiered budget, floored at 1024);temperatureis dropped when thinking is enabled. - Added System-prompt prompt caching —
cacheControl.breakpoints >= 1emits the system prompt withcache_control: { type: "ephemeral" }. - Added Capabilities —
reasoning,promptCaching, andpdfare now advertised;audiostays absent.
- Added Cost-truth capabilities —
reasoning,promptCaching,pdf, andaudioare reported truthfully per model family (inferred from the model id, overridable viabedrock.model(...)). - Added Reasoning / extended thinking —
ModelCallOptions.reasoningmaps to Conversethinkingfor reasoning-capable models, and no-ops elsewhere so unsupported params never reach the wire. - Added Prompt-cache write breakpoints —
cacheControl.breakpointsappends a ConversecachePointblock for caching-capable models. - Added
Usage.cacheWriteTokenspopulated from ConversecacheWriteInputTokens;reasoningTokensis left unset (Bedrock reports no reasoning channel).
- Added
Usage.reasoningTokensis populated from Gemini'sthoughtsTokenCount(alongsidecachedTokens), surfaced only when reported> 0. - Added
ModelCallOptions.reasoningmaps to Gemini'sthinkingConfig(maxTokens→thinkingBudget,effort→ a bucketed budget) for reasoning-capable models. - Added
ModelCapabilitiesnow reportsreasoning,promptCaching,audio, andpdf;cacheControlis accepted as a graceful no-op.
- Added
Usage.reasoningTokensis populated fromcompletion_tokens_details.reasoning_tokens(o-series / gpt-5 hidden reasoning channel), emitted only when> 0. - Added
ModelCallOptions.reasoning.effortmaps to the nativereasoning_effortparam for reasoning-capable models;reasoning.maxTokenshas no Chat Completions equivalent. - Added
ModelCapabilities.reasoningis inferred from the model name (overridable via.model(...));promptCachingis alwaystrue(OpenAI caches automatically), andcacheControlwrite breakpoints are a no-op.
- Added
ModelCapabilities.reasoningis inferred from thinking-capable model tags (overridable viaollama.model({ name, reasoning }));promptCaching/audio/pdfreportfalse. - Added
ModelCallOptions.reasoningmaps onto Ollama's nativethinkflag for reasoning-capable models;reasoning.maxTokensandcacheControlare graceful no-ops.
4.2.11 June 17, 2026
Soft deletes go end-to-end in @warlock.js/cascade. @warlock.js/core adds warlock add notifications, and @warlock.js/notifications gains model-driven column mapping, read-state, and multi-tenant support.
- Added
Migration.createauto-wires thedeletedAtcolumn when the model's delete strategy is"soft"(opt out with{ softDeletes: false }) - Changed Require
@mongez/reinforcements≥ 3.3.0 — the update validator now uses its newwhenhelper for conditional schema fields - Fixed Soft
destroy()now setsdeletedAton the in-memory model — the instance was left stale before - Fixed Update validation no longer strips or rejects the
deletedAtcolumn under strict mode (now whitelisted like the timestamps)
- Added
lowerStage3Decorators()— Vite/Vitest plugin that lowers TC39 Stage-3 decorators with esbuild before oxc / the SSR rewrite mangles them; drop it first inpluginsso model-decorated files load under Vitest 4 / Vite 8. - Added
warlock add notifications— installs@warlock.js/notifications(+ themailfeature), ejectsconfig/notifications.ts, and scaffolds the app-ownedNotificationmodel + migration (idempotent). - Added Notifications connector — a built-in, config-gated connector that lazy-imports
@warlock.js/notifications, so core keeps no hard dependency on it. - Changed
warlock add testnow scaffolds avite.config.tsthat includeslowerStage3Decorators(), so a fresh project can test decorated models out of the box. - Changed
warlock add testtest/test:coveragescripts now run one-shot (vitest run) instead of watch mode — CI-safe by default. - Changed Bumped
@mongez/reinforcementsto 3.3.0 - Fixed
startHttpTestServernow starts early-phase connectors (database, cache, logger, …) before app modules, then late-phase (http, socket) after — mirroring dev/prod boot order; fixes aMissingDataSourceErrorunder the Vitest integration harness.
- Changed In-app column mapping moved onto the model as
static columnMap(recipient/tenant/readAt/isRead); accessors, repository, and channels all derive from it. NewNotificationColumnMaptype. - Changed Read-state is presence-based — declaring
readAt,isRead, or both selects the representation (defaultread_at); the mode-agnosticunreadfilter replacesisRead. - Changed Multi-tenant support — when the model declares a
tenantcolumn, thedatabasechannel reads it off the recipient andcreateFor(...)writes it. - Changed
inApp.list/inApp.listUnreadnow forward full list options (page/limit/orderBy+ filters). - Changed
notificationColumns(model)derives its columns fromcolumnMap; the SQL-vs-MongoDBdataSourcebranch is removed.
- Changed Bumped
@mongez/reinforcementsto 3.3.0
- Changed Bumped
@mongez/reinforcementsto 3.3.0
- Changed Bumped
@mongez/reinforcementsto 3.3.0
- Changed Bumped
@mongez/reinforcementsto 3.3.0
- Changed Bumped
@mongez/reinforcementsto 3.3.0
- Changed Bumped
@mongez/reinforcementsto 3.3.0 (package dependency + project template)
4.2.10 June 17, 2026
Patch: @warlock.js/auth moves its internal @mongez/* utilities from peer to regular dependencies, clearing the install warnings. @warlock.js/logger softens its console timestamp to gray.
- Fixed
@mongez/copper,@mongez/events, and@mongez/reinforcementsare now regulardependenciesinstead ofpeerDependencies— they're framework-internal utilities your app never imports, so declaring them as peers producedunmet peer dependencywarnings on install.
- Changed
ConsoleLog's timestamp (and the↳context arrow) switch from bright-blackgrayto the 256-colorslate— recessive but cleanly legible where bright-black read muddy.
- Changed The project template now pins the latest
@mongez/*versions (@mongez/reinforcements@^3.2.0,@mongez/agent-kit@^1.2.0) so freshly scaffolded apps start on current dependencies. (@warlock.js/*versions are still rewritten to the scaffolder's own version at install time.)
4.2.9 June 17, 2026
Patch: @warlock.js/logger's console output is retuned for scannability — a dimmed time-only timestamp, aligned level columns, and a restored white-on-red fatal badge.
- Changed
ConsoleLogoutput retuned for scannability — a time-onlyHH:mm:ss.SSStimestamp dimmed to gray, fixed-width level tags so the columns align, andfatalrestored to a white-on-bright-red background badge. (FileLog/JSONFileLogkeep the full ISO timestamp.)
4.2.8 June 17, 2026
Patch: @warlock.js/logger now prints each level's name beside its icon (ℹ info, ⚠ warn, ✗ error, …) for at-a-glance reading.
- Changed
ConsoleLognow prints each level's name beside its icon (⚙ debug,ℹ info,⚠ warn,✗ error,✓ success,☠ fatal) for at-a-glance reading.
4.2.7 June 17, 2026
Patch: create-warlock now ships its templates/ folder, fixing the "Something went wrong" error when scaffolding a new project.
- Fixed The published package now ships its
templates/folder, so scaffolding a new project works from the installed package — it was missing from the build, which failed the wizard with "Something went wrong" at the template-copy step.
4.2.6 June 17, 2026
Patch: create-warlock ships its bin folder again, restoring the CLI that was dropped from 4.2.5.
- Fixed The published package now ships its
binfolder again, so thecreate-warlockCLI works from the installed package — it was omitted from the 4.2.5 build.
4.2.5 June 15, 2026
Patch: warlock add notifications now scaffolds the in-app notifications HTTP surface — routes plus a list / mark-read / clear controller, gated by auth.
- Added
warlock add notificationsnow scaffolds the in-app read/dismiss HTTP surface —routes.ts+ anotifications.controller.ts(list / unread-count / mark-read / mark-all-read / clear / delete), gated byauthMiddlewareand recipient-scoped viainApp. Pulls@warlock.js/auth.
4.2.4 June 15, 2026
Patch: corrects a worker-loader build path in @warlock.js/core that 4.2.3 left broken.
- Fixed Fix the worker-loader path in the build entry points — a wrong path in 4.2.3 left the worker entry broken (and blocked the 4.2.3 publish for some packages).
4.2.3 June 15, 2026
Patch: ships the worker scripts as @warlock.js/core build entry points. A wrong path here blocked publishing for some packages — fixed in 4.2.4.
- Fixed Add the worker scripts as build entry points so they ship in the published package.
4.2.2 June 15, 2026
Patch: ships the warlock CLI entry (cli/start) in @warlock.js/core's build.
- Fixed Add
cli/startto the build entry points so thewarlockCLI entry ships in the published package.
4.2.1 June 15, 2026
Patch: @warlock.js/core and @warlock.js/cascade ship their bin folders, restoring the warlock and cascade CLIs dropped from 4.2.0.
- Fixed Ship the
binfolder so thewarlockCLI works from the published package — it was omitted from the 4.2.0 build.
- Fixed Ship the
binfolder so thecascadeCLI works from the published package — it was omitted from the 4.2.0 build.
4.2.0 June 15, 2026
A security overhaul of @warlock.js/auth — brute-force throttling, atomic refresh-token rotation, and CSPRNG secrets (the jwt config gives way to accessToken / refreshToken). Introduces two packages: @warlock.js/notifications (multi-channel notifications) and @warlock.js/access (RBAC + ABAC authorization).
- New Shipped Warlock.js Notifications Package.
- New Shipped Warlock.js Access Package.
- Added
loginThrottleMiddleware— failure-aware brute-force / credential-stuffing protection: counts only failed logins, locks per-account and per-IP, and rejects pre-controller with429(cache-backed, fails open). AddsAuthErrorCodes.TooManyAttempts(EC004). - Added
accessToken/refreshTokenconfiguration blocks, making a separate refresh-token secret first-class. - Added Overridable token storage — register a custom model under
config.auth.accessToken.model/refreshToken.modeland.extend()the exported schemas to add columns (e.g. a multi-tenantorganization_id). - Added
tokenType(access|refresh) claim, stamped on issue and verified on read, so an access token can't be presented as a refresh token. - Added
expires_aton access tokens;warlock auth.cleanupnow purges expired access tokens too. - Fixed Default access-token lifetime was ~3.6 seconds (a numeric
expiresInread as milliseconds) and is now 1 hour. - Fixed Targeted revocation queried
userIdinstead of theuser_idcolumn, so logout / refresh-token removal threw on Postgres and silently no-oped on MongoDB; token queries now route through named model statics. - Fixed Token deletions were fire-and-forget (false success for callers, uncatchable rejections) and are now awaited.
- Fixed The route middleware matched on
userTypeinstead of theuser_typecolumn. - Fixed
revokeAllTokens/revokeTokenFamilyreported an empty set, sotoken.revoked/token.familyRevokednever fired; the revoked rows are now captured before revocation. - Fixed A throwing synchronous auth-event listener no longer turns a completed login into a
500. - Deprecated The
auth.jwt.*configuration block. UseaccessToken/refreshTokeninstead — the legacy shape is still read and mapped forward with a one-time deprecation warning. - Removed Unread
access_tokenscolumnsis_activeandlast_access. - Removed The unused
auth.password.saltconfiguration key. - Security
warlock jwt.generatenow derivesJWT_SECRET/JWT_REFRESH_SECRETfrom a CSPRNG (Random.token) instead ofMath.random(). - Security Refresh-token rotation is atomic — a guarded conditional
UPDATEmeans two concurrent rotations can't both succeed, and a replayed token revokes its entire family.
- Added
log.flush()— awaitable async counterpart toflushSync(), draining every channel viaPromise.allSettledwith per-channel isolation. Implemented byFileLog/JSONFileLog. - Added
SentryLogchannel — forwards entries to Sentry (eventLevelsbecome events, others breadcrumbs;module/actionas tags).@sentry/nodeis an optional, lazily-imported peer. - Added
log.fatal()+fatallevel — ranked strictly aboveerrorfor unrecoverable failures; does not auto-flush or exit. - Added
ConsoleLogrendersfatalwith a☠icon on a bright-red background, distinct fromerror's✗. - Changed
captureAnyUnhandledRejection()now escalatesuncaughtExceptiontolog.fatal(waserror);unhandledRejectionstays aterror. - Changed
LoggingData.typeis now typed asLogLevel(was a duplicated inline union). - Changed
LogContract/LogChannelnow expose an optionalflush?()alongsideflushSync?(). - Fixed
@sentry/nodeis referenced only via local types + an indirect dynamic import, so source-served consumers no longer getTS2307: Cannot find module '@sentry/node'when they don't install the optional peer.
- Changed MongoDB and PostgreSQL drivers now log a failed initial
connect()atlog.fatal(waslog.error) — a boot-time database connection failure is unrecoverable, sofatalkeeps "page on fatal only" alerting clean. Per-query and disconnect failures stay aterror. - Fixed PostgreSQL
increment/decrement(and the*Manyvariants) bound the amount as$1, colliding with the first filter placeholder (SET n = n + $1 WHERE id = $1) so every filtered counter update wrote the wrong number; the amount now binds after the filter params.
- Fixed No-argument tools (declared without an
inputschema) no longer crash on invocation —tool.invokenow skips validation when no schema is present and passes the raw input to the handler.
- Added Opt-in
promptCachingflag on the model config — marks tool definitions withcache_control: { type: "ephemeral" }so multi-trip agents reuse the static tool schemas at the cache-read rate. Off by default.
- Changed Redis driver now logs a failed initial
connect()atlog.fatal(waslog.error) — a boot-time cache connection failure is unrecoverable, sofatalkeeps "page on fatal only" alerting clean.
- Changed
herald-connectorandhttp-connectornow log a failed boot-time connection atlog.fatal(waslog.error) — an unrecoverable broker connection or HTTP port-bind failure makes "page on fatal only" alerting clean; the HTTP connector flushes logs beforeprocess.exit(1). Disconnect / shutdown failures stay aterror.
4.1.15 June 4, 2026
The first public release of Warlock.js — 17 packages published together at 4.1.15. Every change from here on is recorded per package and aggregated on this page.
No changes recorded for this package yet.