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.
- package/AGENTS.md +1 -1
- package/manifest.v1.schema.json +9 -1
- package/package.json +3 -3
- package/site/content/docs/console/runs.mdx +10 -0
- package/site/content/docs/console/traces.mdx +4 -0
- package/site/content/docs/elements/ai.mdx +54 -32
- package/site/content/docs/elements/channel.mdx +97 -9
- package/site/content/docs/elements/clock.mdx +1 -0
- package/site/content/docs/elements/flow.mdx +42 -8
- package/site/content/docs/get-started/basic-usage.mdx +5 -1
- package/site/content/docs/get-started/installation.mdx +9 -5
- package/site/content/docs/meta.json +2 -0
- package/site/content/docs/providers/azure-redis.mdx +85 -0
- package/site/content/docs/providers/cockroachdb.mdx +86 -0
- package/site/content/docs/providers/digitalocean-caching.mdx +88 -0
- package/site/content/docs/providers/dragonfly-cloud.mdx +82 -0
- package/site/content/docs/providers/elasticache.mdx +92 -0
- package/site/content/docs/providers/index.mdx +101 -0
- package/site/content/docs/providers/memorystore.mdx +89 -0
- package/site/content/docs/providers/meta.json +20 -0
- package/site/content/docs/providers/neon.mdx +115 -0
- package/site/content/docs/providers/redis-cloud.mdx +93 -0
- package/site/content/docs/providers/supabase.mdx +117 -0
- package/site/content/docs/providers/upstash.mdx +91 -0
- package/site/content/docs/providers/yugabytedb.mdx +95 -0
- package/site/content/docs/recipes/caddy.mdx +128 -0
- package/site/content/docs/recipes/cockroachdb.mdx +127 -0
- package/site/content/docs/recipes/dragonfly.mdx +125 -0
- package/site/content/docs/recipes/index.mdx +146 -0
- package/site/content/docs/recipes/llama-cpp.mdx +153 -0
- package/site/content/docs/recipes/mailpit.mdx +136 -0
- package/site/content/docs/recipes/meilisearch.mdx +139 -0
- package/site/content/docs/recipes/meta.json +31 -0
- package/site/content/docs/recipes/ollama.mdx +142 -0
- package/site/content/docs/recipes/openbao.mdx +141 -0
- package/site/content/docs/recipes/pgdog.mdx +137 -0
- package/site/content/docs/recipes/postgres.mdx +136 -0
- package/site/content/docs/recipes/redis.mdx +130 -0
- package/site/content/docs/recipes/rustfs.mdx +148 -0
- package/site/content/docs/recipes/sglang.mdx +105 -0
- package/site/content/docs/recipes/supabase-docker.mdx +145 -0
- package/site/content/docs/recipes/timescale.mdx +129 -0
- package/site/content/docs/recipes/traefik.mdx +136 -0
- package/site/content/docs/recipes/valkey.mdx +121 -0
- package/site/content/docs/recipes/vllm.mdx +106 -0
- package/site/content/docs/recipes/yugabytedb.mdx +135 -0
- package/site/content/docs/reference/cli.md +42 -6
- package/site/content/docs/reference/configuration.mdx +5 -3
- package/site/content/docs/reference/environment-variables.mdx +12 -12
- package/site/content/docs/reference/fx.mdx +38 -4
- package/src/cli/ai-setup/ai-setup.test.ts +32 -12
- package/src/cli/ai-setup/apply.ts +65 -23
- package/src/cli/ai-setup/catalog.ts +1316 -35
- package/src/cli/ai-setup/detect-ollama.ts +47 -0
- package/src/cli/ai-setup/index.ts +44 -7
- package/src/cli/ai-setup/prompts.ts +430 -486
- package/src/cli/ai-setup/recommend.test.ts +150 -50
- package/src/cli/ai-setup/recommend.ts +118 -101
- package/src/cli/ask-seed.test.ts +68 -0
- package/src/cli/ask-seed.ts +79 -0
- package/src/cli/db-seed-redact.test.ts +25 -0
- package/src/cli/db-seed.ts +36 -7
- package/src/cli/db.test.ts +1 -1
- package/src/cli/db.ts +2 -2
- package/src/cli/dev-app-runner.ts +6 -0
- package/src/cli/dev-controls.test.ts +107 -0
- package/src/cli/dev-controls.ts +354 -0
- package/src/cli/dev-schema-sync.ts +12 -4
- package/src/cli/dev.test.ts +50 -30
- package/src/cli/dev.ts +633 -66
- package/src/cli/docker-cli.test.ts +15 -0
- package/src/cli/drizzle-env.test.ts +40 -6
- package/src/cli/drizzle-env.ts +22 -1
- package/src/cli/ensure-drizzle-config.ts +6 -4
- package/src/cli/hero-meta.test.ts +13 -1
- package/src/cli/hero-meta.ts +28 -6
- package/src/cli/index.ts +5 -0
- package/src/cli/registry.ts +24 -0
- package/src/cli/replay.test.ts +105 -0
- package/src/cli/replay.ts +206 -0
- package/src/cli/start.ts +12 -1
- package/src/compiler/effects-infer.ts +37 -8
- package/src/compiler/extract.test.ts +125 -0
- package/src/compiler/extract.ts +31 -6
- package/src/config/index.ts +3 -3
- package/src/console/server/claim.ts +3 -1
- package/src/console/ui/dist/assets/{index-ClX0ZCe5.js → index-D-9KFce5.js} +2 -2
- package/src/console/ui/dist/assets/panel-overview-CW0CQEc2.js +1 -0
- package/src/console/ui/dist/assets/panel-runs-BxuyDI3x.js +1 -0
- package/src/console/ui/dist/assets/{panel-signals-Dsxdu_AR.js → panel-signals-BXofbj7v.js} +1 -1
- package/src/console/ui/dist/assets/{panel-store-hy7O8HOs.js → panel-store-B8mH-arU.js} +1 -1
- package/src/console/ui/dist/assets/{panel-traces-CBiAe5go.js → panel-traces-DSA-ZWnf.js} +1 -1
- package/src/console/ui/dist/index.html +1 -1
- package/src/console/ui/overview/slo.ts +31 -3
- package/src/console/ui/overview/types.ts +6 -0
- package/src/console/ui/runs/errors.test.ts +53 -0
- package/src/console/ui/runs/errors.ts +88 -0
- package/src/console/ui/runs/index.ts +8 -0
- package/src/console/ui/runs/search.ts +16 -0
- package/src/console/ui/shell/panels/runs/RunsPanel.tsx +72 -1
- package/src/docker/ai-model-status.test.ts +101 -0
- package/src/docker/ai-model-status.ts +276 -0
- package/src/docker/compose-health.test.ts +115 -0
- package/src/docker/compose-health.ts +234 -0
- package/src/docker/compose.ts +24 -8
- package/src/docker/derive.ts +35 -9
- package/src/docker/docker.test.ts +304 -12
- package/src/docker/helpers.ts +56 -0
- package/src/docker/index.ts +52 -0
- package/src/docker/ollama-pull.ts +232 -0
- package/src/docker/recipes/cockroach.ts +24 -0
- package/src/docker/recipes/index.ts +32 -0
- package/src/docker/recipes/llama-cpp.ts +273 -0
- package/src/docker/recipes/ollama.ts +19 -23
- package/src/docker/recipes/pgdog.ts +4 -1
- package/src/docker/recipes/postgres.ts +5 -18
- package/src/docker/recipes/sglang.ts +55 -0
- package/src/docker/recipes/supabase.ts +21 -0
- package/src/docker/recipes/timescale.ts +18 -0
- package/src/docker/recipes/vllm.ts +44 -0
- package/src/docker/recipes/yugabyte.ts +23 -0
- package/src/docker/stack-id.test.ts +22 -0
- package/src/docker/stack-id.ts +38 -4
- package/src/docker/types.ts +15 -0
- package/src/drivers/ai-ollama.integration.test.ts +7 -4
- package/src/drivers/signal-engine.ts +10 -0
- package/src/drivers/signal-postgres.ts +24 -2
- package/src/drivers/signal-types.ts +10 -0
- package/src/elements/channel/fallback.test.ts +91 -0
- package/src/elements/channel/hard-bounce.test.ts +82 -0
- package/src/elements/channel/injection.test.ts +153 -0
- package/src/elements/channel/locale-ar.test.ts +102 -0
- package/src/elements/channel/otp-delivery.ts +5 -2
- package/src/elements/channel/receipts-ledger.test.ts +100 -0
- package/src/elements/channel/runtime.ts +24 -1
- package/src/elements/channel/test-helpers.ts +60 -0
- package/src/elements/channel/whatsapp-compliance.test.ts +124 -0
- package/src/elements/store/cache.test.ts +28 -0
- package/src/elements/store/cache.ts +1 -1
- package/src/elements/store/emit-drizzle.ts +43 -9
- package/src/elements/store/runtime.ts +1 -1
- package/src/elements/store/schema-decl.test.ts +2 -0
- package/src/elements/store/schema-decl.ts +15 -0
- package/src/elements/store/seed.ts +1 -1
- package/src/index.ts +1 -0
- package/src/kernel/app.ts +131 -10
- package/src/kernel/boot-bind/signal.ts +6 -2
- package/src/kernel/boot-bind/store.ts +2 -1
- package/src/kernel/boot.test.ts +6 -5
- package/src/kernel/boot.ts +113 -10
- package/src/kernel/compensate.test.ts +131 -0
- package/src/kernel/correlation.test.ts +108 -0
- package/src/kernel/effect-timing.test.ts +82 -0
- package/src/kernel/effects-stamping.test.ts +207 -0
- package/src/kernel/errors.ts +11 -0
- package/src/kernel/flow.ts +38 -0
- package/src/kernel/fx-runs.test.ts +49 -0
- package/src/kernel/fx.test.ts +14 -0
- package/src/kernel/fx.ts +153 -14
- package/src/manifest/sql-resource.ts +41 -0
- package/src/manifest/types.ts +8 -2
- package/src/runs/collect.ts +3 -0
- package/src/runs/export-otlp.ts +64 -0
- package/src/runs/index.ts +16 -0
- package/src/runs/parquet.ts +9 -0
- package/src/runs/types.ts +5 -0
- package/src/runs/window.test.ts +68 -0
- package/src/runs/window.ts +148 -0
- package/src/runtime/boot-warn.test.ts +45 -0
- package/src/runtime/boot-warn.ts +17 -0
- package/src/runtime/dev-request-log.ts +5 -5
- package/src/term.test.ts +95 -10
- package/src/term.ts +369 -14
- package/src/console/ui/dist/assets/panel-overview-DONDzBd2.js +0 -1
- 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** |
|
|
58
|
+
| **6530** | Backend |
|
|
59
59
|
| **6533** | Console |
|
|
60
60
|
| **6535** | MCP |
|
|
61
61
|
| **6536** | Docs MCP (read-only) |
|
package/manifest.v1.schema.json
CHANGED
|
@@ -139,7 +139,15 @@
|
|
|
139
139
|
"properties": {
|
|
140
140
|
"reads": {
|
|
141
141
|
"type": "array",
|
|
142
|
-
"items": {
|
|
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.
|
|
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": "
|
|
103
|
-
"drizzle-orm": "
|
|
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
|
|
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
|
-
|
|
180
|
-
|
|
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
|
-
|
|
184
|
-
|
|
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
|
-
|
|
188
|
+
`create-oke` customize offers **AI setup** (Recommended llama.cpp · Customize ·
|
|
189
|
+
Off) before install; `--ai` forces the full model wizard.
|
|
188
190
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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: "
|
|
212
|
+
ai: { local: "openai-compatible", docker: "openai-compatible", prod: "anthropic" },
|
|
199
213
|
},
|
|
200
214
|
images: {
|
|
201
|
-
ai: "
|
|
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: "
|
|
209
|
-
model:
|
|
222
|
+
provider: "openai-compatible",
|
|
223
|
+
model: process.env.OKE_AI_MODEL ?? "smollm2", // Docker Hub ai/ id
|
|
210
224
|
});
|
|
211
225
|
```
|
|
212
226
|
|
|
213
|
-
- **
|
|
214
|
-
- **
|
|
215
|
-
- **
|
|
216
|
-
|
|
217
|
-
|
|
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,
|
|
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
|
|
224
|
-
|
|
|
225
|
-
| OpenAI
|
|
226
|
-
|
|
|
227
|
-
|
|
|
228
|
-
|
|
|
229
|
-
|
|
|
230
|
-
|
|
|
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`).
|
|
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
|
|
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
|
|
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`.
|
|
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
|
-
|
|
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
|
|
156
|
+
// smtp provider-error → resend tried → receipt status "fallback"
|
|
112
157
|
```
|
|
113
158
|
|
|
114
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
75
|
-
|
|
|
76
|
-
| trigger
|
|
77
|
-
| `in`
|
|
78
|
-
| `out`
|
|
79
|
-
| `errors`
|
|
80
|
-
| `retry`
|
|
81
|
-
| `do`
|
|
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
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
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
|
|
@@ -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>
|