@plumbus/core 0.8.0-beta.0 → 0.8.0-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (275) hide show
  1. package/CHANGELOG.md +197 -0
  2. package/README.md +8 -1
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/ai/ai/cost-tracker.d.ts +22 -2
  5. package/dist/ai/ai/model-pricing.d.ts +8 -2
  6. package/dist/ai/ai/openai-strict-schema.d.ts +84 -0
  7. package/dist/ai/ai/provider.d.ts +90 -6
  8. package/dist/ai/ai-service.d.ts +53 -1
  9. package/dist/ai/ai-service.d.ts.map +1 -1
  10. package/dist/ai/ai-service.js +550 -119
  11. package/dist/ai/ai-service.js.map +1 -1
  12. package/dist/ai/cost-tracker.d.ts +22 -2
  13. package/dist/ai/cost-tracker.d.ts.map +1 -1
  14. package/dist/ai/cost-tracker.js +59 -20
  15. package/dist/ai/cost-tracker.js.map +1 -1
  16. package/dist/ai/explainability.d.ts +2 -1
  17. package/dist/ai/explainability.d.ts.map +1 -1
  18. package/dist/ai/explainability.js +1 -0
  19. package/dist/ai/explainability.js.map +1 -1
  20. package/dist/ai/index.d.ts +2 -2
  21. package/dist/ai/index.d.ts.map +1 -1
  22. package/dist/ai/index.js +1 -1
  23. package/dist/ai/index.js.map +1 -1
  24. package/dist/ai/model-pricing.d.ts +8 -2
  25. package/dist/ai/model-pricing.d.ts.map +1 -1
  26. package/dist/ai/model-pricing.js +113 -15
  27. package/dist/ai/model-pricing.js.map +1 -1
  28. package/dist/ai/openai-strict-schema.d.ts +85 -0
  29. package/dist/ai/openai-strict-schema.d.ts.map +1 -0
  30. package/dist/ai/openai-strict-schema.js +730 -0
  31. package/dist/ai/openai-strict-schema.js.map +1 -0
  32. package/dist/ai/prompt-overrides.d.ts +19 -0
  33. package/dist/ai/prompt-overrides.d.ts.map +1 -0
  34. package/dist/ai/prompt-overrides.js +60 -0
  35. package/dist/ai/prompt-overrides.js.map +1 -0
  36. package/dist/ai/provider-failure.d.ts +9 -0
  37. package/dist/ai/provider-failure.d.ts.map +1 -0
  38. package/dist/ai/provider-failure.js +112 -0
  39. package/dist/ai/provider-failure.js.map +1 -0
  40. package/dist/ai/provider.d.ts +90 -6
  41. package/dist/ai/provider.d.ts.map +1 -1
  42. package/dist/ai/provider.js +546 -97
  43. package/dist/ai/provider.js.map +1 -1
  44. package/dist/ai/rag/pipeline.d.ts +21 -1
  45. package/dist/ai/rag/pipeline.d.ts.map +1 -1
  46. package/dist/ai/rag/pipeline.js +17 -1
  47. package/dist/ai/rag/pipeline.js.map +1 -1
  48. package/dist/ai/tool-loop.d.ts +8 -1
  49. package/dist/ai/tool-loop.d.ts.map +1 -1
  50. package/dist/ai/tool-loop.js +7 -0
  51. package/dist/ai/tool-loop.js.map +1 -1
  52. package/dist/ai/types/config.d.ts +13 -0
  53. package/dist/ai/types/fields.d.ts +6 -0
  54. package/dist/ai/types/prompt.d.ts +9 -0
  55. package/dist/ai/usage-validation.d.ts.map +1 -1
  56. package/dist/ai/usage-validation.js +6 -1
  57. package/dist/ai/usage-validation.js.map +1 -1
  58. package/dist/ai/validation.d.ts +7 -3
  59. package/dist/ai/validation.d.ts.map +1 -1
  60. package/dist/ai/validation.js +38 -25
  61. package/dist/ai/validation.js.map +1 -1
  62. package/dist/ai/zod-to-provider-schema.d.ts.map +1 -1
  63. package/dist/ai/zod-to-provider-schema.js +246 -16
  64. package/dist/ai/zod-to-provider-schema.js.map +1 -1
  65. package/dist/api/route-generator.d.ts +2 -12
  66. package/dist/api/route-generator.d.ts.map +1 -1
  67. package/dist/api/route-generator.js +33 -41
  68. package/dist/api/route-generator.js.map +1 -1
  69. package/dist/approvals/index.d.ts +1 -1
  70. package/dist/approvals/index.d.ts.map +1 -1
  71. package/dist/approvals/index.js.map +1 -1
  72. package/dist/approvals/memory-store.d.ts.map +1 -1
  73. package/dist/approvals/memory-store.js +22 -0
  74. package/dist/approvals/memory-store.js.map +1 -1
  75. package/dist/approvals/schema.d.ts +34 -0
  76. package/dist/approvals/schema.d.ts.map +1 -1
  77. package/dist/approvals/schema.js +2 -0
  78. package/dist/approvals/schema.js.map +1 -1
  79. package/dist/approvals/service.d.ts.map +1 -1
  80. package/dist/approvals/service.js +36 -0
  81. package/dist/approvals/service.js.map +1 -1
  82. package/dist/approvals/sql-store.d.ts.map +1 -1
  83. package/dist/approvals/sql-store.js +42 -1
  84. package/dist/approvals/sql-store.js.map +1 -1
  85. package/dist/approvals/types.d.ts +11 -0
  86. package/dist/approvals/types.d.ts.map +1 -1
  87. package/dist/approvals/types.js +1 -0
  88. package/dist/approvals/types.js.map +1 -1
  89. package/dist/audit/service.d.ts.map +1 -1
  90. package/dist/audit/service.js +22 -2
  91. package/dist/audit/service.js.map +1 -1
  92. package/dist/cli/cli.d.ts.map +1 -1
  93. package/dist/cli/cli.js +2 -1
  94. package/dist/cli/cli.js.map +1 -1
  95. package/dist/cli/commands/browser-extension.d.ts +16 -0
  96. package/dist/cli/commands/browser-extension.d.ts.map +1 -1
  97. package/dist/cli/commands/browser-extension.js +16 -5
  98. package/dist/cli/commands/browser-extension.js.map +1 -1
  99. package/dist/cli/commands/dev.d.ts.map +1 -1
  100. package/dist/cli/commands/dev.js +7 -1
  101. package/dist/cli/commands/dev.js.map +1 -1
  102. package/dist/cli/commands/e2e.d.ts +12 -0
  103. package/dist/cli/commands/e2e.d.ts.map +1 -1
  104. package/dist/cli/commands/e2e.js +11 -0
  105. package/dist/cli/commands/e2e.js.map +1 -1
  106. package/dist/cli/commands/generate-openapi-from-zod.d.ts.map +1 -1
  107. package/dist/cli/commands/generate-openapi-from-zod.js +9 -1
  108. package/dist/cli/commands/generate-openapi-from-zod.js.map +1 -1
  109. package/dist/cli/commands/index.d.ts +1 -0
  110. package/dist/cli/commands/index.d.ts.map +1 -1
  111. package/dist/cli/commands/index.js +1 -0
  112. package/dist/cli/commands/index.js.map +1 -1
  113. package/dist/cli/commands/init.d.ts +1 -1
  114. package/dist/cli/commands/init.d.ts.map +1 -1
  115. package/dist/cli/commands/init.js +63 -3
  116. package/dist/cli/commands/init.js.map +1 -1
  117. package/dist/cli/commands/migrate.d.ts.map +1 -1
  118. package/dist/cli/commands/migrate.js +8 -14
  119. package/dist/cli/commands/migrate.js.map +1 -1
  120. package/dist/cli/commands/payments.d.ts +56 -0
  121. package/dist/cli/commands/payments.d.ts.map +1 -0
  122. package/dist/cli/commands/payments.js +184 -0
  123. package/dist/cli/commands/payments.js.map +1 -0
  124. package/dist/cli/commands/run.d.ts.map +1 -1
  125. package/dist/cli/commands/run.js +2 -7
  126. package/dist/cli/commands/run.js.map +1 -1
  127. package/dist/cli/commands/seed.d.ts.map +1 -1
  128. package/dist/cli/commands/seed.js +2 -7
  129. package/dist/cli/commands/seed.js.map +1 -1
  130. package/dist/cli/commands/start.d.ts.map +1 -1
  131. package/dist/cli/commands/start.js +9 -1
  132. package/dist/cli/commands/start.js.map +1 -1
  133. package/dist/cli/commands/worker.js +1 -1
  134. package/dist/cli/commands/worker.js.map +1 -1
  135. package/dist/cli/discover.d.ts +2 -0
  136. package/dist/cli/discover.d.ts.map +1 -1
  137. package/dist/cli/discover.js +39 -5
  138. package/dist/cli/discover.js.map +1 -1
  139. package/dist/config/loader.d.ts +4 -1
  140. package/dist/config/loader.d.ts.map +1 -1
  141. package/dist/config/loader.js +112 -24
  142. package/dist/config/loader.js.map +1 -1
  143. package/dist/data/connection.d.ts +23 -0
  144. package/dist/data/connection.d.ts.map +1 -1
  145. package/dist/data/connection.js +23 -6
  146. package/dist/data/connection.js.map +1 -1
  147. package/dist/data/schema-generator.d.ts.map +1 -1
  148. package/dist/data/schema-generator.js +2 -2
  149. package/dist/data/schema-generator.js.map +1 -1
  150. package/dist/define/definePrompt.d.ts.map +1 -1
  151. package/dist/define/definePrompt.js +6 -0
  152. package/dist/define/definePrompt.js.map +1 -1
  153. package/dist/durable/index.d.ts +2 -2
  154. package/dist/durable/index.d.ts.map +1 -1
  155. package/dist/durable/index.js +2 -2
  156. package/dist/durable/index.js.map +1 -1
  157. package/dist/durable/postgres-persist.d.ts +47 -2
  158. package/dist/durable/postgres-persist.d.ts.map +1 -1
  159. package/dist/durable/postgres-persist.js +152 -6
  160. package/dist/durable/postgres-persist.js.map +1 -1
  161. package/dist/durable/spine-claim.d.ts +22 -1
  162. package/dist/durable/spine-claim.d.ts.map +1 -1
  163. package/dist/durable/spine-claim.js +54 -3
  164. package/dist/durable/spine-claim.js.map +1 -1
  165. package/dist/errors/http.d.ts.map +1 -1
  166. package/dist/errors/http.js +9 -1
  167. package/dist/errors/http.js.map +1 -1
  168. package/dist/events/dispatcher.d.ts +20 -0
  169. package/dist/events/dispatcher.d.ts.map +1 -1
  170. package/dist/events/dispatcher.js +61 -26
  171. package/dist/events/dispatcher.js.map +1 -1
  172. package/dist/events/pipeline-audit.d.ts +14 -0
  173. package/dist/events/pipeline-audit.d.ts.map +1 -0
  174. package/dist/events/pipeline-audit.js +29 -0
  175. package/dist/events/pipeline-audit.js.map +1 -0
  176. package/dist/events/worker.d.ts +3 -0
  177. package/dist/events/worker.d.ts.map +1 -1
  178. package/dist/events/worker.js +14 -14
  179. package/dist/events/worker.js.map +1 -1
  180. package/dist/execution/capability-error-hook.d.ts +24 -0
  181. package/dist/execution/capability-error-hook.d.ts.map +1 -0
  182. package/dist/execution/capability-error-hook.js +17 -0
  183. package/dist/execution/capability-error-hook.js.map +1 -0
  184. package/dist/execution/capability-executor.d.ts +2 -2
  185. package/dist/execution/capability-executor.d.ts.map +1 -1
  186. package/dist/execution/capability-executor.js +45 -7
  187. package/dist/execution/capability-executor.js.map +1 -1
  188. package/dist/execution/context-factory.d.ts.map +1 -1
  189. package/dist/execution/context-factory.js +9 -1
  190. package/dist/execution/context-factory.js.map +1 -1
  191. package/dist/execution/deferred-flow-service.d.ts.map +1 -1
  192. package/dist/execution/deferred-flow-service.js +7 -0
  193. package/dist/execution/deferred-flow-service.js.map +1 -1
  194. package/dist/execution/index.d.ts +1 -0
  195. package/dist/execution/index.d.ts.map +1 -1
  196. package/dist/execution/index.js.map +1 -1
  197. package/dist/fields/index.d.ts +7 -0
  198. package/dist/fields/index.d.ts.map +1 -1
  199. package/dist/fields/index.js +10 -1
  200. package/dist/fields/index.js.map +1 -1
  201. package/dist/flows/dead-letter.d.ts +18 -1
  202. package/dist/flows/dead-letter.d.ts.map +1 -1
  203. package/dist/flows/dead-letter.js +17 -1
  204. package/dist/flows/dead-letter.js.map +1 -1
  205. package/dist/flows/engine.d.ts +16 -1
  206. package/dist/flows/engine.d.ts.map +1 -1
  207. package/dist/flows/engine.js +218 -29
  208. package/dist/flows/engine.js.map +1 -1
  209. package/dist/flows/flow-service.d.ts.map +1 -1
  210. package/dist/flows/flow-service.js +3 -0
  211. package/dist/flows/flow-service.js.map +1 -1
  212. package/dist/flows/step-executor.d.ts +8 -0
  213. package/dist/flows/step-executor.d.ts.map +1 -1
  214. package/dist/flows/step-executor.js.map +1 -1
  215. package/dist/index.d.ts +4 -4
  216. package/dist/index.d.ts.map +1 -1
  217. package/dist/index.js +1 -1
  218. package/dist/index.js.map +1 -1
  219. package/dist/runtime/bootstrap.d.ts +18 -1
  220. package/dist/runtime/bootstrap.d.ts.map +1 -1
  221. package/dist/runtime/bootstrap.js +36 -5
  222. package/dist/runtime/bootstrap.js.map +1 -1
  223. package/dist/runtime/load-extensions.d.ts +2 -1
  224. package/dist/runtime/load-extensions.d.ts.map +1 -1
  225. package/dist/runtime/load-extensions.js +49 -1
  226. package/dist/runtime/load-extensions.js.map +1 -1
  227. package/dist/runtime/register-consumers.d.ts +3 -0
  228. package/dist/runtime/register-consumers.d.ts.map +1 -1
  229. package/dist/runtime/register-consumers.js +27 -6
  230. package/dist/runtime/register-consumers.js.map +1 -1
  231. package/dist/runtime/start-worker-pool.d.ts.map +1 -1
  232. package/dist/runtime/start-worker-pool.js +36 -15
  233. package/dist/runtime/start-worker-pool.js.map +1 -1
  234. package/dist/server/bootstrap.d.ts +42 -16
  235. package/dist/server/bootstrap.d.ts.map +1 -1
  236. package/dist/server/bootstrap.js +91 -21
  237. package/dist/server/bootstrap.js.map +1 -1
  238. package/dist/testing/context.d.ts +2 -0
  239. package/dist/testing/context.d.ts.map +1 -1
  240. package/dist/testing/context.js +8 -3
  241. package/dist/testing/context.js.map +1 -1
  242. package/dist/testing/field-validation.d.ts.map +1 -1
  243. package/dist/testing/field-validation.js +3 -0
  244. package/dist/testing/field-validation.js.map +1 -1
  245. package/dist/types/config.d.ts +13 -0
  246. package/dist/types/config.d.ts.map +1 -1
  247. package/dist/types/context.d.ts +70 -4
  248. package/dist/types/context.d.ts.map +1 -1
  249. package/dist/types/fields.d.ts +6 -0
  250. package/dist/types/fields.d.ts.map +1 -1
  251. package/dist/types/index.d.ts +1 -1
  252. package/dist/types/index.d.ts.map +1 -1
  253. package/dist/types/index.js.map +1 -1
  254. package/dist/types/prompt.d.ts +9 -0
  255. package/dist/types/prompt.d.ts.map +1 -1
  256. package/dist/worker/bootstrap.d.ts +19 -0
  257. package/dist/worker/bootstrap.d.ts.map +1 -1
  258. package/dist/worker/bootstrap.js +156 -84
  259. package/dist/worker/bootstrap.js.map +1 -1
  260. package/instructions/README.md +4 -1
  261. package/instructions/ai-classification.md +132 -0
  262. package/instructions/ai.md +94 -15
  263. package/instructions/cli.md +19 -1
  264. package/instructions/entities.md +3 -1
  265. package/instructions/flows.md +6 -0
  266. package/instructions/framework.md +13 -3
  267. package/instructions/peer-dependencies.md +2 -1
  268. package/instructions/prompts.md +2 -1
  269. package/migrations/durable-tenant/0002_approval_cancellation.sql +3 -0
  270. package/migrations/durable-tenant/meta/_journal.json +7 -0
  271. package/package.json +5 -4
  272. package/dist/ai/__typecheck-fixtures__/legacy-cost-api.d.ts +0 -5
  273. package/dist/ai/__typecheck-fixtures__/legacy-cost-api.d.ts.map +0 -1
  274. package/dist/ai/__typecheck-fixtures__/legacy-cost-api.js +0 -15
  275. package/dist/ai/__typecheck-fixtures__/legacy-cost-api.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,139 @@
1
1
  # @plumbus/core changelog
2
2
 
3
+ ## 0.8.0-beta.10 — 2026-10-12
4
+
5
+ Carries core 0.7.7–0.7.9 and subsequent main fixes into the beta family, alongside the request-timeout, strict-output and budget fixes below.
6
+
7
+ ### Added
8
+
9
+ - Anthropic/Bedrock prompt caching, OpenAI Decisions integration with shared contracts `~0.3.0-beta.1`, payments CLI and collection discovery, `field.bigint()`, authentication runtime discovery, and agent wiring v19.
10
+ - Capability error hooks now cover HTTP, flow, job and event execution while retaining the beta data-plane routing.
11
+
12
+ ### Updated
13
+
14
+ - Merge the latest model compatibility/pricing, per-attempt validation accounting and e2e server-output draining fixes. Publish this family under `plumbus-next` using repository tag `v0.8.0-beta.10`; stable `latest` remains unchanged.
15
+
16
+ ### Fixed
17
+
18
+ - **Node 24 publication tests preserve native fetch dispatcher globals.** The timeout fixtures instrument the selected dispatcher and restore both original property descriptors, including Node's non-configurable globals, on success and failure. Coverage includes legacy-slot fallback and streaming dispatch. This fixes the beta publication test gate without changing provider runtime behavior.
19
+ - **One failed AI call no longer locks a dollar budget for the rest of the day.** With a daily or per-tenant cost limit (e.g. `AI_OPENAI_DAILY_COST_LIMIT`), a call that failed without usage was recorded at unknown cost, and every later call that day was refused with `AI budget exceeded: Cost data unavailable for prior requests — daily budget cannot be enforced.` — even when the call never left the machine (`Connect Timeout Error … fetch failed`), and each refused retry added another unknown row. Failed calls are now charged by what they can have cost. A call that never reached the provider (DNS failure, refused or timed-out connect, TLS handshake failure, a signal aborted before the call, a budget refusal of a retry) or that the provider turned away with a 4xx other than 408/424 is recorded at `cost: 0`, for `streamGenerate` too: the built-in OpenAI and Anthropic adapters now attach the caught error to their stream `error` event as `ProviderStreamEvent.cause`. A call whose usage never came back (connection dropped after sending, read timeout, 5xx, 408/424) or whose stream was cut off before its `done` event keeps `cost: null` and gets a new `AICostRecord.estimatedCost`: its input estimate plus its `maxTokens` (4,096 when unset) at the model's catalog price, never less than what a cut-off stream had already reported, plus the known cost of answered attempts; budgets count it and refuse only once the day's total reaches the limit. Answered validation attempts are no longer dropped from the failure row when a later attempt fails in transit. Where cost cannot be estimated at all (a model with no price answered without a cost), the refusal is scoped to that model: later calls to that same provider and model are refused, other models keep working, and checks that cannot name a model (`checkProviderCostBudget`, decisions, governed calls) stay fail-closed. `CostTracker.checkBudget()` accepts optional `provider` and `model` for that scoping.
20
+ - **OpenAI strict structured outputs accept `z.string().url()` fields.** OpenAI strict mode supports only the string formats `date-time`, `time`, `date`, `duration`, `email`, `hostname`, `ipv4`, `ipv6` and `uuid`, and answered a prompt whose output had a `z.string().url()` field with HTTP 400 `Invalid schema for response_format 'response': In context=('properties', 'link'), 'uri' is not a valid format.` Every OpenAI strict path (Chat Completions `response_format`, streaming included; the `structuredOutputTransport: 'tool'` function on Chat Completions and Responses; Responses `text.format`) now leaves an unsupported format out of the sent schema and names it in the property's description (`Expected string format: uri.`). The prompt's Zod schema still validates the answer, so a value that is not a URL still fails validation and is retried. Anthropic and Bedrock still receive the format. Caller tools are not strict (on the Responses API they now say so with `strict: false`, see below), so OpenAI does not reject their formats.
21
+ - **OpenAI strict structured outputs accept an output whose root is not an object: `classify()`, and array, union or enum outputs.** OpenAI strict mode requires an object at the root of the schema ("Root objects must not be anyOf and must be an object"), so with `enableStrictStructuredOutputs` every `classify()` call (its schema is an array of labels), an `extract()` schema that is a `z.array(...)`, and a prompt `output` that is an array, a union or discriminated union, an enum, a number or a nullable object sent a schema OpenAI refuses. Every OpenAI strict path (Chat Completions `response_format`, streaming included; the `structuredOutputTransport: 'tool'` function on Chat Completions and Responses; Responses `text.format`) now sends such a schema as the single required property `value` of an object, and the adapter takes the answer back out of `{"value": …}` before the AI service validates it, so callers get the same data as before: `ProviderResponse.content`, `streamGenerate` `delta` events and the `partialText` of an `AIIncompleteOutputError` for an answer cut off at `maxTokens` carry the bare answer (a stream holds back only the opening `{"value":`). Object roots are sent as before.
22
+ - **OpenAI strict structured outputs accept intersections and strings with several patterns (`allOf`).** OpenAI strict mode does not support `allOf`, which the schema converter produces for `a.and(b)` / `z.intersection(...)` and for a string with more than one pattern (`.regex(a).regex(b)`, `.startsWith(a).endsWith(b)`, `z.string().cidr()`). Every OpenAI strict path now merges the members into one schema: an intersection of objects becomes one object with both sides' properties and `required` keys (a property both declare gets the merge of its two schemas), types and enums are intersected, a union inside another is spread into both, and an object intersected with a union (`z.object({...}).and(z.discriminatedUnion(...))`) goes into each branch, so every branch lists all the properties it allows (as does the `type` of `z.string().ip()`, next to its `ipv4`/`ipv6` branches); the nulls of such an output's optional properties are dropped again before Zod validates it. What one schema cannot say goes into the description: the patterns of a string with several (`Must match pattern: ….`) and the second of two different patterns or formats a shared property has. The Zod schema still validates the original, so an answer that breaks a dropped pattern still fails validation and is retried. (Intersections of objects and `z.string().cidr()` are now rewritten for every provider by the converter itself, see below.)
23
+ - **Intersections of objects (`a.and(b)`) are sent as one object to every provider.** The schema converter (`zodToProviderJsonSchema`, the schema Anthropic and `@plumbus/ai-bedrock` `structuredOutputs: 'native'` receive) sent an intersection as `allOf` and forced `additionalProperties: false` onto each member, so each member refused the other member's keys and no answer satisfied the schema on a provider that reads it strictly (Anthropic, Bedrock, and the caller tools built with it, which OpenAI receives without strict mode). It now merges the members into one object with both sides' properties and `required` keys (a property both declare gets the merge of its two schemas), and an object intersected with a union goes into each branch. A branch the intersection rules out is dropped instead of being sent with a description: `a.and(b.optional())` used to carry `b`'s never-matching `{ "not": {} }` branch (a keyword no provider documents and OpenAI strict mode refuses) and `a.and(b.nullable())` a branch accepting `a`'s keys alone, which Zod then refused; both now go out as the one object `a & b`, and two discriminated unions on the same key keep only the pairs whose discriminators match. The optional-parameter count Anthropic limits (24) is taken from the merged schema, as sent. MCP tool manifests (`zodInputToJsonSchema`, the capability `inputSchema`) and caller-tool parameters built with `zodToProviderJsonSchema` get the same schema, and so do this release's other converter changes below (a tuple in a manifest is an array of its item schemas, its order and length in the description).
24
+ - **`z.string().cidr()` accepts IPv4 networks on Anthropic and Bedrock.** zod-to-json-schema lists the IPv4 and IPv6 CIDR patterns in `allOf` (as if both applied), and the converter dropped the IPv4 one into the description because it uses non-capturing groups `(?:…)`, so Anthropic and Bedrock were sent an IPv6-only pattern and could not answer `10.0.0.0/8`. The converter now sends the two as an `anyOf` (`{ type: "string", pattern: <IPv4> }` or `{ type: "string", pattern: <IPv6> }`), which OpenAI strict mode accepts as it is. A non-capturing group in any pattern is now sent as a plain group, which matches the same strings, instead of being moved to the description (a pattern with a backreference still is). Keywords next to a union (the `type` of `z.string().ip()`) now go into each branch for every provider, and a string's `type` into each member of the `allOf` listing its patterns (`.regex(a).regex(b)`, `.cidr().startsWith(…)`, whose IPv4/IPv6 branches had no `type`), so every branch and member is a whole schema with a `type`, as Anthropic's SDK helpers send them.
25
+ - **Optional outputs, tuples and defaults work with strict structured outputs.** An output that is `.optional()` (and any `.optional()` that is not an object property: an array item, a union member) carried a never-matching `{ "not": {} }` branch, which no provider's documented subset includes; on OpenAI it also put the object root in the `{"value": …}` envelope. It is now dropped, so an optional object output is sent as the object. A tuple was sent as `prefixItems`, which OpenAI strict mode refused (HTTP 400 `Invalid schema for response_format 'response': In context=('properties', 'point'), array schema missing items.`) and Anthropic and Bedrock do not list (Anthropic: "Array constraints beyond `minItems` of 0 or 1" are not supported); a rest element went out as the draft-07 `additionalItems`. A tuple is now sent to every provider as an array whose `items` is its item schemas (an `anyOf` when they differ, counted as one union parameter), with the order and length in the description (`Must hold exactly 2 items, in this order: (1) string; (2) number.`). OpenAI strict mode does not list `default` among its supported keywords: on every OpenAI strict path a `.default(…)` is now named in the description (`Defaults to "untitled".`), the property goes out nullable as an optional one, and Zod fills in the default for the `null`; Anthropic and Bedrock still receive `default`. The prompt's Zod schema still validates the original (each tuple position, its length, the default).
26
+ - **OpenAI cache writes are counted and priced.** OpenAI reports the prompt tokens it writes to its cache (`usage.prompt_tokens_details.cache_write_tokens` on Chat Completions, streamed or not; `usage.input_tokens_details.cache_write_tokens` on the Responses API), and GPT-5.6 and later bill them at 1.25× the input rate (`gpt-6-luna`: $0.125 per million tokens against $0.10 input). The adapter ignored the field, so `usage.cacheWriteTokens` was absent and the writes were priced as ordinary input — a first request with a 2,192-token cached prefix on `gpt-6-luna` was recorded at $0.000235 instead of $0.0002898. `TokenUsage.cacheWriteTokens` is now set whenever OpenAI reports it, and catalog pricing charges it as OpenAI bills it: 1.25× input (2× that above the 272K long-context threshold) on GPT-5.6, GPT-6 and later models, and the ordinary input rate on earlier models, which bill no extra for a cache write ("No additional cache-write charge"; previously every cache write was priced at 1.25×, which only Claude and GPT-5.6+ charge). Cache writes are part of the input tokens, so they are priced once. Claude pricing is unchanged.
27
+ - **Reasoning tokens are reported on the OpenAI Responses API, on Chat Completions streams, and through validation retries.** Responses calls (caller tools under GPT-5.6/GPT-6 reasoning, and the `structuredOutputTransport: 'tool'` function there) ignored `output_tokens_details.reasoning_tokens`, so their usage had no `reasoningTokens` while the same call on Chat Completions had it; Chat Completions streams dropped `completion_tokens_details.reasoning_tokens` too. Both now set `TokenUsage.reasoningTokens` the same way, and it is no longer lost on the way to the caller: it is summed over validation retries (so `generateWithUsage` on a prompt with an output schema returns it), tool-loop rounds and the streaming validation fallback, passed through on `streamGenerate` `done` usage, and kept on the `AICostRecord.usage` ledger row (a value that is not a non-negative number, from a custom adapter, is dropped there rather than failing the call). Pricing is unchanged: reasoning tokens are part of `outputTokens` on both APIs, which is what is priced, and are not charged again.
28
+ - **Caller tools on the OpenAI Responses API stay non-strict: optional arguments are no longer filled in.** Caller tools were sent to Responses without `strict`, and with `strict` omitted the Responses API "will attempt to normalize your schema into strict mode when possible": it made every property required, so a `get_weather` tool whose `unit` was optional was echoed back as `strict: true` with `"required": ["city", "unit"]`, and the model answered `{"city":"Paris","unit":"celsius"}` to a request that named no unit — an invented value the app's input check cannot tell from a real one. Responses caller tools are now sent with `strict: false`, as they behave on Chat Completions (non-strict by default): the same request now comes back `{"city":"Paris"}`. Schemas are sent as declared, so none that strict mode cannot take is refused.
29
+ - **`structuredOutputTransport: 'tool'` works on GPT-5.6 and GPT-6 under reasoning.** The forced structured-output function was always sent to Chat Completions, which refuses function tools under reasoning on GPT-5.6 and GPT-6: with the default reasoning setting `gpt-6-luna` answered HTTP 400 `Function tools with reasoning_effort are not supported for gpt-6-luna in /v1/chat/completions. To use function tools, use /v1/responses or set reasoning_effort to 'none'.` On those models, unless reasoning is disabled, the tool now goes to the Responses API as a strict function with a forced `tool_choice`. The reasoning setting (explicit effort, legacy `reasoningEffort`, or the model default) is sent as configured, and the function's arguments come back as the answer (`finishReason: 'stop'`, or `'length'` when cut off at `max_output_tokens`). With reasoning disabled, and on every other model (explicit effort included), the tool stays on Chat Completions as before, so OpenAI-compatible servers without `/responses` are unaffected.
30
+ - **Long AI requests are no longer cut off at five minutes, and every prompt can set its own timeout.** Provider requests had one fixed timeout per adapter (`requestTimeout`, default 120 s), and above 300 s it did not even hold: a non-streamed answer arrives only once the model has finished, and Node's `fetch` (undici) stops waiting by itself after 300 s without response headers or without the next body chunk. A long structured answer with reasoning (e.g. ~18k output tokens) failed with `TypeError: fetch failed` (`UND_ERR_HEADERS_TIMEOUT`) at 300 s even with `AI_OPENAI_REQUEST_TIMEOUT=600000`, and a short prompt could not ask for less than the adapter's limit. The built-in OpenAI and Anthropic adapters now set undici's header and body limits one second above the request's timeout on every request, through the process's own dispatcher (a proxy or agent installed with undici's `setGlobalDispatcher` still applies), so the timeout alone decides (undici's coarse timers can fire up to half a second early, so limits equal to the timeout would beat it on a reused connection); it covers the whole answer, a streamed one included. A request its timeout ends now fails with a `ProviderAPIError` carrying the new `timeoutMs` (the limit reached), `retryable: false` and no `statusCode` — e.g. `OpenAI API request timed out after 900000 ms` — instead of a bare `TimeoutError: The operation was aborted due to timeout`. It is not retried (the provider may have billed the work, and the same limit usually ends the same way) and is charged an estimate like any call whose usage never came back. A `streamGenerate` call cut off mid-stream throws it after the deltas already yielded. The caller's own `signal` is unchanged: its abort is rethrown as raised, and when both fire the first decides. `@plumbus/ai-bedrock` reports its timeouts the same way.
31
+ - **Flow workers apply `PROMPT_{NAME}_*` overrides.** The worker pool's AI service (`buildWorkerAiService`, used by `plumbus worker`, the worker side of `plumbus dev`/`start`, and the MCP serve context) applied only `resolveAiOverrides`, not the env overrides from `config.aiProviders.promptOverrides`, so `PROMPT_{NAME}_MODEL`, `_PROVIDER`, `_TEMPERATURE`, `_MAX_TOKENS` and `_TIMEOUT_MS` took effect in HTTP capabilities but were silently ignored in flow steps, jobs and event handlers. The worker now starts from the same env overrides as the server, with `resolveAiOverrides` merged over them as before.
32
+ - **An unusable `PROMPT_{NAME}_TIMEOUT_MS`, `_MAX_TOKENS` or `_TEMPERATURE` is rejected when the config loads.** They were read with `parseInt`/`parseFloat`, so `abc` became `NaN` and `15m` became 15: an invalid timeout then failed every call to the prompt with `AIInvalidRequestError` (`timeout_invalid`), an invalid max-tokens dropped the OpenAI output cap, and an invalid temperature was sent as `null`. `loadConfig()` (`loadPromptOverrides()`) now throws an error naming the variable — e.g. `Invalid PROMPT_WRITER_TIMEOUT_MS="15m": expected a whole number of milliseconds from 1 to 2147483647` — so `plumbus dev`, `start` and `worker` stop at startup. `TIMEOUT_MS` must be a whole number from 1 to 2 147 483 647, `MAX_TOKENS` a whole number of at least 1, `TEMPERATURE` a number from 0 to 2; a blank value is no override.
33
+ - **`resolveAiOverrides` no longer drops a prompt's `PROMPT_{NAME}_*` env overrides, and an override keyed by the prompt's name wins over env.** The dynamic wrapper (`wrapAIServiceWithDynamicOverrides`, used by the HTTP server and the worker pool) merged the hook's `promptOverrides` into the env ones a whole entry at a time, so a hook answer of `{ model }` for a prompt replaced that prompt's entry and dropped its `PROMPT_{NAME}_TIMEOUT_MS` and `_MAX_TOKENS` (the call fell back to the prompt definition, or the adapter's 120 s timeout). The lookup also tried the env-style key (`report_summarize`) before the prompt's name (`report.summarize`), so an env override beat a hook override keyed by the real name. Each field (`provider`, `model`, `temperature`, `maxTokens`, `reasoning`, `timeoutMs`) is now resolved on its own: per-call value → `resolveAiOverrides` → `PROMPT_{NAME}_*` → prompt definition → defaults; a field a layer leaves unset (or `null`) keeps the lower layer's value. `reasoning` and the deprecated `reasoningEffort` count as one field: the highest layer that sets either decides both, so an override's `reasoningEffort` replaces the prompt definition's `reasoning` (adapters prefer `reasoning` when both are sent, so it used to win). In each layer an entry is found by the prompt's name and by its env-style key, the name winning when both set a field (new `resolvePromptOverride`, internal); only override fields are read from an entry, so a hook can return database rows as they are. The hook's answer now replaces the previous one on each call instead of accumulating, so an override it stops returning no longer applies, and a hook that returns no `defaultModel` / `defaultProvider` falls back to the configured ones (also in services bound with `withContext`). **Behaviour change:** a hook entry that sets only `model` (or only `provider`) now keeps the other one from `PROMPT_{NAME}_*`, where it used to skip the env entry and take the prompt definition's; set both when an override moves a prompt to another provider. A hook that answered `{}` on a cache miss or database error, relying on its last answer staying in force, now falls back to the env values for that call.
34
+ - **`resolveAiOverrides`' `defaultProvider` takes effect.** `createAIService` read the default provider once when it was created, so the hook's `defaultProvider` never routed a call from the base service, and a `withContext` service kept whichever value the last call had left. The default provider is now read on every call.
35
+ - **A provider `REQUEST_TIMEOUT`, `MAX_TOKENS` or `DAILY_COST_LIMIT` that is not a plain number is rejected when the config loads.** `AI_REQUEST_TIMEOUT`, `AI_MAX_TOKENS`, `AI_DAILY_COST_LIMIT` and their `AI_OPENAI_*`, `AI_ANTHROPIC_*` and `AI_BEDROCK_*` forms (and `AI_BEDROCK_PRICING_TTL_MS`) were read with `parseInt`/`parseFloat`: `abc` became `NaN` and failed every call later (or the budget setup with an error naming no variable), and `15m` became a 15 ms timeout. `loadConfig()` now throws naming the variable, e.g. `Invalid AI_OPENAI_REQUEST_TIMEOUT="15m": expected a whole number of milliseconds from 1 to 2147483647`. Accepted: `REQUEST_TIMEOUT` a whole number of milliseconds from 1 to 2 147 483 647; `MAX_TOKENS` a whole number, 0 or more; `DAILY_COST_LIMIT` a number of US dollars, 0 or more (decimals allowed); `AI_BEDROCK_PRICING_TTL_MS` a whole number of milliseconds, 0 or more. Unit suffixes, exponents and signs are refused; surrounding spaces are ignored, and a blank value is unset (a blank `AI_OPENAI_MAX_TOKENS=` used to become `NaN`). **Behaviour change:** a value that only worked because `parseInt` stopped at the first non-digit (`120000ms`, `4096 tokens`, a `MAX_TOKENS` of `4096.5`) now stops startup; write the plain number.
36
+ - **`ctx.ai.retrieve` cancels its embedding request when its `signal` fires.** `retrieve` accepted a `signal` (inside flow steps `ctx.signal`) but never passed it on, and `EmbeddingRequest` had no signal, so a cancelled request still waited for the query's embedding, up to the adapter's timeout. `EmbeddingRequest.signal` is new; `ctx.ai.retrieve` passes its signal to the RAG pipeline (`RetrievalQuery.signal`, and `IngestDocumentInput.signal` for `ingest`), which passes it to every `embed()` call; the OpenAI adapter aborts the embedding fetch and rethrows the caller's abort as raised. The pipeline also checks the signal itself for adapters that ignore it: an already-aborted `retrieve` embeds nothing, one aborted during its embedding does not search, and an aborted `ingest` embeds no further batch and stores nothing (an embedding already paid for is still reported to `onEmbeddingCost`). Custom adapters should pass `request.signal` to their transport in `embed()`.
37
+
38
+ ### Added
39
+
40
+ - **Per-prompt and per-call request timeouts.** `definePrompt({ model: { timeoutMs } })` sets how long one provider request for the prompt may take, in milliseconds; `generate`, `generateWithUsage` and `streamGenerate` take a per-call `timeoutMs`; `promptOverrides` / `resolveAiOverrides` take `timeoutMs`, and `PROMPT_{NAME}_TIMEOUT_MS` sets it from the environment. Resolution: call → override → prompt → the adapter's `requestTimeout` (default 120 000 ms, unchanged when nothing is set). Each validation retry, each retried 408/429/5xx answer and the streaming validation fallback get their own. Values must be whole milliseconds from 1 to 2 147 483 647: `definePrompt` rejects others, and per-call or override values fail with `AIInvalidRequestError` (`reason: 'timeout_invalid'`) before anything is sent. See [Request Timeouts](../../docs/ai/ai-integration.md#request-timeouts).
41
+ - **Per-call `timeoutMs` on `extract`, `classify`, `retrieve`, `runToolLoop` and embeddings.** These calls always used the adapter's `requestTimeout`. `ctx.ai.extract()` and `ctx.ai.classify()` now take `timeoutMs` (each validation retry gets its own; with a decision provider it is passed on as that provider's `timeoutMs`, at most 300 000), `runToolLoop()` takes `timeoutMs` for every round and the final request, `EmbeddingRequest` takes `timeout` (the OpenAI adapter and `@plumbus/ai-bedrock` honour it), and the RAG pipeline's `ingest()`/`retrieve()` and `ctx.ai.retrieve()` take `timeoutMs` for their embedding requests. Values outside 1–2 147 483 647 fail with `AIInvalidRequestError` (`timeout_invalid`) before anything is sent; the RAG pipeline checks its `timeoutMs` itself, so this holds with an adapter that ignores `EmbeddingRequest.timeout`. See [Request Timeouts](../../docs/ai/ai-integration.md#request-timeouts).
42
+ - **`ProviderAPIError`, `isProviderAPIError` and `isProviderTimeoutError` are exported from `@plumbus/core`.** An application could only spot a provider timeout by checking the error's shape. `isProviderAPIError(error)` recognises any provider HTTP failure, including the same-shaped errors `@plumbus/ai-bedrock` builds; `isProviderTimeoutError(error)` is true only when the request's own timeout ended it and narrows `timeoutMs` to a number.
43
+
44
+ ## 0.8.0-beta.9 — 2026-10-01
45
+
46
+ ### Fixed
47
+
48
+ - **GPT-6 models report their reasoning levels.** `ProviderModel.reasoningEfforts` came from a table that stopped at GPT-5.6, so `listModels()` answered `null` ("unknown") for `gpt-6-sol`, `gpt-6-luna`, `gpt-6-astra` and `gpt-6.1-sol`, and an application offering choices from it could offer none. Per OpenAI's model pages: `gpt-6-sol` and `gpt-6-luna` take `none`…`max`; `gpt-6-astra` and `gpt-6.1-sol` take `low`…`max` (no `none`). `gpt-6.1-sol` is priced ($2 / $10 per million tokens).
49
+
50
+ ### Changed
51
+
52
+ - **Pricing synced with OpenAI's and Anthropic's pricing pages on 2026-10-01.** Added `claude-sonnet-5-5` ($2 input / $0.20 cached input / $10 output per million tokens, flat across its 1M window) and `gpt-rosalind-research` ($5 / $0.50 / $25; OpenAI starts billing it on 2026-10-05, and the catalog charges it from now on). `gpt-6.1-sol` cache reads cost $0.10, 5% of input rather than the 10% default. The full-request long-context premium above 272K input tokens (2× input and cache, 1.5× output) now also applies to `gpt-6-astra`, `gpt-6.1-sol`, `gpt-5.6-terra`, `gpt-5.6-luna`, `gpt-5.5`, `gpt-5.5-pro`, `gpt-5.4` and `gpt-5.4-pro`, which OpenAI prices that way; they were charged base rates at any length, so long requests were under-recorded. No other published rate changed.
53
+ - **Manual pricing tool.** `update-model-pricing` reads Anthropic rows whose input or output cell carries a footnote (it had stopped parsing Sonnet 5), records OpenAI long-context thresholds, maps the Life Sciences category, compares cache rates with a float tolerance, and checks the per-minute and per-character rates in `@plumbus/voice-openai`.
54
+
55
+ ## 0.8.0-beta.8 — 2026-10-01
56
+
57
+ ### Fixed
58
+
59
+ - **Connections made from `config.database` honour `ssl`.** The server and worker pool's connection (`connectPostgresDatabase`), `plumbus seed`, `plumbus run`, and `plumbus migrate`'s create-database and reset connections built their postgres.js options without `ssl`, so they connected in plain text even when the configuration asked for TLS — which production always does. A Postgres that requires TLS refused them: Amazon RDS for PostgreSQL 15+ (`rds.force_ssl=1`) answered `28000 … no encryption`, and the worker's startup preflight surfaced it as `Failed query: SELECT lease_owner, lease_expires_at FROM flow_executions LIMIT 0`. All five now take their options from one `postgresConnectionOptions(config.database)`; `ssl: true` negotiates TLS and verifies the server certificate (supply a private CA, e.g. the RDS bundle, with `NODE_EXTRA_CA_CERTS`). Tenant data-plane connections and `migrate apply` already passed `ssl`.
60
+
61
+ ### Changed
62
+
63
+ - **`DATABASE_SSL=false` now turns TLS off in production.** Production still defaults to TLS; an explicit `false` is for a production Postgres without TLS, which the fix above would otherwise stop connecting to (`validateConfig` keeps its `database.ssl should be enabled in production` warning).
64
+
65
+ ## 0.8.0-beta.7 — 2026-09-27
66
+
67
+ Carries core 0.7.5 and 0.7.6 from `main` into the beta family.
68
+
69
+ ### Fixed
70
+
71
+ - **GPT-6 OpenAI requests.** Chat Completions send `max_completion_tokens` for `gpt-6*`; active or default reasoning omits the unsupported sampling `temperature` (explicitly disabled reasoning keeps it); tool calls under GPT-6 default reasoning use the Responses API.
72
+ - **Event pipeline audit is best-effort.** Dispatch and delivery audit entries (`event.dispatch.*`, `event.consumer.*`, including the 0.8 `event.consumer.skipped`) are written through `recordPipelineAudit`: a failed audit write is logged and no longer skips the publish, the consumer handler or the dead-letter write. Beta.2 had already made the outcomes valid, but a failing audit write still counted as a failed publish or a failed consumer attempt, so the row was retried or dead-lettered instead of delivered. `createOutboxDispatcher` and `createEventWorker` take an optional `logger`, which the worker pool passes.
73
+ - **Stranded outbox claims are released.** On every plane it polls, the dispatcher returns `event_outbox` rows left in `processing` for longer than `claimTimeoutMs` (default 5 minutes) to `pending` before selecting, so rows claimed by a dispatcher that stopped before publishing are delivered.
74
+ - **Timer polls cannot reject.** Per-plane failures were already contained; the timer now also catches the poll itself, and every outbox-poll failure goes to the logger when one is configured.
75
+
76
+ ## 0.8.0-beta.6 — 2026-09-23
77
+
78
+ Carries core 0.7.2 and 0.7.4 from `main` into the beta family (0.7.4 absorbed the unpublished 0.7.3).
79
+
80
+ ### Added
81
+
82
+ - **Typed decisions through `ctx.ai.decide()`.** Capabilities and flows call decision adapters over the shared `@plumbus/ai-decision` contract, with security checks over state and questions, request bounds, budgets, cancellation forwarding and per-call cost/usage records attributed to tenant and actor. Register providers with `export const decisions = { providers, defaultProvider }` in `app/server.ts` (API and worker processes share it); named definitions are discovered under `app/decisions/`. Successful and failed decision calls reach `onAICostRecorded`, keeping known billed metadata after an invalid provider answer and recording unknown cost as `null`. `mockAI({ decide })` returns decision results in tests. Core now depends on `@plumbus/ai-decision` `~0.3.0-beta.0`; the TypeSafe and Laya adapters stay optional installs. Docs: [Decision providers](../../docs/ai/decision-providers.md).
83
+ - **`ctx.ai.classify()` selects a provider per call.** Optional `provider` and `model` pick a registered text provider or a TypeSafe/Laya decision adapter. Decision classification batches one probability question per label and returns the labels at or above `threshold` (default 0.5), with the same validation, security, budgets, cancellation and a single `classify` cost record; a name registered in both registries is refused. Calls without `provider` keep the default text provider and the `string[]` result. Text classification keeps provider admission and cost context. Recipe: `instructions/ai-classification.md`.
84
+ - **Agent wiring v17.** `plumbus init` links the classification recipe and the decision provider instruction indexes from every agent format; refresh existing apps with `plumbus init --patch --agent all`.
85
+ - **Structured final answers with native tools.** `generateWithUsage({ tools, outputValidation: 'prompt' })` validates a final answer against the prompt output schema once and records the usage of a failed validation; a malformed answer is not retried because a retry could repeat tool effects, and `tool_calls` results bypass validation. The OpenAI adapter forwards the schema beside native tools (Chat Completions `response_format`, Responses `text.format`). Pair with `@plumbus/chat` 0.3.0-beta.1 for structured custom-agent output. Docs: [AI integration](../../docs/ai/ai-integration.md#structured-answers-with-native-tools).
86
+ - **Pricing.** `claude-opus-5-5`: $4 input, $0.20 cached input, $5 five-minute cache writes, $20 output per million tokens, no long-context surcharge. `gpt-6-sol` ($2 / $0.20 / $10) and `gpt-6-luna` ($0.10 / $0.01 / $0.50), including cache writes and the full-request long-context premium above 272K input tokens.
87
+
88
+ ### Fixed
89
+
90
+ - **Generated OpenAPI documents a capability's declared `api.method`.** `plumbus generate` used the kind default (`get` for queries, `post` otherwise), so a capability the server routes as `PATCH` was documented as `post` and generated clients got a 404. The declared method now wins; the kind default applies only when none is declared.
91
+
92
+ ## 0.8.0-beta.5 — 2026-09-20
93
+
94
+ ### Fixed
95
+
96
+ - Polling flow workers acquire one lease at a time, bound each cycle by the configured batch limit, and prevent overlapping polls from reclaiming waiting work.
97
+ - Static capability and HTTP queued-job access checks precede input parsing, preventing unauthorized schema disclosures. Input-aware hooks still receive parsed input.
98
+ - `migrate apply` exits unsuccessfully on migration, preflight, database-selection, creation, and connection errors while preserving connection cleanup and JSON diagnostics.
99
+
100
+ ## 0.8.0-beta.4 — 2026-09-17
101
+
102
+ Repository tag `v0.8.0-beta.3` already pointed at the format-only commit and no `0.8.0-beta.3` package was published, so this increment is `beta.4`.
103
+
104
+ ### Added
105
+
106
+ - **Approval request cancellation.** `ApprovalService.cancel({ requestId, auth, reason })` withdraws a still-pending request after human-actor and host-authorization revalidation, records cancellation actor/reason evidence, and atomically cancels linked open/claimed human tasks without creating an approval decision. Durable tenant migration `0002_approval_cancellation.sql` adds the evidence columns. Docs: [Approvals](../../docs/core-concepts/approvals.md).
107
+
108
+ ### Fixed
109
+
110
+ - **Tenant flow heartbeats extend the authoritative spine hint.** A long-running tenant-placed step now extends its guarded `opaque_dispatch` lease before extending the tenant `flow_executions` lease. A stale worker cannot extend or acknowledge a hint owned by a peer, so a healthy step is not re-dispatched when the original spine lease duration elapses.
111
+ - **Flow retry policies now schedule their declared backoff.** A failed step with retries remaining moves to `waiting`, releases its lease, and is claimable only at its computed fixed/exponential `wakeAt`. Tenant-placed flows persist `retry-scheduled` execution state, a retry wait, and a future-dated dispatch outbox row in one tenant transaction before acknowledging the inbound spine hint.
112
+ - **Dispatch outbox and execution state use the framework schema.** The event pump and durable writers resolve `dispatch_outbox` / `execution_state` as `frameworkSchema ?? PLUMBUS_FRAMEWORK_SCHEMA ?? FRAMEWORK_SCHEMA` and no longer qualify those tables by the plane handle's `coreSchema` (that name is for tenant entity tables). Server and worker bootstraps pass `frameworkSchema` through to the engine and pump. Audit metadata with NUL bytes is named instead of rejected by jsonb.
113
+
114
+ ## 0.8.0-beta.2 — 2026-09-16
115
+
116
+ ### Added
117
+
118
+ - **Host data-plane resolver exports.** `app/server.ts` may export `dataPlaneResolver`, `listTenantRefs`, `untenantedDataPlane`, `requestDataPlane`, `workerDataPlane` and `resolveTenantRef`; `plumbus dev` / `start` / `worker` pass them to `createServer` and the worker pool. Request-side flow engines then carry `spineDispatch`, so a flow started by a request is written to the tenant plane with an opaque hint on the control plane. `untenantedDataPlane: 'control-plane'` (`FlowSpineDispatchConfig.untenanted`) lets flows with no tenant stay classic spine rows claimed beside the tenant hints (default `'refuse'`); `requestDataPlane: 'control-plane'` keeps request repositories on the boot connection and `workerDataPlane: 'control-plane'` does the same for claimed units (`WorkerPoolConfig.unitDataPlane`; both default to `'resolved'`). Under `'resolved'`, a `tenantScoped: false` capability reads the control plane even for a tenant-bound request. A host that supplies a resolver must not also supply `createDataService`. `retryDeadLetteredFlow` takes an optional `placement` (`{ spineDb, coreSchema? }`) that reopens a tenant-placed execution and publishes a fresh spine hint (`republished` on the result). A host enabling spine dispatch needs the framework's `opaque_dispatch` table on its control plane (`migrations/spine/0000_opaque_dispatch.sql`) and the `core_plumbus` durable tables on each tenant plane. Docs: [Tenant data planes](../../docs/sdk-reference/tenant-data-planes.md).
119
+ - **AI provider admission and trace propagation.** `ServerConfig` (and `app/server.ts` exports, honoured by `dev` / `start` / `worker`) accept `aiProviderConcurrency`, `resolveAIProviderHeaders` and `onAIProviderSpan`; `createAIService` takes the same as `providerConcurrency`, `resolveProviderHeaders` and `onProviderSpan`. The concurrency ceiling is an immediate per-scope admission shared by every identity-bound service — default scope provider + tenant + `costContext.serviceArea`, `resolveScope` overrides it — and saturation throws a `PlumbusError('conflict')` with `failureCode` / `reason` `ai-provider-concurrency-exhausted` and `retryAfterSeconds: 1` instead of queueing. `resolveAIProviderHeaders` runs before every real provider attempt (validation retries and streams included) and its result reaches the adapter as `ProviderRequest.transportHeaders`; `authorization`, `proxy-authorization`, `x-api-key`, `api-key` and `content-type` are reserved and refused. `onAIProviderSpan` receives the completed client span (`AIProviderSpan`: provider, operation, tenant, actor, correlation id, cost context, `startedAt`, `durationMs`, `status`, `traceparent`, `errorType`) and is best effort — hook failures never change the model-call result. `AIService.withContext` accepts `correlationId`, and flow workers bind the persisted flow correlation id so one trace spans request → flow → provider. New exports: `AIProviderCallContext`, `AIProviderConcurrencyConfig`, `AIProviderOperation`, `AIProviderSpan`. Docs: [AI integration](../../docs/ai/ai-integration.md).
120
+ - **`ctx.flows.terminate(executionId)`.** `FlowService` now exposes the engine's `terminate`: it aborts the active step and closes the execution as `cancelled` without running compensations, where `cancel` runs every declared compensation for completed steps. Docs: [Flows — cancellation](../../docs/core-concepts/flows.md).
121
+
122
+ ### Fixed
123
+
124
+ - **Event dispatch audit outcomes are valid.** The dispatcher and the event worker recorded `pending` / `retry` / `dead_lettered` / `skipped` as audit outcomes, which `createAuditService` refuses, so with a real audit service every dispatch attempt threw and one plane's rejected poll escaped the timer and ended the worker process. `event.dispatch.attempt`, `event.consumer.attempt` and `event.consumer.skipped` now carry no outcome; `event.dispatch.failed` and `event.consumer.dead_lettered` are `failure` (the former adds `disposition: retry | dead_lettered`). A failing plane's outbox poll is contained and the other planes keep being polled.
125
+ - **Spine hint lifecycle under a data-plane resolver.** The outbox pump publishes only never-published rows (it re-readied every unacknowledged row on each poll, clearing live leases); acknowledged hints close their outbox rows; the follow-up hint between drained steps is published under the running worker's lease; a claim drops the hint of a terminal row instead of reviving it; `engine.cancel` and the runner's failure finaliser close the durable state of a tenant-placed execution; and a hint whose plane fails to resolve is dead-lettered after `FlowSpineDispatchConfig.maxClaimAttempts` (default 10) claims with `privacy_safe_failure_category_id = 'plane-unresolved'` instead of being re-leased forever.
126
+
127
+ ## 0.8.0-beta.1 — 2026-09-11
128
+
129
+ ### Changed
130
+
131
+ - **403 responses keep `metadata.reason`.** `errorToHttpResponse` still replaces the message with `Access denied` and drops every other metadata key, but a string `reason` set at the throw site (`session-revoked`, `tenant-suspended`) now reaches the client so it can act on the refusal without learning anything else about it.
132
+
133
+ ### Fixed
134
+
135
+ - **Audit writer refusals are surfaced, not relabelled.** A custom `AuditWriter` that throws a `PlumbusError` is refusing the record (for example a validation error about the payload). `AuditService.record` now rethrows it unchanged on the first attempt instead of retrying three times and wrapping it as `Audit persistence failed`. Non-Plumbus errors keep the retry-and-wrap behaviour.
136
+
3
137
  ## 0.8.0-beta.0 — 2026-09-11 — core 0.8 beta family
4
138
 
5
139
  ### Upgrade boundary
@@ -39,6 +173,69 @@
39
173
  - `WorkerPoolConfig.schedulePlanes` and `app/server.ts` `export const schedulePlanes`: tenant planes for the scheduler alone, for hosts that route tenant data themselves; flows, events and the outbox keep the pool plane.
40
174
  - The scheduler syncs and polls the pool only when a registered flow is scheduled on the spine, tolerates a plane it cannot resolve or read, and reports the driver's cause.
41
175
 
176
+ ## 0.7.9 — 2026-10-08
177
+
178
+ - Fix `plumbus e2e` freezing the frontend dev server partway through a long run. The server's stdout was piped but never read, and `next dev` writes a line there for every request with stdout in blocking mode, so after about 80 KB (roughly 1,000 page loads) the server blocked in that write for good. The test in flight timed out at its 30 s limit and every later test would too, which looked like one random late test failing. The runner now reads and discards that stream; stderr is still relayed as `[frontend]` lines.
179
+ - Add prompt caching for Anthropic and Bedrock (#63). Pass `cache: true` (system prompt + tools) or `{ system?, tools?, messages? }` on `ctx.ai.generate`, `generateWithUsage`, `streamGenerate`, or `runToolLoop`, which forwards it to every round. Anthropic requests get `cache_control` on the system prompt, the last tool, and (when asked) the last message; `@plumbus/ai-bedrock` 0.2.2 adds Converse `cachePoint`. OpenAI ignores the option. Without `cache` or the default below, requests are unchanged. Cache reads and writes are reported in `usage.cachedInputTokens` / `usage.cacheWriteTokens` and priced as before.
180
+ - Add a prompt-cache default for every call: `AI_PROMPT_CACHE=true` (or sections such as `system,messages`) loads into `aiProviders.cache` (`ai.cache` in single-provider mode), and the API server, workers, and `plumbus mcp serve` pass it to `createAIService({ cache })`. A per-call `cache` overrides it. Other values log a warning and leave caching off.
181
+ - Fix multi-turn history in the Anthropic adapter's `stream()`: it now converts `messages` with the same mapping as `complete()`. It used to copy each message as `{ role, content }`, so a `tool` message went out with a role the Messages API does not accept, assistant tool calls were dropped, and thinking blocks kept in `providerState` were not sent back. `stream()` still sends no tools.
182
+ - Support the new optional OpenAI Decisions API provider, `@plumbus/ai-decision-openai` 0.2.0 (#72). Register it under `decisions.providers` with a key such as `openai-decisions` (distinct from the `openai` text provider) and select it from `ctx.ai.classify()` / `ctx.ai.decide()`; the classification recipe (`instructions/ai-classification.md`) and `instructions/ai.md` show how. Core now depends on `@plumbus/ai-decision` `~0.2.3` (was `~0.2.2`), the shared version the adapter needs: it adds the transport endpoint path, adapter-declared rounding (two decimals for OpenAI), and `DecisionProviderError.refusedQuestions`. TypeSafe and Laya results are validated exactly as before, and core's decision code is unchanged.
183
+ - Bump agent wiring to v19. Every agent format (Copilot, Cursor, AGENTS.md, CLAUDE.md; inline and reference, flat and monorepo) adds a link to `node_modules/@plumbus/ai-decision-openai/instructions/README.md` next to the TypeSafe/Jev and Laya indexes, and the Cursor capability rule names OpenAI Decisions in its classification line. Nothing else changes from v18. Refresh apps with `plumbus init --patch --agent all`; `plumbus doctor` reports older wiring as stale.
184
+ - `onCapabilityError` also fires when a capability fails in the worker: flow steps, jobs, and event handlers (#71). It used to run only for HTTP routes, so background failures never reached the hook. Apps that export `onCapabilityError` from `app/server.ts` start receiving these failures without a code change, because the worker started by `plumbus dev`, `plumbus start`, and `plumbus worker` now wires it. A new optional `source` field (`'http' | 'flow' | 'job' | 'event'`) tells them apart. In the worker, `sourceIp` and `userAgent` are undefined and `userId` can be a service identity (`event-worker` for event handlers), so a hook that assumes an HTTP caller should check `source`.
185
+ - The hook stays fire-and-forget: errors it throws are logged, and event retries and dead-lettering, job status, and flow step retries and `onFlowError` are unchanged. Each failed attempt fires once, so an event handler that fails on all N delivery attempts fires N times.
186
+ - Apps with their own bootstrap pass the hook as `createWorkerPool({ onCapabilityError })`. Flow steps are reported when `stepDeps` comes from `buildStepDeps()`, which now names the capability on failures it ran (an optional `capability` field on the step result). Jobs and event handlers are reported when `createWorkerPool()` registers their consumers; apps that call `registerCapabilityConsumers()` themselves pass it the new `onCapabilityError` option. The payload type is exported as `CapabilityErrorInfo` and replaces the inline types on `ServerConfig` and `RouteGeneratorConfig`; hooks typed against the old shape still compile.
187
+ - Add standard pricing for `gpt-6.1-sol` ($2 input / $0.10 cached input / $2.50 cache writes / $10 output per MTok, doubling input and cache and 1.5× output above 272K input tokens), `claude-sonnet-5-5` ($2 / $0.10 / $2.50 five-minute cache writes / $10, flat across the 1M window), and `claude-haiku-5-5` ($0.10 / $0.01 / $0.125 / $0.50 for prompts up to 100,000 tokens; $0.50 / $0.05 / $0.625 / $2.50 above). `gpt-6-astra` now applies its published long-context premium above 272K input tokens ($20 / $2 cached / $25 cache writes / $75), which it lacked; its $1 cached-input rate is now listed explicitly (unchanged). Pricing checked against OpenAI's and Anthropic's pricing pages on 2026-10-08; the other GPT-6 and current Anthropic rows match.
188
+ - Add optional `ModelRate.longContextRates`: published rates used above `longContextThreshold` in place of the 2× input/cache and 1.5× output rule, for Haiku 5.5's 5× step at 100,000 tokens. Entries without it are priced as before.
189
+ - Price validation retries per attempt: `generateWithValidation` adds up each attempt's adapter cost, else the catalog price of that attempt's own usage, into `ValidatedResponse.cost` and the new `AIValidationError.cost`, and `generate`, `extract`, `classify`, and the streaming validation fallback record that sum. Pricing the summed usage once could cross a long-context threshold no single request crossed, such as two 60K-token Claude Haiku 5.5 attempts billed at its over-100K rates.
190
+ - OpenAI usage now carries `cacheWriteTokens` from `cache_write_tokens` for GPT-5.6 and GPT-6 models (Chat Completions, streaming, and Responses), so their cache writes cost the 1.25× input rate OpenAI bills; they were priced as ordinary input. Earlier OpenAI models have no cache-write charge and are unchanged.
191
+ - GPT-6 reasoning settings the API rejects now fail with `AIInvalidRequestError` before any I/O instead of an HTTP 400: effort `minimal` on every GPT-6 model, and `{ mode: 'disabled' }` on `gpt-6-astra` and `gpt-6.1-sol`, which have no `none` effort.
192
+ - `gpt-5.6-terra`, `gpt-5.6-luna`, `gpt-5.5`, `gpt-5.5-pro`, `gpt-5.4`, and `gpt-5.4-pro` now apply the long-context prices OpenAI's pricing page lists above 272K input tokens: 2× input, cache reads, and cache writes and 1.5× output for the whole request (`gpt-5.6-terra`: $4 input / $0.40 cached / $18 output instead of $2 / $0.20 / $12). They were priced at the short-context rates at any length.
193
+ - Anthropic adapter: Claude Fable 5.1, Mythos 5.1, Fable 5, Mythos 5, Mythos Preview, Opus 5.5, Opus 5, Opus 4.8, Opus 4.7, Sonnet 5.5, Sonnet 5, and Haiku 5.5 return HTTP 400 for any temperature other than 1, thinking on or off, so requests to them failed whenever the adapter's `temperature: 0.7` default was sent (no explicit temperature and no effort or budget reasoning). No temperature is sent to these models any more, neither the default nor one set on a call or a prompt (such as `@plumbus/chat`'s scope check at `0` or the CLI prompt template's `0.7`), as the OpenAI adapter already does for `gpt-5.5` and later. For them, `{ mode: 'disabled' }` is sent as `thinking: { type: 'between_tools' }` on Sonnet 5.5 and fails with `reasoning_disabled_unsupported` on Opus 5.5, Fable 5.1, Mythos 5.1, Fable 5, Mythos 5, and Mythos Preview, which cannot turn thinking off; `{ mode: 'budget' }` fails with `reasoning_budget_unsupported` on all of them except Mythos Preview. Requests to other Claude models, such as `claude-sonnet-4-5`, `claude-haiku-4-5`, and `claude-opus-4-6`, are sent byte for byte as before.
194
+
195
+ ## 0.7.8 — 2026-09-27
196
+
197
+ - `plumbus dev` and `plumbus start` load an `authenticationRuntime` export from `app/server.ts` (named or on the default export) and pass it to `createServer`, so apps that do not own their bootstrap can use `@plumbus/auth` browser sign-in. Without the export nothing changes. With it, capability routes authenticate through the runtime's composite authenticator (bearer first, then the session cookie) and `routeConfig.authAdapter` is the deny-all adapter, so custom routes in `onRoutesRegistered` should call `routeConfig.requestAuthenticator`.
198
+
199
+ ## 0.7.7 — 2026-09-27
200
+
201
+ - Add `field.bigint()`: a 64-bit integer column (PostgreSQL `bigint`, read and written as a JS number, safe integers only) for money in minor units and running totals. `field.number()` stays a 32-bit `integer`; test field validation rejects unsafe integers in bigint fields. Docs now state the actual column types (`field.decimal()` is floating point and must not hold money).
202
+ - Add `plumbus payments doctor [--live] [--webhook-url] [--fail-on-warning] [--json]`, `plumbus payments webhooks setup --url`, and `plumbus payments catalog sync|check [--json]` (the add-on's billing catalog at the provider; `check` exits 1 when it differs). They load the app's `app/payments/index.ts` export from the new optional `@plumbus/payments` add-on; core does not depend on it.
203
+ - Discovery registers exported collections: an exported array, or a plain object without a `name` (for example `export const paymentCapabilities = payments.capabilities`), contributes each capability, entity, event, or flow it contains, one level deep. A resource exported both alone and in a collection is registered once.
204
+ - Bump agent wiring to v18: all agent formats reference the `@plumbus/payments` and `@plumbus/payments-stripe` instruction files, and `@plumbus/auth-cognito`'s `attested-sign-in.md` (server-attested sign-in, auth-cognito 0.2.2). Refresh apps with `plumbus init --patch --agent all`.
205
+
206
+ ## 0.7.6 — 2026-09-27
207
+
208
+ - Fix the event pipeline stalling whenever audit is wired, which the worker pool does by default (#65). Since 0.7.0 `createAuditService` refuses outcomes outside `success | failure | denied`, but the outbox dispatcher and event worker still wrote `pending`, `retry` and `dead_lettered`. Every dispatch left its outbox row in `processing` and no consumer handler ran. Attempts now record no outcome (stored as `success`), `event.dispatch.failed` and `event.consumer.dead_lettered` record `failure`, and a failed dispatch keeps `retry` / `dead_lettered` in a `disposition` metadata field. Apps that rewrote these literals in `dist/` can remove that patch.
209
+ - Make dispatch and delivery audit entries best-effort: a failed audit write is logged (worker pool logger, else console) and no longer skips the consumer handler, the publish, or the dead-letter write. Capabilities running inside a consumer still record their own audit.
210
+ - Return outbox rows claimed more than five minutes ago (`claimTimeoutMs`) to `pending`, so rows left in `processing` by a stopped dispatcher, including those stranded by this bug, are published after upgrading.
211
+ - Log a failed timer-driven outbox poll instead of leaving an unhandled promise rejection.
212
+
213
+ ## 0.7.5 — 2026-09-24
214
+
215
+ - Fix GPT-6 OpenAI request compatibility: Chat Completions use `max_completion_tokens`, active/default reasoning omits unsupported sampling temperature, and tool calls with GPT-6 default reasoning use the Responses API. Explicitly disabled reasoning stays on Chat Completions and retains configured temperature.
216
+
217
+ ## 0.7.4 — 2026-09-23
218
+
219
+ - Bump agent wiring to v17 and add a packaged classification recipe covering explicit provider/model selection, defaults, thresholds, and cost recording. All agent formats link the recipe and decision provider instruction indexes; refresh existing apps with `plumbus init --patch --agent all`.
220
+
221
+ - Let `ctx.ai.classify()` select a registered text or decision provider with optional `provider`/`model`. TypeSafe/Laya classification batches probability questions and returns labels meeting `threshold` (default 0.5), sharing decision validation, security, budgets, cancellation, and one `classify` cost record. Existing calls keep their generative default.
222
+
223
+ - Audit 20 decision integration failure scenarios with 30 unit/integration/real-HTTP regression cases. Snapshot billing context and isolate ledger observers from caller results. Share one cost-record ID/timestamp between the tracker and persistence hook.
224
+
225
+ - Add `ctx.ai.decide()` over the shared decision contract, with per-call cost/usage records, tenant/actor attribution, named definitions, security checks over state and questions, budgets, and cancellation forwarding.
226
+ - Wire explicit `app/server.ts` decision registration and `app/decisions/` discovery into API and workers. Preserve decision feature flags through service wrappers and add decision responses to `mockAI`.
227
+ - Reuse `onAICostRecorded` for successful and failed decision calls, preserving unknown cost as null and retaining known billed metadata after invalid provider answers.
228
+ - Depend on `@plumbus/ai-decision@~0.2.2`; vendor adapters remain optional. Keep typecheck fixtures out of production compilation.
229
+
230
+ ## 0.7.2 — 2026-09-23
231
+
232
+ - Add `claude-opus-5-5` standard pricing: $4 input, $0.20 cached input, $5 five-minute cache writes, and $20 output per million tokens, with no long-context surcharge. Rechecked earlier Opus rates against Anthropic's pricing on 2026-09-23.
233
+
234
+ - Add standard pricing for `gpt-6-sol` ($2 input / $0.20 cached input / $10 output per MTok) and `gpt-6-luna` ($0.10 / $0.01 / $0.50), including cache writes and the full-request long-context premium above 272K input tokens. Verified against OpenAI documentation on 2026-09-23.
235
+
236
+ - Support explicit same-call final-answer schema validation with native tools and forward the schema on OpenAI Chat Completions/Responses; malformed output is not retried.
237
+ - Install alongside `@plumbus/chat@0.2.2` to use prompt-schema validation for structured custom-agent output. Existing core `0.7.x` peer ranges are unchanged.
238
+
42
239
  ## 0.7.1 — 2026-09-10
43
240
 
44
241
  ### Fixed
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # @plumbus/core
2
2
 
3
+ Release notes: [changelog](./CHANGELOG.md) and [0.8 upgrade notes](../../docs/upgrading-core-0.8.md).
4
+
3
5
  **AI-native, contract-driven TypeScript application framework.**
4
6
 
5
7
  Define your application through six composable primitives — Capabilities, Entities, Events, Flows, Prompts, and Translations — and get deny-by-default security, advisory governance, audit trails, and managed AI integration out of the box. Optional companion packages extend the surface for partner APIs, MCP agents, chat, knowledge, UI generation, and browser extensions — install only what you need.
@@ -151,6 +153,7 @@ node_modules/@plumbus/core/instructions/
151
153
  ├── flows.md # Workflow steps and retry logic
152
154
  ├── prompts.md # Prompt content, system/description, model config
153
155
  ├── ai.md # ctx.ai operations, RAG, cost tracking
156
+ ├── ai-classification.md # Provider/model selection, TypeSafe/Jev, Laya, OpenAI Decisions
154
157
  ├── translations.md # i18n catalogs and ctx.translations
155
158
  ├── security.md # Access policies and tenant isolation
156
159
  ├── governance.md # Advisory rules and compliance
@@ -164,7 +167,11 @@ node_modules/@plumbus/core/instructions/
164
167
  └── upgrading-0.5-capabilities.md # 0.5.x capability invocation migration
165
168
  ```
166
169
 
167
- Wire them up with `plumbus init --agent all`.
170
+ Wire them up with `plumbus init --agent all`. After upgrading to this 0.8 beta family (stable line: core 0.7.9+), run
171
+ `plumbus init --patch --agent all` for wiring v19 (v17 added the classification recipe;
172
+ v18 adds the payments packages and Cognito server-attested sign-in; v19 adds the OpenAI
173
+ Decisions provider). For classification, provider/model selection, or TypeSafe/Jev, Laya,
174
+ or OpenAI Decisions, start with [the classification recipe](instructions/ai-classification.md).
168
175
 
169
176
  ## Documentation
170
177