okengine 0.9.1 → 0.10.1

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 (175) hide show
  1. package/AGENTS.md +1 -1
  2. package/manifest.v1.schema.json +9 -1
  3. package/package.json +3 -3
  4. package/site/content/docs/console/runs.mdx +10 -0
  5. package/site/content/docs/console/traces.mdx +4 -0
  6. package/site/content/docs/elements/ai.mdx +54 -32
  7. package/site/content/docs/elements/channel.mdx +97 -9
  8. package/site/content/docs/elements/clock.mdx +1 -0
  9. package/site/content/docs/elements/flow.mdx +42 -8
  10. package/site/content/docs/get-started/basic-usage.mdx +5 -1
  11. package/site/content/docs/get-started/installation.mdx +9 -5
  12. package/site/content/docs/meta.json +2 -0
  13. package/site/content/docs/providers/azure-redis.mdx +85 -0
  14. package/site/content/docs/providers/cockroachdb.mdx +86 -0
  15. package/site/content/docs/providers/digitalocean-caching.mdx +88 -0
  16. package/site/content/docs/providers/dragonfly-cloud.mdx +82 -0
  17. package/site/content/docs/providers/elasticache.mdx +92 -0
  18. package/site/content/docs/providers/index.mdx +101 -0
  19. package/site/content/docs/providers/memorystore.mdx +89 -0
  20. package/site/content/docs/providers/meta.json +20 -0
  21. package/site/content/docs/providers/neon.mdx +115 -0
  22. package/site/content/docs/providers/redis-cloud.mdx +93 -0
  23. package/site/content/docs/providers/supabase.mdx +117 -0
  24. package/site/content/docs/providers/upstash.mdx +91 -0
  25. package/site/content/docs/providers/yugabytedb.mdx +95 -0
  26. package/site/content/docs/recipes/caddy.mdx +128 -0
  27. package/site/content/docs/recipes/cockroachdb.mdx +127 -0
  28. package/site/content/docs/recipes/dragonfly.mdx +125 -0
  29. package/site/content/docs/recipes/index.mdx +146 -0
  30. package/site/content/docs/recipes/llama-cpp.mdx +153 -0
  31. package/site/content/docs/recipes/mailpit.mdx +136 -0
  32. package/site/content/docs/recipes/meilisearch.mdx +139 -0
  33. package/site/content/docs/recipes/meta.json +31 -0
  34. package/site/content/docs/recipes/ollama.mdx +142 -0
  35. package/site/content/docs/recipes/openbao.mdx +141 -0
  36. package/site/content/docs/recipes/pgdog.mdx +137 -0
  37. package/site/content/docs/recipes/postgres.mdx +136 -0
  38. package/site/content/docs/recipes/redis.mdx +130 -0
  39. package/site/content/docs/recipes/rustfs.mdx +148 -0
  40. package/site/content/docs/recipes/sglang.mdx +105 -0
  41. package/site/content/docs/recipes/supabase-docker.mdx +145 -0
  42. package/site/content/docs/recipes/timescale.mdx +129 -0
  43. package/site/content/docs/recipes/traefik.mdx +136 -0
  44. package/site/content/docs/recipes/valkey.mdx +121 -0
  45. package/site/content/docs/recipes/vllm.mdx +106 -0
  46. package/site/content/docs/recipes/yugabytedb.mdx +135 -0
  47. package/site/content/docs/reference/cli.md +42 -6
  48. package/site/content/docs/reference/configuration.mdx +5 -3
  49. package/site/content/docs/reference/environment-variables.mdx +12 -12
  50. package/site/content/docs/reference/fx.mdx +38 -4
  51. package/src/cli/ai-setup/ai-setup.test.ts +32 -12
  52. package/src/cli/ai-setup/apply.ts +65 -23
  53. package/src/cli/ai-setup/catalog.ts +1316 -35
  54. package/src/cli/ai-setup/detect-ollama.ts +47 -0
  55. package/src/cli/ai-setup/index.ts +44 -7
  56. package/src/cli/ai-setup/prompts.ts +430 -486
  57. package/src/cli/ai-setup/recommend.test.ts +150 -50
  58. package/src/cli/ai-setup/recommend.ts +118 -101
  59. package/src/cli/ask-seed.test.ts +68 -0
  60. package/src/cli/ask-seed.ts +79 -0
  61. package/src/cli/db-seed-redact.test.ts +25 -0
  62. package/src/cli/db-seed.ts +36 -7
  63. package/src/cli/db.test.ts +1 -1
  64. package/src/cli/db.ts +2 -2
  65. package/src/cli/dev-app-runner.ts +6 -0
  66. package/src/cli/dev-controls.test.ts +107 -0
  67. package/src/cli/dev-controls.ts +354 -0
  68. package/src/cli/dev-schema-sync.ts +12 -4
  69. package/src/cli/dev.test.ts +50 -30
  70. package/src/cli/dev.ts +633 -66
  71. package/src/cli/docker-cli.test.ts +15 -0
  72. package/src/cli/drizzle-env.test.ts +40 -6
  73. package/src/cli/drizzle-env.ts +22 -1
  74. package/src/cli/ensure-drizzle-config.ts +6 -4
  75. package/src/cli/hero-meta.test.ts +13 -1
  76. package/src/cli/hero-meta.ts +28 -6
  77. package/src/cli/index.ts +5 -0
  78. package/src/cli/registry.ts +24 -0
  79. package/src/cli/replay.test.ts +105 -0
  80. package/src/cli/replay.ts +206 -0
  81. package/src/cli/start.ts +12 -1
  82. package/src/compiler/effects-infer.ts +37 -8
  83. package/src/compiler/extract.test.ts +125 -0
  84. package/src/compiler/extract.ts +31 -6
  85. package/src/config/index.ts +3 -3
  86. package/src/console/server/claim.ts +3 -1
  87. package/src/console/ui/dist/assets/{index-ClX0ZCe5.js → index-D-9KFce5.js} +2 -2
  88. package/src/console/ui/dist/assets/panel-overview-CW0CQEc2.js +1 -0
  89. package/src/console/ui/dist/assets/panel-runs-BxuyDI3x.js +1 -0
  90. package/src/console/ui/dist/assets/{panel-signals-Dsxdu_AR.js → panel-signals-BXofbj7v.js} +1 -1
  91. package/src/console/ui/dist/assets/{panel-store-hy7O8HOs.js → panel-store-B8mH-arU.js} +1 -1
  92. package/src/console/ui/dist/assets/{panel-traces-CBiAe5go.js → panel-traces-DSA-ZWnf.js} +1 -1
  93. package/src/console/ui/dist/index.html +1 -1
  94. package/src/console/ui/overview/slo.ts +31 -3
  95. package/src/console/ui/overview/types.ts +6 -0
  96. package/src/console/ui/runs/errors.test.ts +53 -0
  97. package/src/console/ui/runs/errors.ts +88 -0
  98. package/src/console/ui/runs/index.ts +8 -0
  99. package/src/console/ui/runs/search.ts +16 -0
  100. package/src/console/ui/shell/panels/runs/RunsPanel.tsx +72 -1
  101. package/src/docker/ai-model-status.test.ts +101 -0
  102. package/src/docker/ai-model-status.ts +276 -0
  103. package/src/docker/compose-health.test.ts +115 -0
  104. package/src/docker/compose-health.ts +234 -0
  105. package/src/docker/compose.ts +24 -8
  106. package/src/docker/derive.ts +35 -9
  107. package/src/docker/docker.test.ts +304 -12
  108. package/src/docker/helpers.ts +56 -0
  109. package/src/docker/index.ts +52 -0
  110. package/src/docker/ollama-pull.ts +232 -0
  111. package/src/docker/recipes/cockroach.ts +24 -0
  112. package/src/docker/recipes/index.ts +32 -0
  113. package/src/docker/recipes/llama-cpp.ts +273 -0
  114. package/src/docker/recipes/ollama.ts +19 -23
  115. package/src/docker/recipes/pgdog.ts +4 -1
  116. package/src/docker/recipes/postgres.ts +5 -18
  117. package/src/docker/recipes/sglang.ts +55 -0
  118. package/src/docker/recipes/supabase.ts +21 -0
  119. package/src/docker/recipes/timescale.ts +18 -0
  120. package/src/docker/recipes/vllm.ts +44 -0
  121. package/src/docker/recipes/yugabyte.ts +23 -0
  122. package/src/docker/stack-id.test.ts +22 -0
  123. package/src/docker/stack-id.ts +38 -4
  124. package/src/docker/types.ts +15 -0
  125. package/src/drivers/ai-ollama.integration.test.ts +7 -4
  126. package/src/drivers/signal-engine.ts +10 -0
  127. package/src/drivers/signal-postgres.ts +24 -2
  128. package/src/drivers/signal-types.ts +10 -0
  129. package/src/elements/channel/fallback.test.ts +91 -0
  130. package/src/elements/channel/hard-bounce.test.ts +82 -0
  131. package/src/elements/channel/injection.test.ts +153 -0
  132. package/src/elements/channel/locale-ar.test.ts +102 -0
  133. package/src/elements/channel/otp-delivery.ts +5 -2
  134. package/src/elements/channel/receipts-ledger.test.ts +100 -0
  135. package/src/elements/channel/runtime.ts +24 -1
  136. package/src/elements/channel/test-helpers.ts +60 -0
  137. package/src/elements/channel/whatsapp-compliance.test.ts +124 -0
  138. package/src/elements/store/cache.test.ts +28 -0
  139. package/src/elements/store/cache.ts +1 -1
  140. package/src/elements/store/emit-drizzle.ts +43 -9
  141. package/src/elements/store/runtime.ts +1 -1
  142. package/src/elements/store/schema-decl.test.ts +2 -0
  143. package/src/elements/store/schema-decl.ts +15 -0
  144. package/src/elements/store/seed.ts +1 -1
  145. package/src/index.ts +1 -0
  146. package/src/kernel/app.ts +131 -10
  147. package/src/kernel/boot-bind/signal.ts +6 -2
  148. package/src/kernel/boot-bind/store.ts +2 -1
  149. package/src/kernel/boot.test.ts +6 -5
  150. package/src/kernel/boot.ts +113 -10
  151. package/src/kernel/compensate.test.ts +131 -0
  152. package/src/kernel/correlation.test.ts +108 -0
  153. package/src/kernel/effect-timing.test.ts +82 -0
  154. package/src/kernel/effects-stamping.test.ts +207 -0
  155. package/src/kernel/errors.ts +11 -0
  156. package/src/kernel/flow.ts +38 -0
  157. package/src/kernel/fx-runs.test.ts +49 -0
  158. package/src/kernel/fx.test.ts +14 -0
  159. package/src/kernel/fx.ts +153 -14
  160. package/src/manifest/sql-resource.ts +41 -0
  161. package/src/manifest/types.ts +8 -2
  162. package/src/runs/collect.ts +3 -0
  163. package/src/runs/export-otlp.ts +64 -0
  164. package/src/runs/index.ts +16 -0
  165. package/src/runs/parquet.ts +9 -0
  166. package/src/runs/types.ts +5 -0
  167. package/src/runs/window.test.ts +68 -0
  168. package/src/runs/window.ts +148 -0
  169. package/src/runtime/boot-warn.test.ts +45 -0
  170. package/src/runtime/boot-warn.ts +17 -0
  171. package/src/runtime/dev-request-log.ts +5 -5
  172. package/src/term.test.ts +95 -10
  173. package/src/term.ts +369 -14
  174. package/src/console/ui/dist/assets/panel-overview-DONDzBd2.js +0 -1
  175. package/src/console/ui/dist/assets/panel-runs-D5zf-D9c.js +0 -1
package/AGENTS.md CHANGED
@@ -55,7 +55,7 @@ A direct `node:` import (or any other side-channel I/O) in element or flow code
55
55
 
56
56
  | Port | Surface |
57
57
  | -------- | -------------------- |
58
- | **6530** | App |
58
+ | **6530** | Backend |
59
59
  | **6533** | Console |
60
60
  | **6535** | MCP |
61
61
  | **6536** | Docs MCP (read-only) |
@@ -139,7 +139,15 @@
139
139
  "properties": {
140
140
  "reads": {
141
141
  "type": "array",
142
- "items": { "$ref": "#/$defs/ResourceRef" },
142
+ "items": {
143
+ "oneOf": [
144
+ { "$ref": "#/$defs/ResourceRef" },
145
+ {
146
+ "const": "runs",
147
+ "description": "Runs wide-event store via fx.runs (observability read)."
148
+ }
149
+ ]
150
+ },
143
151
  "uniqueItems": true
144
152
  },
145
153
  "writes": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okengine",
3
- "version": "0.9.1",
3
+ "version": "0.10.1",
4
4
  "description": "One law. Eight elements. Ten exports. One package. One manifest. Every backend need is derived, never added.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -99,8 +99,8 @@
99
99
  "arktype": "^2.2.3",
100
100
  "axe-core": "^4.12.1",
101
101
  "clsx": "^2.1.1",
102
- "drizzle-kit": "^1.0.0-rc.4",
103
- "drizzle-orm": "^1.0.0-rc.4",
102
+ "drizzle-kit": "1.0.0-rc.4",
103
+ "drizzle-orm": "1.0.0-rc.4",
104
104
  "drizzle-seed": "^0.3.1",
105
105
  "drizzle-zod": "^0.8.3",
106
106
  "gflows": "^1.2.1",
@@ -18,6 +18,10 @@ Answers: **wide events — one record per flow execution, queried by dimension**
18
18
  title="Wide events"
19
19
  description="Dimensions from effects — no manual instrumentation tax."
20
20
  />
21
+ <Card
22
+ title="Error patterns"
23
+ description="Failed runs in the lookback window, grouped by error code."
24
+ />
21
25
  <Card
22
26
  title="Outlier explanation"
23
27
  description="Compare all dimensions between slow and baseline populations."
@@ -32,6 +36,12 @@ Answers: **wide events — one record per flow execution, queried by dimension**
32
36
  />
33
37
  </Cards>
34
38
 
39
+ Use **Since** (`5m` · `1h` · `24h` · all time) to bound the population. Error patterns count
40
+ `error_code` occurrences in that window — the same store Traces and Overview read.
41
+
42
+ Local replay: `oke replay --request-id <runId>` re-invokes from the stored input snapshot
43
+ (dry-run when the ledger has `send` / `ask`).
44
+
35
45
  ## Catalog
36
46
 
37
47
  <Cards>
@@ -26,6 +26,10 @@ Answers: **one timeline across http → store → signal → durable steps**
26
26
  />
27
27
  </Cards>
28
28
 
29
+ Chains use `WideEvent.parentId`: producers stamp it on `fx.emit` / `fx.call`;
30
+ consumers record it on their run. Per-effect timing comes from the ledger
31
+ (`timestamp` + `duration`) — not a second instrumentation API.
32
+
29
33
  ## Catalog
30
34
 
31
35
  <Cards>
@@ -171,65 +171,87 @@ There is deliberately **no production default**: `prod` must name a driver, whic
171
171
 
172
172
  ```bash title="Terminal"
173
173
  oke ai setup
174
+ oke ai setup --provider llama-cpp --yes
174
175
  oke ai setup --provider ollama --yes
175
176
  ```
176
177
 
177
- Writes `drivers.ai`, `.env.local`, and `src/ai.ts`.
178
+ Writes `drivers.ai`, commented AI overrides in `.env.local` (so they do not
179
+ shadow `docker/.env.docker`), and `src/core/ai.ts`. Uncomment `.env.local`
180
+ lines only for a host-managed AI endpoint.
178
181
 
179
- Ollama: short quiz (use case · speed/quality · vision) → detect installed models
180
- picks that fit your **machine tier** (~4GB OS/IDE headroom). Labels say
181
- **≈16GB-class**, not “download 16GB” — pull size is usually much smaller than RAM.
182
+ **Recommended** defaults to llama.cpp (lightest local footprint). Customize
183
+ offers llama.cpp · Ollama · vLLM · SGLang · cloud providers.
182
184
 
183
- Cloud: short chat list + **Other…** for any model id. **← Back** returns a step.
184
- `create-oke` runs the same model wizard **before** install when you enable AI
185
- (customize or `--ai`). Standalone `oke ai setup` remains for existing apps.
185
+ llama.cpp and Ollama: machine banner model tier / manual (Docker Hub `ai/` vs
186
+ Ollama library). Cloud: API token model.
186
187
 
187
- ### Local Ollama
188
+ `create-oke` customize offers **AI setup** (Recommended llama.cpp · Customize ·
189
+ Off) before install; `--ai` forces the full model wizard.
188
190
 
189
- `ollama` is a local-model driver — any pulled model works. Pin `drivers.ai`;
190
- optionally pin `images.ai` for compose. Native `complete`, `stream`, and
191
- `embed` (`POST /api/embeddings`) no OpenAI shim required for local RAG.
191
+ ### Local inference (decision matrix)
192
+
193
+ | Workload | Image recipe / driver |
194
+ | -------------------------------- | ---------------------------------------------------------- |
195
+ | Single-user / dev (default) | [llama.cpp](/docs/recipes/llama-cpp) + `openai-compatible` |
196
+ | Local library UX | [Ollama](/docs/recipes/ollama) + `ollama` |
197
+ | Multi-user GPU concurrency | [vLLM](/docs/recipes/vllm) + `openai-compatible` |
198
+ | Structured / agent GPU workloads | [SGLang](/docs/recipes/sglang) + `openai-compatible` |
199
+ | Managed cloud | `anthropic` / `openai-compatible` / … (no `images.ai`) |
200
+
201
+ All local recipes **loopback-publish only**, pin explicit tags (never
202
+ `latest`), and require curated model sources — never arbitrary untrusted GGUF
203
+ (2026 GGUF-parser CVEs).
204
+
205
+ ### Default local: llama.cpp
192
206
 
193
207
  ```typescript title="oke.config.ts"
194
208
  import { defineConfig } from "okengine/config";
195
209
 
196
210
  export default defineConfig({
197
211
  drivers: {
198
- ai: { local: "ollama", docker: "ollama", prod: "anthropic" },
212
+ ai: { local: "openai-compatible", docker: "openai-compatible", prod: "anthropic" },
199
213
  },
200
214
  images: {
201
- ai: "ollama/ollama:latest",
215
+ ai: "ghcr.io/ggml-org/llama.cpp:server-b10290", // ≥ b8146; never :latest
202
216
  },
203
217
  });
204
218
  ```
205
219
 
206
- ```typescript title="src/ai.ts"
220
+ ```typescript title="src/core/ai.ts"
207
221
  export const smart = ai.model("smart", {
208
- provider: "ollama",
209
- model: "qwen3.5:9b", // balanced local-dev starting point — override freely
222
+ provider: "openai-compatible",
223
+ model: process.env.OKE_AI_MODEL ?? "smollm2", // Docker Hub ai/ id
210
224
  });
211
225
  ```
212
226
 
213
- - **Model:** set on `ai.model(…, { model })` or `OKE_AI_MODEL`. `qwen3.5:9b` ([Ollama library](https://ollama.com/library/qwen3.5); same as `qwen3.5:latest`) is a reasonable balanced starting point for local dev — not the only option. On Apple Silicon, prefer `qwen3.5:9b-mlx` for the MLX build.
214
- - **URL:** default `http://127.0.0.1:11434`. Override with `OKE_AI_URL` or `OLLAMA_HOST` (bare `host:port` is fine).
215
- - **Docker:** the image recipe pulls the configured model and writes `OKE_AI_URL` into `docker/.env.docker`.
216
- - **Fail-loud:** unreachable or unhealthy Ollama throws `OllamaUnavailableError` — never a silent mock fallback.
217
- - **PII:** `ollama` counts as on-premise for the build-time egress check (same as `mock`).
227
+ - **URL:** `http://127.0.0.1:8080/v1` (recipe writes `OKE_AI_URL`).
228
+ - **Model:** curated Docker Hub [`ai/`](https://hub.docker.com/u/ai) id via `OKE_AI_MODEL`.
229
+ - **PII:** local OpenAI-compatible endpoints count as on-premise when they are not third-party cloud.
230
+
231
+ ### Ollama (fully supported alternative)
232
+
233
+ Native `ollama` driver — `complete`, `stream`, `embed` without an OpenAI shim.
234
+ Pin `ollama/ollama:0.32.6` or newer (≥ **0.17.1**). See
235
+ [Ollama recipe](/docs/recipes/ollama).
218
236
 
219
237
  ### OpenAI-compatible endpoints (one driver)
220
238
 
221
- `openai-compatible` is the shared chat/completions client — OpenAI, Groq, Together, OpenRouter, vLLM, LM Studio, and Ollama’s `/v1` shim. You do **not** get a separate driver per vendor; only `baseUrl`, `apiKey`, `model`, and optional `headers` change.
239
+ `openai-compatible` is the shared chat/completions client — OpenAI, Groq,
240
+ Together, OpenRouter, llama.cpp, vLLM, SGLang, LM Studio, and Ollama’s `/v1`
241
+ shim. Only `baseUrl`, `apiKey`, `model`, and optional `headers` change.
222
242
 
223
- | Target | Typical `baseUrl` | Key |
224
- | ------------------ | ------------------------------------- | ----------------------------------------------------------- |
225
- | OpenAI | `https://api.openai.com/v1` (default) | required (`OPENAI_API_KEY`) |
226
- | Groq | `https://api.groq.com/openai/v1` | required |
227
- | Together | `https://api.together.xyz/v1` | required |
228
- | OpenRouter | `https://openrouter.ai/api/v1` | required; optional `HTTP-Referer` / `X-Title` via `headers` |
229
- | LM Studio / local | e.g. `http://127.0.0.1:1234/v1` | optional |
230
- | Ollama OpenAI shim | `http://127.0.0.1:11434/v1` | optional |
243
+ | Target | Typical `baseUrl` | Key |
244
+ | ------------------- | ------------------------------------- | ----------------------------------------------------------- |
245
+ | OpenAI | `https://api.openai.com/v1` (default) | required (`OPENAI_API_KEY`) |
246
+ | llama.cpp (default) | `http://127.0.0.1:8080/v1` | optional |
247
+ | vLLM / SGLang | `http://127.0.0.1:8000/v1` / `:30000` | optional |
248
+ | Groq | `https://api.groq.com/openai/v1` | required |
249
+ | Together | `https://api.together.xyz/v1` | required |
250
+ | OpenRouter | `https://openrouter.ai/api/v1` | required; optional `HTTP-Referer` / `X-Title` via `headers` |
251
+ | LM Studio / local | e.g. `http://127.0.0.1:1234/v1` | optional |
252
+ | Ollama OpenAI shim | `http://127.0.0.1:11434/v1` | optional |
231
253
 
232
- Prefer the native `ollama` driver for local Ollama (`/api/chat`). Point `openai-compatible` at Ollama `/v1` only when you need the OpenAI wire shape.
254
+ Prefer the native `ollama` driver for local Ollama (`/api/chat`).
233
255
 
234
256
  **Consequence:** a missing key against the default OpenAI cloud base fails at open; a custom `baseUrl` may omit the key. HTTP errors always throw — never a silent mock fallback.
235
257
 
@@ -248,7 +270,7 @@ The one you declared. There is no fallback or guess — if `prod` has no AI driv
248
270
  </Accordion>
249
271
  <Accordion title="oke boot: ollama driver needs OKE_AI_URL">
250
272
 
251
- Docker mode expects `OKE_AI_URL` from `docker/.env.docker` (written when `images.ai` is pinned). Locally, the default is `http://127.0.0.1:11434`. If asks fail with `OllamaUnavailableError`, confirm the server answers and that you have pulled the model you named (`ollama pull qwen3.5:9b`, or `qwen3.5:9b-mlx` on Apple Silicon, or your `OKE_AI_MODEL`). Prefer `oke ai setup` to wire driver, env, and `src/ai.ts` in one pass.
273
+ Docker mode expects `OKE_AI_URL` from `docker/.env.docker` (written when `images.ai` is pinned). Keep `OKE_AI_*` in `.env.local` commented — an active `OKE_AI_URL` there wins and shadows the compose URL. Locally, the Ollama default is `http://127.0.0.1:11434`. If asks fail with `OllamaUnavailableError`, confirm the server answers at that URL and that the model is listed under `GET /api/tags` (boot pulls via `POST /api/pull` against the container — do not rely on a host `ollama pull`, which may talk to a different install). Prefer `oke ai setup` to wire driver, commented env overrides, and `src/core/ai.ts` in one pass.
252
274
 
253
275
  </Accordion>
254
276
  <Accordion title="A prompt edit made answers worse — how do I catch that pre-merge?">
@@ -81,11 +81,42 @@ Locally the `console` driver captures mail into an inbox instead of sending; in
81
81
 
82
82
  <ChannelPhysics />
83
83
 
84
- ### Consent is checked before sending
84
+ ### Consent and hard-bounce suppression
85
85
 
86
- Opt-out is first-class: a subject who opted out of a medium is **suppressed** — the send resolves without contacting the provider, and the receipt status is `suppressed/opted-out`. You never hand-roll "did they unsubscribe?" checks.
86
+ Opt-out is first-class: a subject who opted out of a medium is **suppressed** — the send resolves without contacting the provider, and the receipt status is `suppressed/opted-out`.
87
87
 
88
- **Known limit:** default suppression, consent, and receipt stores are **process-local memory**. Opt-out / hard-bounce / receipt on replica A is invisible to replica B. Boot prints a one-shot warning when those defaults are used. Inject shared stores for multi-instance, or run a single Channel consumer, until a durable driver ships.
88
+ Hard bounces are protective too. After a provider outcome of `hard-bounce` is ingested, the address is auto-added to suppression; the **next** send to that address is blocked before any transport runs (`suppressed/prior-bounce`). Soft bounces and complaints do **not** auto-suppress.
89
+
90
+ ```typescript
91
+ // After a provider webhook (or Console ingest):
92
+ runtime.ingestOutcome({
93
+ messageId,
94
+ state: "hard-bounce",
95
+ to: user.email,
96
+ medium: "email",
97
+ });
98
+ // Next fx.send / runtime.send to that address → ok: false, driverId: "suppression"
99
+ ```
100
+
101
+ **Known limit:** default suppression, consent, and receipt stores are **process-local memory**. Opt-out / hard-bounce / receipt on replica A is invisible to replica B. Boot prints a one-shot warning when those defaults are used.
102
+
103
+ ### Inject shared stores (multi-instance workaround)
104
+
105
+ Until a durable driver ships, inject your own stores that implement the same surfaces — boot skips the process-local warn when both `suppression` and `receipts` are provided:
106
+
107
+ ```typescript
108
+ import { createConsentStore, createReceiptLedger, createSuppressionStore } from "okengine";
109
+
110
+ const consent = createConsentStore(); // or a wrapper over shared storage
111
+ const suppression = createSuppressionStore({ consent });
112
+ const receipts = createReceiptLedger();
113
+
114
+ await app.boot({
115
+ channel: { suppression, receipts /* templates, catalog, drivers… */ },
116
+ });
117
+ ```
118
+
119
+ `consent` alone is wrapped into the default suppression store when `suppression` is omitted. Prefer passing `suppression` when you share bounce + opt-out state.
89
120
 
90
121
  ### Locale resolves, then the catalog
91
122
 
@@ -97,6 +128,20 @@ Locales come from `oke.config.ts` `i18n`. Catalog bodies use `{{field}}` — not
97
128
  ICU ([i18n](/docs/reference/i18n) / `fx.t` owns flow copy). Pass `locale` /
98
129
  `profileLocale` / `acceptLanguage` on `fx.send`, or omit them for `fx.locale`.
99
130
 
131
+ ```typescript
132
+ // Catalog (template → locale → body)
133
+ {
134
+ "order-confirmed": {
135
+ en: { subject: "Order {{orderId}}", text: "Hello {{name}}" },
136
+ ar: { subject: "طلب {{orderId}}", text: "مرحبًا {{name}}" },
137
+ },
138
+ }
139
+ // locale: "ar" + data { name: "علي", orderId: "ORD-42" }
140
+ // → subject "طلب ORD-42", text "مرحبًا علي"
141
+ ```
142
+
143
+ **Consequence:** `Accept-Language: ar-SA` does **not** match a catalog key named `ar`. Provide both keys, or send `locale: "ar"`.
144
+
100
145
  ### Fallback chains are same-medium
101
146
 
102
147
  `via` orders **driver ids** for the template's medium — first success wins,
@@ -108,12 +153,42 @@ await fx.send(orderConfirmed, {
108
153
  data: { name, orderId, total },
109
154
  via: ["smtp", "resend"],
110
155
  });
111
- // smtp fails → resend tried → receipt status "fallback" with both attempts
156
+ // smtp provider-error → resend tried → receipt status "fallback"
112
157
  ```
113
158
 
114
- ### Receipts for everything
159
+ Permanent client errors do **not** advance the chain (invalid address, HTTP
160
+ 400/401/403) — the same distinction OTP cross-medium delivery uses. A provider
161
+ timeout or 5xx does failover.
162
+
163
+ ### Receipt ledger
164
+
165
+ Each send records attempts — driver, ok/error, timestamp, message id — so "did
166
+ the user get it?" is a Console query. Dry run: kernel records a would-have-fired
167
+ send and never opens a provider — no Channel receipt.
168
+
169
+ | Status | Meaning |
170
+ | --------------------------------- | --------------------------------------------- |
171
+ | `sent` | Delivered on the first successful driver |
172
+ | `fallback` | Earlier drivers failed; a later one succeeded |
173
+ | `suppressed/opted-out` | Consent blocked the send (no provider call) |
174
+ | `suppressed/prior-bounce` | Prior hard bounce blocked the send |
175
+ | `blocked/invalid-address` | Permanent client error (no useful failover) |
176
+ | `hard-bounce` / `soft-bounce` / … | Post-send outcomes via `ingestOutcome` |
177
+
178
+ ### WhatsApp: template vs session (Known gap)
179
+
180
+ WhatsApp Business allows free-form ("session") messages only inside a
181
+ customer-initiated **24h** window; outside that window you must send a
182
+ pre-approved **template**.
183
+
184
+ OKE drivers choose payload shape by whether a template name is present on the
185
+ message (`template` / `data.template` → template API; otherwise free text).
115
186
 
116
- Each send records its attempts — driver, ok/error, timestamp, message id — so "did the user actually get it?" is a Console query, not a guess. In a dry run, the kernel records a would-have-fired send and never opens a provider — no Channel receipt is written.
187
+ There is **no** session-window clock or gate on the Channel runtime today.
188
+ Sending free-form outside the window risks provider rejection or account
189
+ action — handle that yourself until a real design ships.
190
+
191
+ OTP WhatsApp paths often send free text via provider failover.
117
192
 
118
193
  ## Per-environment drivers
119
194
 
@@ -188,12 +263,22 @@ Nothing _should_ arrive — the `console` driver captures mail instead of sendin
188
263
  </Accordion>
189
264
  <Accordion title="A user says they stopped receiving messages">
190
265
 
191
- Check consent first: if they opted out, sends to them are suppressed by design. The Console shows the suppression on the receipt — it is a delivered-as-intended outcome, not a bug.
266
+ Check consent first: if they opted out, sends to them are suppressed by design. After a hard bounce, later sends are `suppressed/prior-bounce` without contacting the provider. The Console shows the suppression on the receipt — it is a delivered-as-intended outcome, not a bug.
267
+
268
+ </Accordion>
269
+ <Accordion title="Opt-out on replica A, still sending on replica B">
270
+
271
+ Default stores are process-local. Inject shared `suppression` + `receipts` on boot (see above), or run a single Channel consumer, until a durable driver ships.
272
+
273
+ </Accordion>
274
+ <Accordion title="Primary failed with invalid address and didn't try secondary">
275
+
276
+ By design. Permanent client errors (invalid address, HTTP 400/401/403) do not failover — every provider would reject the same address. Provider outages and 5xx do advance the chain.
192
277
 
193
278
  </Accordion>
194
279
  <Accordion title="The Arabic version didn't render">
195
280
 
196
- The send still succeeds: the body falls back to your default locale, then `en`, when the catalog misses that exact key. Check that an Arabic entry exists under the same tag you send (`ar` vs `ar-SA`) and that the template lists it in `locales`.
281
+ The send still succeeds: the body falls back to your default locale, then `en`, when the catalog misses that exact key. Check that an Arabic entry exists under the same tag you send (`ar` vs `ar-SA`) and that the template lists it in `locales`. Catalog interpolation is `{{field}}` only — ICU plurals live in [i18n](/docs/reference/i18n).
197
282
 
198
283
  </Accordion>
199
284
  <Accordion title="How do I ingest SNDR / Unifonic delivery webhooks?">
@@ -209,7 +294,9 @@ import {
209
294
 
210
295
  Verify the signature when the provider sends one, parse the body, then call
211
296
  `ingestOutcome` on your channel runtime with the bounce/complaint fields.
212
- Console reads the receipt ledger — never raw webhook JSON.
297
+
298
+ Hard bounce auto-adds suppression. Console reads the receipt ledger — never
299
+ raw webhook JSON.
213
300
 
214
301
  </Accordion>
215
302
  <Accordion title="How do I know which driver finally delivered?">
@@ -242,6 +329,7 @@ Confirm a visible OS notification titled `okengine webpush interop`.
242
329
  - [Console · Channels](/docs/console/channels) — receipts, attempts, suppression
243
330
  - [Environment Variables](/docs/reference/environment-variables) — Channel boot credentials
244
331
  - [Configuration](/docs/reference/configuration) — `drivers.channel.*` maps
332
+ - [i18n](/docs/reference/i18n) — ICU / `fx.t` vs Channel `{{field}}` catalogs
245
333
  - [Signal](/docs/elements/signal) — machine-to-machine messaging, the other side of the line
246
334
 
247
335
  ## Next
@@ -194,6 +194,7 @@ They are not sharing one CronStore. In-memory leases are per process. Point ever
194
194
  ## Learn more
195
195
 
196
196
  - [Flow](/docs/elements/flow) — `on(trigger, flow)` and the `fx` surface
197
+ - [fx · Runs](/docs/reference/fx#runs-observability-read) — native SLO checkers via `on(every(…))` + `fx.runs`
197
198
  - [Console · Clock](/docs/console/clock) — health numbers, pause / edit / wake early
198
199
  - [Signal](/docs/elements/signal) — reacting to events instead of time
199
200
 
@@ -71,14 +71,15 @@ curl -X POST localhost:6530/orders -d '{"sku":"SKU-1","qty":2}' -H 'content-type
71
71
 
72
72
  ## Anatomy of a Flow
73
73
 
74
- | Part | Role |
75
- | -------- | --------------------------------------------------------------- |
76
- | trigger | What starts the flow — `http`, a signal, `every`, a row change |
77
- | `in` | Input contract — validated before `do` runs; bad input is a 422 |
78
- | `out` | Output contract — the return value is checked against it |
79
- | `errors` | Typed failures — **returned** with `fx.fail`, never thrown |
80
- | `retry` | Optional whole-`do` backoff on thrown errors (same journal) |
81
- | `do` | The work — every read, write, emit, and call goes through `fx` |
74
+ | Part | Role |
75
+ | ------------ | --------------------------------------------------------------- |
76
+ | trigger | What starts the flow — `http`, a signal, `every`, a row change |
77
+ | `in` | Input contract — validated before `do` runs; bad input is a 422 |
78
+ | `out` | Output contract — the return value is checked against it |
79
+ | `errors` | Typed failures — **returned** with `fx.fail`, never thrown |
80
+ | `retry` | Optional whole-`do` backoff on thrown errors (same journal) |
81
+ | `do` | The work — every read, write, emit, and call goes through `fx` |
82
+ | `compensate` | Durable only — undo phase after terminal failure (same journal) |
82
83
 
83
84
  Failures are values, not exceptions:
84
85
 
@@ -234,6 +235,34 @@ Replay is **at-least-once** for a step whose body never persisted (crash mid-ste
234
235
 
235
236
  For flaky sub-steps, wrap the work in `fx.retry` **inside** `fx.step` so a completed charge is never retried on resume. Coarse whole-body retry is also available as `flow({ retry: { retries, delay, backoff, jitter } })` and reuses the same journal session.
236
237
 
238
+ After a **terminal** failure (`fx.fail` or exhausted retries / uncaught throw), optional
239
+ `compensate` runs under the same journal. Undo work must use **new** step names
240
+ (`undo:…`) — never rewind forward steps:
241
+
242
+ ```typescript
243
+ export const chargeOrder = flow({
244
+ durable: true,
245
+ in: OrderRef,
246
+ out: z.boolean(),
247
+ do: async ({ orderId }, fx) => {
248
+ await fx.step("reserve", () => reserve(orderId));
249
+ await fx.step("charge", () => charge(orderId));
250
+ return true;
251
+ },
252
+ compensate: async ({ input, completedSteps }, fx) => {
253
+ if (completedSteps.includes("charge")) {
254
+ await fx.step("undo:charge", () => refund(input.orderId));
255
+ }
256
+ if (completedSteps.includes("reserve")) {
257
+ await fx.step("undo:reserve", () => release(input.orderId));
258
+ }
259
+ },
260
+ });
261
+ ```
262
+
263
+ Thrown errors on the durable execute path commit the journal as `failed` (and land on Runs).
264
+ `JournalSuspend` (sleep) is park, not failure — compensate does not run.
265
+
237
266
  ## Composition is just calls
238
267
 
239
268
  Wiring is values flowing between flows — declared in code, never configured in a dashboard:
@@ -268,6 +297,11 @@ Use `fx.all` (or `fx.race`) with thunks. The first rejection aborts siblings thr
268
297
 
269
298
  Put `fx.retry` **inside** `fx.step`, not around it. Completed steps replay from the journal and never re-run. `flow({ retry })` is for coarse whole-body retries on the same session.
270
299
 
300
+ </Accordion>
301
+ <Accordion title="When does compensate run?">
302
+
303
+ Only after terminal failure for that journal attempt — not on sleep, not mid-retry, not on success. Undo bodies must be idempotent (`undo:*` steps resume like any other step). Irreversible effects (`send` / `ask`) need developer-declared undos or an explicit no-op.
304
+
271
305
  </Accordion>
272
306
  <Accordion title="How do I share logic between flows — a private function?">
273
307
 
@@ -68,11 +68,15 @@ Adopted modules become namespaces on the client:
68
68
  import { oke } from "okengine";
69
69
  import * as main from "./flows/main";
70
70
  import * as notes from "./flows/notes";
71
+ import { db } from "./core";
71
72
 
72
- export const app = oke({ name: "notes" }).adopt({ main, notes });
73
+ export const app = oke({ name: "notes", stores: [db] }).adopt({ main, notes });
73
74
  export type App = typeof app;
74
75
  ```
75
76
 
77
+ The scaffolded `src/app.ts` also registers secrets, signals, and channel
78
+ templates in the same `oke({ … })` call — no post-construction mutation.
79
+
76
80
  ```typescript title="client"
77
81
  import { createClient } from "okengine/client";
78
82
  import type { App } from "./app";
@@ -60,11 +60,15 @@ bunx create-oke@latest my-app --yes
60
60
  ```
61
61
 
62
62
  Two Notes starters: **standard** (local-first) and **advanced** (docker-ready +
63
- files/digest/AI).
63
+ files/digest/AI). Both ship a minimal GitHub Actions workflow (typecheck +
64
+ `bun test` on push/PR) so the first push has working CI.
64
65
 
65
- On a TTY: pick a template, then **recommended defaults**, **customize** (local
66
- or docker first; saved to `~/.oke/create-defaults.json`), or **reuse** matching
67
- saved settings. Project name rejects a non-empty existing path.
66
+ On a TTY: pick a template, then **recommended defaults**, **customize**, or
67
+ **reuse** matching saved settings (saved to `~/.oke/create-defaults.json`).
68
+
69
+ Customize asks local or docker first, walks facets (including `store.index` with
70
+ `none`), then **AI setup** (Recommended / Customize / Off). Project name rejects
71
+ a non-empty existing path.
68
72
 
69
73
  Non-TTY / `--yes` stay scriptable. Defaults: local SQLite · Docker/prod Postgres.
70
74
  `--sql postgres` pins SQL; `--ai` runs the model wizard before install.
@@ -88,7 +92,7 @@ Four ports come up together (mnemonic: **O·K·E = 6·5·3**):
88
92
  <Surfaces />
89
93
 
90
94
  Open `http://localhost:6533`. Claim the Console with the **claim code** printed
91
- once in the `oke dev` log, then create the first operator.
95
+ once under the `oke dev` hero, then create the first operator.
92
96
 
93
97
  Password policy matches Gate auth: **≥ 12 characters, with a letter and a
94
98
  number.** If the Console lists your flows, the install worked — **derived, not
@@ -8,6 +8,8 @@
8
8
  "plugins",
9
9
  "console",
10
10
  "deployment",
11
+ "recipes",
12
+ "providers",
11
13
  "reference",
12
14
  "ai"
13
15
  ]
@@ -0,0 +1,85 @@
1
+ ---
2
+ title: "Azure Cache for Redis"
3
+ description: "Azure-managed Redis — Access keys blade, TLS port 6380 vs 6379, Basic tier no SLA."
4
+ icon: "Zap"
5
+ source: "docs/spec/unified-theory.md"
6
+ ---
7
+
8
+ Azure Cache for Redis spans Basic → Enterprise. New caches expect TLS.
9
+ `drivers.store.kv` stays `redis`.
10
+
11
+ <Callout title="The one rule">
12
+ Build `rediss://` from **Overview** hostname + **Access keys** primary key + SSL port **6380**. Do
13
+ not use 6379 unless you explicitly allowed non-TLS.
14
+ </Callout>
15
+
16
+ ## Find credentials (current portal)
17
+
18
+ 1. Azure Portal → your **Azure Cache for Redis** resource.
19
+ 2. **Overview** → copy **Host name** (`….redis.cache.windows.net`).
20
+ 3. **Settings → Authentication** → **Access keys** tab (or **Overview → Show access
21
+ keys**) → copy **Primary** key.
22
+ 4. Ports: Overview link next to **Ports**, or docs defaults — **6380** TLS, **6379**
23
+ non-TLS (disabled by default on new caches).
24
+ 5. Enterprise tiers may use port **10000** — check Overview for that SKU.
25
+
26
+ ```bash title="process env"
27
+ REDIS_URL=rediss://:PRIMARY_KEY@my-cache.redis.cache.windows.net:6380
28
+ ```
29
+
30
+ Azure's “connection string” blade often shows StackExchange-style
31
+ `host:6380,password=…,ssl=True` — translate that to `rediss://:password@host:6380` for
32
+ oke.
33
+
34
+ ## Production guidance
35
+
36
+ | Tier | Notes |
37
+ | -------------------- | --------------------------------------- |
38
+ | Basic | Shared, **no SLA** — not for production |
39
+ | Standard | Replicated |
40
+ | Premium / Enterprise | Clustering, persistence, VNet |
41
+
42
+ Non-TLS: **Settings → Advanced settings → Allow access only via SSL = No** — avoid in
43
+ production. Entra ID auth exists on newer tiers; oke's URL driver expects access-key
44
+ auth in `REDIS_URL`.
45
+
46
+ ## Real gotcha — port 6380 vs 6379
47
+
48
+ Using `redis://…:6379` against a TLS-only cache fails. New caches disable non-TLS;
49
+ always start from **6380** + `rediss://` unless you intentionally opened 6379.
50
+
51
+ ## Troubleshooting
52
+
53
+ <Accordions>
54
+ <Accordion title="Connection reset / SSL errors on 6379">
55
+
56
+ TLS-only cache. Switch to port **6380** and `rediss://`. Confirm Advanced settings still
57
+ require SSL.
58
+
59
+ </Accordion>
60
+ <Accordion title="WRONGPASS invalid username-password pair">
61
+
62
+ Primary key rotated or secondary key pasted by mistake. Re-copy **Primary** from
63
+ **Authentication → Access keys** and update `REDIS_URL` (empty username, key as
64
+ password).
65
+
66
+ </Accordion>
67
+ </Accordions>
68
+
69
+ ## Learn more
70
+
71
+ - [Redis (image)](/docs/recipes/redis)
72
+ - [ElastiCache](/docs/providers/elasticache) · [Memorystore](/docs/providers/memorystore)
73
+ - [Store · KV](/docs/elements/store#kv)
74
+
75
+ ## Next
76
+
77
+ <Cards>
78
+ <Card title="ElastiCache" description="AWS's equivalent." href="/docs/providers/elasticache" />
79
+ <Card title="Memorystore" description="GCP's equivalent." href="/docs/providers/memorystore" />
80
+ <Card
81
+ title="CockroachDB"
82
+ description="Managed SQL-side pair."
83
+ href="/docs/providers/cockroachdb"
84
+ />
85
+ </Cards>