okengine 0.3.4 → 0.3.6

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 (98) hide show
  1. package/AGENTS.md +4 -0
  2. package/manifest.v1.schema.json +43 -1
  3. package/package.json +16 -2
  4. package/site/content/docs/ai/skills.mdx +9 -7
  5. package/site/content/docs/console/vault.mdx +4 -0
  6. package/site/content/docs/elements/channel.mdx +5 -4
  7. package/site/content/docs/elements/clock.mdx +1 -0
  8. package/site/content/docs/elements/flow.mdx +15 -0
  9. package/site/content/docs/elements/gate.mdx +16 -11
  10. package/site/content/docs/elements/signal.mdx +8 -7
  11. package/site/content/docs/elements/store.mdx +48 -8
  12. package/site/content/docs/elements/vault.mdx +1 -1
  13. package/site/content/docs/plugins/ip-allowlist.mdx +2 -2
  14. package/site/content/docs/reference/configuration.mdx +2 -2
  15. package/site/content/docs/reference/environment-variables.mdx +8 -0
  16. package/site/content/docs/reference/fx.mdx +40 -0
  17. package/site/content/docs/reference/plugins.mdx +18 -18
  18. package/src/compiler/extract.test.ts +63 -0
  19. package/src/compiler/extract.ts +36 -15
  20. package/src/console/server/channels.ts +2 -0
  21. package/src/console/server/clock.ts +3 -0
  22. package/src/console/server/flows.ts +13 -0
  23. package/src/console/server/gates.ts +2 -0
  24. package/src/console/server/plugins.ts +20 -1
  25. package/src/console/server/signals.ts +5 -0
  26. package/src/console/server/store.test.ts +17 -0
  27. package/src/console/server/store.ts +24 -0
  28. package/src/console/ui/channels/types.ts +1 -0
  29. package/src/console/ui/clock/types.ts +1 -0
  30. package/src/console/ui/display.test.ts +14 -0
  31. package/src/console/ui/display.ts +9 -0
  32. package/src/console/ui/dist/assets/{index-BWo8R7NR.js → index-CrKMmO__.js} +2 -2
  33. package/src/console/ui/dist/assets/panel-channels-DCDd4WAC.js +1 -0
  34. package/src/console/ui/dist/assets/panel-clock-DjGGFPzr.js +1 -0
  35. package/src/console/ui/dist/assets/panel-gates-B5eTE8XH.js +1 -0
  36. package/src/console/ui/dist/assets/{panel-overview-BznEOTnb.js → panel-overview-BsFvDdts.js} +1 -1
  37. package/src/console/ui/dist/assets/panel-plugins-Cj7DK1er.js +1 -0
  38. package/src/console/ui/dist/assets/{panel-runs-CGWNHLR4.js → panel-runs-C0gmnoYL.js} +1 -1
  39. package/src/console/ui/dist/assets/panel-signals-whmDXIg3.js +1 -0
  40. package/src/console/ui/dist/assets/panel-store-CEMHLvaw.js +1 -0
  41. package/src/console/ui/dist/assets/{panel-traces-DBLx2ilD.js → panel-traces-BDiAuVSK.js} +1 -1
  42. package/src/console/ui/dist/assets/panel-vault-C9wjbki8.js +1 -0
  43. package/src/console/ui/dist/index.html +1 -1
  44. package/src/console/ui/gates/types.ts +1 -0
  45. package/src/console/ui/plugins/fixture.ts +7 -0
  46. package/src/console/ui/plugins/types.ts +3 -0
  47. package/src/console/ui/shell/client.ts +3 -0
  48. package/src/console/ui/shell/panels/channels/ChannelsPanel.tsx +7 -2
  49. package/src/console/ui/shell/panels/clock/ClockPanel.tsx +9 -2
  50. package/src/console/ui/shell/panels/gates/GatesPanel.tsx +12 -5
  51. package/src/console/ui/shell/panels/plugins/PluginsPanel.tsx +18 -4
  52. package/src/console/ui/shell/panels/signals/SignalsPanel.tsx +13 -2
  53. package/src/console/ui/shell/panels/store/StorePanel.tsx +11 -4
  54. package/src/console/ui/shell/panels/vault/VaultPanel.tsx +9 -3
  55. package/src/console/ui/signals/types.ts +1 -0
  56. package/src/console/ui/store/fixture.ts +5 -0
  57. package/src/console/ui/store/types.ts +2 -0
  58. package/src/drivers/conformance.test.ts +11 -0
  59. package/src/drivers/drizzle-dialect.test.ts +4 -0
  60. package/src/drivers/drizzle-dialect.ts +8 -4
  61. package/src/drivers/index.ts +4 -0
  62. package/src/drivers/libsql.ts +179 -0
  63. package/src/drivers/pglite.ts +79 -0
  64. package/src/drivers/pgvector.ts +54 -19
  65. package/src/drivers/types.ts +16 -7
  66. package/src/elements/channel/declare.ts +5 -0
  67. package/src/elements/clock/declare.ts +5 -0
  68. package/src/elements/clock/durable.ts +7 -1
  69. package/src/elements/gate/declare.ts +28 -5
  70. package/src/elements/gate.ts +1 -0
  71. package/src/elements/signal/declare.ts +5 -0
  72. package/src/elements/store/declare.ts +10 -2
  73. package/src/elements/store/index-boot.test.ts +257 -0
  74. package/src/elements/store/runtime.ts +67 -9
  75. package/src/elements/store/schema-decl.ts +7 -0
  76. package/src/kernel/abort-scope.ts +116 -0
  77. package/src/kernel/app.ts +12 -2
  78. package/src/kernel/boot-bind/store.test.ts +59 -1
  79. package/src/kernel/boot-bind/store.ts +64 -2
  80. package/src/kernel/concurrency.test.ts +236 -0
  81. package/src/kernel/concurrency.ts +172 -0
  82. package/src/kernel/flow.ts +9 -0
  83. package/src/kernel/fx.test.ts +12 -0
  84. package/src/kernel/fx.ts +44 -0
  85. package/src/kernel/index.ts +14 -0
  86. package/src/kernel/journal.ts +9 -0
  87. package/src/kernel/plugin/capabilities.test.ts +18 -0
  88. package/src/kernel/plugin.ts +11 -3
  89. package/src/kernel/registry.ts +29 -6
  90. package/src/manifest/types.ts +21 -0
  91. package/src/release/measure.ts +4 -0
  92. package/src/console/ui/dist/assets/panel-channels-BOmQ-onL.js +0 -1
  93. package/src/console/ui/dist/assets/panel-clock-giAq0Ccv.js +0 -1
  94. package/src/console/ui/dist/assets/panel-gates-XclZxWD5.js +0 -1
  95. package/src/console/ui/dist/assets/panel-plugins-CcGM1g64.js +0 -1
  96. package/src/console/ui/dist/assets/panel-signals-CNywkdak.js +0 -1
  97. package/src/console/ui/dist/assets/panel-store-KmTbFHMH.js +0 -1
  98. package/src/console/ui/dist/assets/panel-vault-CEnFc0dk.js +0 -1
package/AGENTS.md CHANGED
@@ -92,3 +92,7 @@ Engine: Bun `>=1.3`.
92
92
  | Manifest | `manifest.v1.schema.json` |
93
93
 
94
94
  **If the documentation is silent, stop and ask.**
95
+
96
+ ## After every implementation
97
+
98
+ Before claiming work done: run [`.agents/skills/oke-ship`](.agents/skills/oke-ship/SKILL.md) — append notes to `changelog.md` under `## Unreleased` (never under a shipped `## v…` section), and update site docs via [`.agents/skills/oke-docs`](.agents/skills/oke-docs/SKILL.md) for any user-facing surface. Version bump is separate: `bun run bump` promotes Unreleased into the next `## vX.Y.Z`.
@@ -321,6 +321,10 @@
321
321
  "type": "string",
322
322
  "enum": ["once", "broadcast", "live"]
323
323
  },
324
+ "description": {
325
+ "description": "Optional human description; consumers fall back to the signal map key.",
326
+ "type": "string"
327
+ },
324
328
  "retries": { "type": "integer", "minimum": 0 },
325
329
  "deadLetter": { "type": "boolean" },
326
330
  "schema": { "$ref": "#/$defs/JsonSchema" },
@@ -354,6 +358,10 @@
354
358
  ]
355
359
  },
356
360
  "sqlName": { "type": "string", "minLength": 1 },
361
+ "description": {
362
+ "description": "Optional human description; consumers fall back to the column map key.",
363
+ "type": "string"
364
+ },
357
365
  "pii": { "type": "boolean" },
358
366
  "sensitive": { "type": "boolean" },
359
367
  "retain": { "type": "string", "minLength": 1 },
@@ -408,6 +416,10 @@
408
416
  "type": "string",
409
417
  "enum": ["sql", "kv", "files", "index"]
410
418
  },
419
+ "description": {
420
+ "description": "Optional human description; consumers fall back to the store map key.",
421
+ "type": "string"
422
+ },
411
423
  "tables": {
412
424
  "type": "object",
413
425
  "propertyNames": { "type": "string", "minLength": 1 },
@@ -451,7 +463,11 @@
451
463
  "cron": { "type": "string", "minLength": 1 },
452
464
  "every": { "type": "string", "minLength": 1 },
453
465
  "timezone": { "type": "string", "minLength": 1 },
454
- "overridable": { "type": "boolean" }
466
+ "overridable": { "type": "boolean" },
467
+ "description": {
468
+ "description": "Optional human description; consumers fall back to the clock map key.",
469
+ "type": "string"
470
+ }
455
471
  }
456
472
  },
457
473
  "Gate": {
@@ -485,6 +501,10 @@
485
501
  "type": "array",
486
502
  "items": { "type": "string", "minLength": 1 },
487
503
  "uniqueItems": true
504
+ },
505
+ "description": {
506
+ "description": "Optional human description; consumers fall back to the gate map key.",
507
+ "type": "string"
488
508
  }
489
509
  }
490
510
  },
@@ -502,6 +522,10 @@
502
522
  "type": "object",
503
523
  "additionalProperties": false,
504
524
  "properties": {
525
+ "description": {
526
+ "description": "Optional human description; consumers fall back to the channel map key.",
527
+ "type": "string"
528
+ },
505
529
  "medium": {
506
530
  "type": "string",
507
531
  "enum": ["email", "sms", "whatsapp", "push", "any"]
@@ -580,6 +604,18 @@
580
604
  }
581
605
  }
582
606
  },
607
+ "PluginTable": {
608
+ "description": "Optional metadata for a plugin-contributed table.",
609
+ "type": "object",
610
+ "additionalProperties": false,
611
+ "properties": {
612
+ "plane": { "type": "string", "minLength": 1 },
613
+ "description": {
614
+ "description": "Optional human description; consumers fall back to the table name.",
615
+ "type": "string"
616
+ }
617
+ }
618
+ },
583
619
  "Plugin": {
584
620
  "type": "object",
585
621
  "additionalProperties": false,
@@ -600,6 +636,12 @@
600
636
  "type": "array",
601
637
  "items": { "type": "string", "minLength": 1 },
602
638
  "uniqueItems": true
639
+ },
640
+ "tables": {
641
+ "description": "Optional metadata for table:* contributions.",
642
+ "type": "object",
643
+ "propertyNames": { "type": "string", "minLength": 1 },
644
+ "additionalProperties": { "$ref": "#/$defs/PluginTable" }
603
645
  }
604
646
  }
605
647
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okengine",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
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": {
@@ -59,7 +59,6 @@
59
59
  "gate": "bun test src/cli/doc-staleness.test.ts src/drivers/vault-driver-removal.test.ts src/kernel/errors.registry.test.ts src/upgrade/codemods.test.ts",
60
60
  "dev": "bun run --cwd site dev",
61
61
  "site:build": "bun run --cwd site build",
62
- "ci:workflow": "bun scripts/ci.ts --workflow",
63
62
  "ci": "bun scripts/ci.ts",
64
63
  "bump": "bun run scripts/bump-version.ts",
65
64
  "release": "bun run scripts/publish.ts",
@@ -80,6 +79,9 @@
80
79
  "@codemirror/state": "^6.7.1",
81
80
  "@codemirror/theme-one-dark": "^6.1.3",
82
81
  "@codemirror/view": "^6.43.6",
82
+ "@electric-sql/pglite": "^0.5.4",
83
+ "@electric-sql/pglite-pgvector": "^0.0.5",
84
+ "@libsql/client": "^0.17.4",
83
85
  "@oxc-project/types": "^0.141.0",
84
86
  "@playwright/test": "^1.62.0",
85
87
  "@sinclair/typebox": "^0.34.52",
@@ -113,12 +115,24 @@
113
115
  "zod": "^4.4.3"
114
116
  },
115
117
  "peerDependencies": {
118
+ "@electric-sql/pglite": ">=0.5.0",
119
+ "@electric-sql/pglite-pgvector": ">=0.0.5",
120
+ "@libsql/client": ">=0.15.0",
116
121
  "drizzle-kit": ">=1.0.0-rc.0",
117
122
  "drizzle-orm": ">=1.0.0-rc.0",
118
123
  "drizzle-seed": ">=0.3.0",
119
124
  "zod": ">=3.23.0"
120
125
  },
121
126
  "peerDependenciesMeta": {
127
+ "@electric-sql/pglite": {
128
+ "optional": true
129
+ },
130
+ "@electric-sql/pglite-pgvector": {
131
+ "optional": true
132
+ },
133
+ "@libsql/client": {
134
+ "optional": true
135
+ },
122
136
  "drizzle-kit": {
123
137
  "optional": true
124
138
  },
@@ -7,13 +7,14 @@ source: "docs/spec/unified-theory.md"
7
7
 
8
8
  Tools alone don't make a good operator — an agent also needs to know the _vocabulary_: what a Flow is, why `fx` is the only door, which driver ids are legal. OKE ships that knowledge as contracts and skills that agents load automatically, so sessions start aligned instead of drifting and getting corrected.
9
9
 
10
- ## The three layers
10
+ ## The four layers
11
11
 
12
12
  | Layer | Path | Loaded when | Teaches |
13
13
  | ------------------ | -------------------------- | ------------------------------------------------- | --------------------------------------------------------------------- |
14
14
  | **Agent contract** | `AGENTS.md` (repo root) | Every agent session, automatically | The one law, eight elements, ten exports, the fx rule, ports, budgets |
15
15
  | **Element skill** | `.agents/skills/oke/` | Building or changing an okengine app | The element contract in depth — declaration patterns per element |
16
16
  | **Docs skill** | `.agents/skills/oke-docs/` | Writing or editing docs under `site/content/docs` | The documentation information-architecture standard and its gates |
17
+ | **Ship skill** | `.agents/skills/oke-ship/` | After any implementation, before claiming done | Changelog under the current version + docs sync via `oke-docs` |
17
18
 
18
19
  ## AGENTS.md — the root contract
19
20
 
@@ -21,14 +22,15 @@ Every OKE app's repo carries an `AGENTS.md` that agents (Cursor, Claude Code, an
21
22
 
22
23
  ## Skills — installable know-how
23
24
 
24
- Skills are `SKILL.md` packages an agent loads when the work matches their description. The two OKE ships:
25
+ Skills are `SKILL.md` packages an agent loads when the work matches their description. The three OKE ships:
25
26
 
26
- | Skill | Use it for | Inside |
27
- | ---------- | ----------------------------------- | --------------------------------------------------------- |
28
- | `oke` | App work — flows, elements, drivers | The agent contract, element patterns, the fx invariants |
29
- | `oke-docs` | Docs work — new pages, rewrites | The page skeleton, verification sources, the density gate |
27
+ | Skill | Use it for | Inside |
28
+ | ---------- | -------------------------------------------- | ------------------------------------------------------------------------- |
29
+ | `oke` | App work — flows, elements, drivers | The agent contract, element patterns, the fx invariants |
30
+ | `oke-docs` | Docs work — new pages, rewrites | The page skeleton, verification sources, the density gate |
31
+ | `oke-ship` | Closing an implementation — changelog + docs | Append under `## Unreleased`; `bun run bump` promotes it into `## vX.Y.Z` |
30
32
 
31
- Both live in the repo under `.agents/skills/`, so they travel with the code and stay versioned with what they describe.
33
+ All live in the repo under `.agents/skills/`, so they travel with the code and stay versioned with what they describe.
32
34
 
33
35
  ## How contracts compose with MCP
34
36
 
@@ -14,6 +14,10 @@ Answers: **secret contracts, who can read each, rotation due**
14
14
  ## What this panel shows
15
15
 
16
16
  <Cards>
17
+ <Card
18
+ title="Contracts"
19
+ description="Optional description as the human title; technical key stays visible."
20
+ />
17
21
  <Card
18
22
  title="Fingerprints"
19
23
  description="Salted hash per environment — rotation and drift without exposure."
@@ -71,10 +71,11 @@ Locally the `console` driver captures mail into an inbox instead of sending; in
71
71
  | `binder.template(name, opts)` | Typed template bound to that medium |
72
72
  | `channel.template(name, opts)` | Medium-agnostic template (one body, any medium) |
73
73
 
74
- | Template option | Type | Meaning |
75
- | --------------- | --------------------- | ------------------------------------------------- |
76
- | `schema` | zod / Standard Schema | The data the template may reference — typed sends |
77
- | `locales` | string[] | Languages this template is rendered in |
74
+ | Template option | Type | Meaning |
75
+ | --------------- | --------------------- | ---------------------------------------------------------- |
76
+ | `description` | string | Human title in the Console (falls back to the template id) |
77
+ | `schema` | zod / Standard Schema | The data the template may reference — typed sends |
78
+ | `locales` | string[] | Languages this template is rendered in |
78
79
 
79
80
  ## The human physics
80
81
 
@@ -98,6 +98,7 @@ Both are triggers consumed with the same `on(trigger, flow)` — the flow undern
98
98
  | `every` | string | — | Fixed interval: `"30s"` · `"10m"` · `"1h"` · `"7d"` |
99
99
  | `timezone` | string | `"UTC"` | IANA timezone for cron evaluation |
100
100
  | `overridable` | boolean | `false` | Allow the Console to edit / pause the schedule |
101
+ | `description` | string | — | Human title in the Console (falls back to the clock name) |
101
102
 
102
103
  ## Sleeping inside a flow
103
104
 
@@ -77,6 +77,7 @@ curl -X POST localhost:6530/orders -d '{"sku":"SKU-1","qty":2}' -H 'content-type
77
77
  | `in` | Input contract — validated before `do` runs; bad input is a 422 |
78
78
  | `out` | Output contract — the return value is checked against it |
79
79
  | `errors` | Typed failures — **returned** with `fx.fail`, never thrown |
80
+ | `retry` | Optional whole-`do` backoff on thrown errors (same journal) |
80
81
  | `do` | The work — every read, write, emit, and call goes through `fx` |
81
82
 
82
83
  Failures are values, not exceptions:
@@ -195,6 +196,8 @@ Everything a flow may touch, on one object:
195
196
  | `fx.clock.now()` / `.sleep(…)` | — | Injected time / durable sleep |
196
197
  | `fx.cache.get/set` | — | Shared cache with effect-aware invalidation |
197
198
  | `fx.step(name, fn)` | — | Named durable step — never re-runs on replay |
199
+ | `fx.all` / `fx.race` / `fx.retry` | — | Structured concurrency + backoff retry |
200
+ | `fx.signal` | — | Ambient `AbortSignal` for the current branch |
198
201
  | `fx.id()` · `fx.log` · `fx.t` | — | UUIDs, redacting logger, i18n |
199
202
  | `fx.auth` · `fx.operator` · `fx.tenant` | — | Who is calling (user / operator / tenant) |
200
203
 
@@ -227,6 +230,8 @@ export const chargeOrder = flow({
227
230
 
228
231
  **Consequence:** kill the process between the two steps and the run **resumes at `confirm`** — completed steps replay from the journal, so the card is not charged twice. This is verified by the engine's own test suite: after resume, `create-intent` has run exactly once.
229
232
 
233
+ 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.
234
+
230
235
  ## Composition is just calls
231
236
 
232
237
  Wiring is values flowing between flows — declared in code, never configured in a dashboard:
@@ -251,6 +256,16 @@ That bypasses the effect graph: the Manifest can't see the call, cache keys miss
251
256
 
252
257
  `fx.fail(code, data)` for expected outcomes — anything in `errors`. A throw is a crash: the run fails with a 500-shaped error and no typed code for the client. Expected failures are values so clients can switch on `error.code`.
253
258
 
259
+ </Accordion>
260
+ <Accordion title="Promise.all left orphan work running after one branch failed">
261
+
262
+ Use `fx.all` (or `fx.race`) with thunks. The first rejection aborts siblings through `fx.signal`. Bare `Promise.all` / `Promise.race` do not cancel losers.
263
+
264
+ </Accordion>
265
+ <Accordion title="Durable flow retried a completed charge">
266
+
267
+ 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.
268
+
254
269
  </Accordion>
255
270
  <Accordion title="How do I share logic between flows — a private function?">
256
271
 
@@ -74,10 +74,14 @@ A denied request never reaches `do`. It returns one of three typed failures, lik
74
74
 
75
75
  ## Two kinds of gates
76
76
 
77
- | Declaration | Question it answers | Evaluated against |
78
- | -------------------------- | -------------------------------------- | ---------------------------------- |
79
- | `gate.policy(name, check)` | Is this principal allowed? (ABAC) | auth / operator / request metadata |
80
- | `gate.rate(options)` | Is there budget left for this subject? | an atomic counter on the kv driver |
77
+ | Declaration | Question it answers | Evaluated against |
78
+ | ------------------------------------- | -------------------------------------- | ---------------------------------- |
79
+ | `gate.policy(name, check \| options)` | Is this principal allowed? (ABAC) | auth / operator / request metadata |
80
+ | `gate.rate(options)` | Is there budget left for this subject? | an atomic counter on the kv driver |
81
+
82
+ `gate.policy(name, check)` stays the terse form. Pass
83
+ `{ check, description }` when you want a human title in the Console
84
+ (falls back to the policy name).
81
85
 
82
86
  ### The policy context
83
87
 
@@ -97,13 +101,14 @@ A policy name containing `:` is also a `Module:Action` permission — the same d
97
101
 
98
102
  ### Rate options
99
103
 
100
- | Option | Type | Default | Meaning |
101
- | ------------- | ------------ | -------------------------- | -------------------------------------------- |
102
- | `max` | number | — (**required**) | Takes allowed within `per` |
103
- | `per` | string | — (**required**) | Window / refill period (`"1m"`, `"60s"`, …) |
104
- | `keyBy` | string | — | Subject dimension (`"ip"`, `"user"`, …) |
105
- | `strategy` | RateStrategy | `"sliding-window-counter"` | Algorithm (below) |
106
- | `overridable` | boolean | `false` | Allow the Console to retune `max`/`per` live |
104
+ | Option | Type | Default | Meaning |
105
+ | ------------- | ------------ | -------------------------- | -------------------------------------------------------- |
106
+ | `max` | number | — (**required**) | Takes allowed within `per` |
107
+ | `per` | string | — (**required**) | Window / refill period (`"1m"`, `"60s"`, …) |
108
+ | `keyBy` | string | — | Subject dimension (`"ip"`, `"user"`, …) |
109
+ | `strategy` | RateStrategy | `"sliding-window-counter"` | Algorithm (below) |
110
+ | `overridable` | boolean | `false` | Allow the Console to retune `max`/`per` live |
111
+ | `description` | string | — | Human title in the Console (falls back to the rate name) |
107
112
 
108
113
  | Strategy | Behavior |
109
114
  | ------------------------ | ------------------------------------------------------ |
@@ -86,13 +86,14 @@ The declaration is identical in shape for all three — switching physics later
86
86
 
87
87
  ### Options
88
88
 
89
- | Option | Type | Default | Meaning |
90
- | ------------ | --------------------------------- | ---------------- | -------------------------------------------------------------- |
91
- | `delivery` | `"once" \| "broadcast" \| "live"` | — (**required**) | Delivery physics |
92
- | `schema` | zod / Standard Schema | — | Payload contract; typed emits and Manifest docs |
93
- | `retries` | number | `3` | Max delivery attempts before dead-letter (`once`) |
94
- | `deadLetter` | boolean | `true` | Preserve exhausted messages in the DLQ (`once`) |
95
- | `optional` | boolean | `false` | Allow emitting while nobody subscribes (skip the orphan check) |
89
+ | Option | Type | Default | Meaning |
90
+ | ------------- | --------------------------------- | ---------------- | -------------------------------------------------------------- |
91
+ | `delivery` | `"once" \| "broadcast" \| "live"` | — (**required**) | Delivery physics |
92
+ | `description` | string | — | Human title in the Console (falls back to the signal name) |
93
+ | `schema` | zod / Standard Schema | | Payload contract; typed emits and Manifest docs |
94
+ | `retries` | number | `3` | Max delivery attempts before dead-letter (`once`) |
95
+ | `deadLetter` | boolean | `true` | Preserve exhausted messages in the DLQ (`once`) |
96
+ | `optional` | boolean | `false` | Allow emitting while nobody subscribes (skip the orphan check) |
96
97
 
97
98
  ## When delivery fails
98
99
 
@@ -90,17 +90,23 @@ export const createNote = on(
90
90
 
91
91
  Pick the facet that matches the physics of your data. Each declaration is one line; the runtime handle shows what flows can do with it.
92
92
 
93
- | Facet | Declares | Best for | `fx.store(…)` handle |
94
- | ------------------------- | ----------------- | ------------------------------------- | ------------------------------------------------------ |
95
- | `store.sql(name, opts)` | a SQL database | domain tables, relations, constraints | `select` · `insert` · `update` · `delete` · `findById` |
96
- | `store.kv(name)` | a key-value space | cache, sessions, rate limits | `get` · `set(key, value, ttl?)` · `delete` · `list` |
97
- | `store.files(name)` | a blob bucket | uploads, exports, attachments | `put` · `get` · `delete` · `list(prefix?)` |
98
- | `store.index(name, opts)` | a vector index | semantic search / RAG (`dims`) | `upsert` · `search(vector, topK?)` · `delete` |
93
+ | Facet | Declares | Best for | `fx.store(…)` handle |
94
+ | -------------------------- | ----------------- | ------------------------------------- | ------------------------------------------------------ |
95
+ | `store.sql(name, opts)` | a SQL database | domain tables, relations, constraints | `select` · `insert` · `update` · `delete` · `findById` |
96
+ | `store.kv(name, opts?)` | a key-value space | cache, sessions, rate limits | `get` · `set(key, value, ttl?)` · `delete` · `list` |
97
+ | `store.files(name, opts?)` | a blob bucket | uploads, exports, attachments | `put` · `get` · `delete` · `list(prefix?)` |
98
+ | `store.index(name, opts)` | a vector index | semantic search / RAG (`dims`) | `upsert` · `search(vector, topK?)` · `delete` |
99
+
100
+ Every facet accepts optional `description` — a human title in the Console
101
+ (falls back to the store name). `store.index` also takes `dims`.
99
102
 
100
103
  ```typescript
101
- export const cache = store.kv("sessions");
104
+ export const cache = store.kv("sessions", { description: "Session cache" });
102
105
  export const uploads = store.files("attachments");
103
- export const embeddings = store.index("docs", { dims: 1536 });
106
+ export const embeddings = store.index("docs", {
107
+ dims: 1536,
108
+ description: "Document embeddings",
109
+ });
104
110
  ```
105
111
 
106
112
  ## CRUD without boilerplate — `store.resource`
@@ -175,6 +181,7 @@ The recommended path: declare tables ORM-agnostically, then let `oke db` emit re
175
181
  | `.default(v)` · `.defaultFn(id \| now)` | defaults |
176
182
  | `.pii()` · `.sensitive()` · `.retain("30d")` | privacy classification |
177
183
  | `.as("sql_name")` | override the automatic `camelCase → snake_case` |
184
+ | `.describe("…")` | human title in the Console (falls back to key) |
178
185
  | `.references(() => col, { onDelete })` | foreign key |
179
186
 
180
187
  ### Foreign keys and relations
@@ -242,6 +249,39 @@ drivers: {
242
249
 
243
250
  Container images come from the `images` map — change the vendor by changing the pin, never the driver id.
244
251
 
252
+ ### Opt-in SQL drivers
253
+
254
+ Two more `store.sql` drivers cover specific parity needs. Both are optional peers — `bun add` them yourself; neither changes the `sqlite` local default.
255
+
256
+ | Driver | Install | Wire dialect | Choose for |
257
+ | -------- | -------------------------------------------------------- | ------------ | ------------------------------------------------------ |
258
+ | `libsql` | `@libsql/client` | sqlite | local vector search without Docker (`index: "libsql"`) |
259
+ | `pglite` | `@electric-sql/pglite` + `@electric-sql/pglite-pgvector` | postgresql | the real Postgres dialect + pgvector, in-process |
260
+
261
+ <Callout title="PGlite is not a latency play">
262
+ PGlite pays a one-time ~1 s WASM init, and warm CRUD is roughly 15× slower than `bun:sqlite`.
263
+ Choose it for dialect/pgvector parity with prod — never as a faster local default.
264
+ </Callout>
265
+
266
+ A SQL-backed index shares the sql facet's already-open connection, so configure the pair together:
267
+
268
+ ```typescript title="oke.config.ts"
269
+ drivers: {
270
+ store: {
271
+ sql: { local: "libsql" },
272
+ index: { local: "libsql" },
273
+ },
274
+ },
275
+ ```
276
+
277
+ | `store.index` id | Shares connection from | Real ANN via |
278
+ | ---------------- | -------------------------- | ------------------------------------ |
279
+ | `memory` | — (in-process, default) | — |
280
+ | `pgvector` | `postgres` or `pglite` sql | HNSW + `cosineDistance` |
281
+ | `libsql` | `libsql` sql | `libsql_vector_idx` + `vector_top_k` |
282
+
283
+ **Consequence:** a configured SQL-backed index that cannot reach its engine — missing peer, missing `vector` extension, wrong sql driver — fails loudly at first use. It never silently falls back to `memory`.
284
+
245
285
  ## Privacy built in
246
286
 
247
287
  Columns tagged `.pii()` or `.sensitive()` are masked at the store boundary — flows, logs, and the Console see a mask, not the value. Revealing cleartext PII requires an explicit `pii:reveal` gate on the flow, so access is a permission, not a convention.
@@ -91,7 +91,7 @@ That's the whole loop: declare → set → read. Everything below is what OKE gu
91
91
 
92
92
  | Option | Type | Meaning |
93
93
  | ------------- | --------------------- | ------------------------------------------------------------------------ |
94
- | `description` | string | Shown in boot-gap listings and the Console |
94
+ | `description` | string | Human title in boot-gap listings and the Console (falls back to the key) |
95
95
  | `rotate` | string | Rotation hint (`"90d"`) — drives the rotation-due signal in the Console |
96
96
  | `schema` | zod / Standard Schema | Validated at boot; a bad value fails boot like a missing one |
97
97
  | `dev` | string | Local-only fallback when no source provides a value (never used in prod) |
@@ -46,8 +46,8 @@ A client whose IP is not on the list receives `403` with a typed denial:
46
46
  <Callout type="error">
47
47
  Standard reverse proxies **append** to `X-Forwarded-For` — left-side hops are attacker-controlled.
48
48
  The plugin trusts the hop `trustedProxyDepth` from the **right** (default `1` = last hop). Set
49
- this to your real proxy count; wrong depth bypasses the allowlist topology-dependent, not
50
- drop-in.
49
+ this to your real proxy count wrong depth bypasses the allowlist (topology-dependent, not
50
+ drop-in).
51
51
  </Callout>
52
52
 
53
53
  ## Notes
@@ -36,10 +36,10 @@ drivers: {
36
36
 
37
37
  | Key | Shape | Driver ids (verified) |
38
38
  | ------------------ | -------------- | ---------------------------------------------------------------------------- |
39
- | `store.sql` | env driver map | `sqlite` · `postgres` · `memory` |
39
+ | `store.sql` | env driver map | `sqlite` · `postgres` · `libsql` · `pglite` · `memory` |
40
40
  | `store.kv` | env driver map | `memory` · `redis` |
41
41
  | `store.files` | env driver map | `memory` · `fs` · `s3` |
42
- | `store.index` | env driver map | `memory` · `pgvector` |
42
+ | `store.index` | env driver map | `memory` · `pgvector` · `libsql` |
43
43
  | `signal` | env driver map | `memory` · `postgres` · `redis` · `nats` |
44
44
  | `clock` | env driver map | `memory` · `postgres` · `frozen` |
45
45
  | `vault` | env driver map | `dotenv` · `openbao` · `memory` |
@@ -18,8 +18,16 @@ OKE reads environment variables at boot for connection detail and secrets — ne
18
18
  | `DATABASE_URL` | Postgres connection (`postgres` driver, drizzle-kit) | `postgres://localhost:5432/oke` |
19
19
  | `OKE_STORE_SQL_URL` | Explicit SQL URL override | — |
20
20
  | `OKE_SQLITE_URL` | SQLite file path (`sqlite` driver) | `.oke/app.sqlite` |
21
+ | `OKE_LIBSQL_URL` | libSQL URL or file path (`libsql` driver) | `.oke/app.libsql` |
22
+ | `OKE_PGLITE_URL` | PGlite data dir or `memory://` (`pglite` driver) | `.oke/pgdata` |
21
23
  | `OKE_SQL_DRIVER` | Force the sql driver id at boot | config map |
22
24
 
25
+ ## Index store
26
+
27
+ | Variable | Used for | Default when unset |
28
+ | ------------------ | ------------------------- | ------------------ |
29
+ | `OKE_INDEX_DRIVER` | Force the index driver id | config map |
30
+
23
31
  ## KV store
24
32
 
25
33
  | Variable | Used for | Default when unset |
@@ -41,8 +41,48 @@ See [Store](/docs/elements/store) for the query-builder surface.
41
41
  | ---------------------------- | ------- | ----------------------------------------------------- |
42
42
  | `fx.call(flow, input?)` | `call` | The callee's `out` — runs through the same pipeline |
43
43
  | `fx.step(name, fn)` | — | Durable step: replays from the journal, never re-runs |
44
+ | `fx.all([...thunks])` | — | Parallel; first rejection aborts siblings |
45
+ | `fx.race([...thunks])` | — | First settle wins; losers aborted |
46
+ | `fx.retry(fn, opts?)` | — | Exponential backoff + jitter (plain Promise) |
47
+ | `fx.signal` | — | Ambient `AbortSignal` for the current branch |
44
48
  | `fx.fail(code, data, opts?)` | — | Typed failure value (`opts.message` overrides) |
45
49
 
50
+ ## Concurrency and retry
51
+
52
+ Pass **thunks** to `all` / `race` — not already-started Promises — so each branch gets an abort scope before work begins.
53
+
54
+ ```typescript
55
+ const [user, stock] = await fx.all([
56
+ () => fx.store(db).findById(users, input.userId),
57
+ () => fx.store(db).findById(inventory, input.sku),
58
+ ]);
59
+
60
+ const charge = await fx.step("charge", () =>
61
+ fx.retry(() => fx.call(stripeCharge, { amount: input.total }), {
62
+ retries: 3,
63
+ delay: "100ms",
64
+ backoff: 2,
65
+ jitter: true,
66
+ }),
67
+ );
68
+ ```
69
+
70
+ | `fx.retry` option | Default | Meaning |
71
+ | ----------------- | ------- | -------------------------------------------- |
72
+ | `retries` | `0` | Extra attempts after the first |
73
+ | `delay` | `50` | Initial backoff — ms number or `"100ms"` |
74
+ | `backoff` | `2` | Multiplier after each retry |
75
+ | `jitter` | `true` | Full jitter on the delay (thundering-herd) |
76
+ | `when` | thrown | Predicate; skips `AbortError` and sleep park |
77
+
78
+ <Callout title="Cooperative cancel">
79
+ Losing branches see `fx.signal` abort. Drivers that do not yet honor the signal may still finish
80
+ in the background — check `fx.signal.aborted` in long user work, and prefer `fx.all` over bare
81
+ `Promise.all`.
82
+ </Callout>
83
+
84
+ **Consequence:** put `fx.retry` inside `fx.step` on durable flows so a completed charge never re-runs on resume. Coarse whole-body retry is `flow({ retry: { … } })` on the same journal session.
85
+
46
86
  ## Channel
47
87
 
48
88
  | Signature | Records | Notes |
@@ -64,22 +64,22 @@ Plugin flows appear in the Manifest, plugin tables land in `schema.generated.ts`
64
64
 
65
65
  Every method below exists on both the fluent definition and the boot-time builder the registry records:
66
66
 
67
- | Method | Contributes |
68
- | -------------------------------- | --------------------------------------------------- |
69
- | `.flow(def)` | An ordinary flow — Manifest, Console, client types |
70
- | `.hook(stage, fn)` | A per-request intercept at one pipeline stage |
71
- | `.edge(fn)` | A handler for HTTP requests that match **no** flow |
72
- | `.decorate(key, value)` | A typed context decoration, visible to flows |
73
- | `.element({ kind, name })` | An element contribution (e.g. `store.sql` facet) |
74
- | `.driver(id, impl)` | A protocol-named driver for an existing element |
75
- | `.image(role, recipe)` | An image recipe for a docker role |
76
- | `.table(name, columns, options)` | A whole DB table, merged into the generated schema |
77
- | `.errors(map)` | Typed errors flows can fail with |
78
- | `.client(name, ext)` | A typed client extension |
79
- | `.consolePanel(panel)` | A Console panel (ESM entry loaded at runtime) |
80
- | `.cli(name, handler)` | An `oke <name>` CLI command |
81
- | `.config(schema)` | A config schema; values live on the plugin identity |
82
- | `.needs(dep)` | A declared dependency (e.g. `"store.kv"`) |
67
+ | Method | Contributes |
68
+ | -------------------------------- | --------------------------------------------------------------------------------------------- |
69
+ | `.flow(def)` | An ordinary flow — Manifest, Console, client types |
70
+ | `.hook(stage, fn)` | A per-request intercept at one pipeline stage |
71
+ | `.edge(fn)` | A handler for HTTP requests that match **no** flow |
72
+ | `.decorate(key, value)` | A typed context decoration, visible to flows |
73
+ | `.element({ kind, name })` | An element contribution (e.g. `store.sql` facet) |
74
+ | `.driver(id, impl)` | A protocol-named driver for an existing element |
75
+ | `.image(role, recipe)` | An image recipe for a docker role |
76
+ | `.table(name, columns, options)` | A whole DB table, merged into the generated schema (`options.description` / `plane` optional) |
77
+ | `.errors(map)` | Typed errors flows can fail with |
78
+ | `.client(name, ext)` | A typed client extension |
79
+ | `.consolePanel(panel)` | A Console panel (ESM entry loaded at runtime) |
80
+ | `.cli(name, handler)` | An `oke <name>` CLI command |
81
+ | `.config(schema)` | A config schema; values live on the plugin identity |
82
+ | `.needs(dep)` | A declared dependency (e.g. `"store.kv"`) |
83
83
 
84
84
  New infrastructure is a **driver** for an existing element, never a ninth element — plugins follow the same law.
85
85
 
@@ -194,11 +194,11 @@ A plugin may declare **its own tables** with `field.*` columns, merged into the
194
194
  plugin("billing", { version: "2.1.0" }).table(
195
195
  "invoices",
196
196
  { id: field.text().primaryKey().defaultFn(id) },
197
- { plane: "user" },
197
+ { plane: "user", description: "Customer invoices" },
198
198
  );
199
199
  ```
200
200
 
201
- Extending an existing **app-owned** table with plugin columns is not supported in v1 — contribute a separate table and reference the app's by key. The optional `plane` metadata (`"operator" | "user" | "shared"`) keeps data-plane isolation intact for privacy tooling.
201
+ Extending an existing **app-owned** table with plugin columns is not supported in v1 — contribute a separate table and reference the app's by key. The optional `plane` metadata (`"operator" | "user" | "shared"`) keeps data-plane isolation intact for privacy tooling. Optional `description` is a human title in the Console (falls back to the table name).
202
202
 
203
203
  ## Identity, config, and dependencies
204
204