okengine 0.9.0 → 0.10.0

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 +105 -14
  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/plugins/otp.mdx +58 -46
  14. package/site/content/docs/providers/azure-redis.mdx +85 -0
  15. package/site/content/docs/providers/cockroachdb.mdx +86 -0
  16. package/site/content/docs/providers/digitalocean-caching.mdx +88 -0
  17. package/site/content/docs/providers/dragonfly-cloud.mdx +82 -0
  18. package/site/content/docs/providers/elasticache.mdx +92 -0
  19. package/site/content/docs/providers/index.mdx +101 -0
  20. package/site/content/docs/providers/memorystore.mdx +89 -0
  21. package/site/content/docs/providers/meta.json +20 -0
  22. package/site/content/docs/providers/neon.mdx +115 -0
  23. package/site/content/docs/providers/redis-cloud.mdx +93 -0
  24. package/site/content/docs/providers/supabase.mdx +117 -0
  25. package/site/content/docs/providers/upstash.mdx +91 -0
  26. package/site/content/docs/providers/yugabytedb.mdx +95 -0
  27. package/site/content/docs/recipes/caddy.mdx +128 -0
  28. package/site/content/docs/recipes/cockroachdb.mdx +127 -0
  29. package/site/content/docs/recipes/dragonfly.mdx +125 -0
  30. package/site/content/docs/recipes/index.mdx +146 -0
  31. package/site/content/docs/recipes/llama-cpp.mdx +153 -0
  32. package/site/content/docs/recipes/mailpit.mdx +136 -0
  33. package/site/content/docs/recipes/meilisearch.mdx +139 -0
  34. package/site/content/docs/recipes/meta.json +31 -0
  35. package/site/content/docs/recipes/ollama.mdx +142 -0
  36. package/site/content/docs/recipes/openbao.mdx +141 -0
  37. package/site/content/docs/recipes/pgdog.mdx +137 -0
  38. package/site/content/docs/recipes/postgres.mdx +136 -0
  39. package/site/content/docs/recipes/redis.mdx +130 -0
  40. package/site/content/docs/recipes/rustfs.mdx +148 -0
  41. package/site/content/docs/recipes/sglang.mdx +105 -0
  42. package/site/content/docs/recipes/supabase-docker.mdx +145 -0
  43. package/site/content/docs/recipes/timescale.mdx +129 -0
  44. package/site/content/docs/recipes/traefik.mdx +136 -0
  45. package/site/content/docs/recipes/valkey.mdx +121 -0
  46. package/site/content/docs/recipes/vllm.mdx +106 -0
  47. package/site/content/docs/recipes/yugabytedb.mdx +135 -0
  48. package/site/content/docs/reference/cli.md +42 -6
  49. package/site/content/docs/reference/configuration.mdx +5 -3
  50. package/site/content/docs/reference/environment-variables.mdx +12 -12
  51. package/site/content/docs/reference/fx.mdx +38 -4
  52. package/src/auth/otp-capability.ts +14 -14
  53. package/src/cli/ai-setup/ai-setup.test.ts +32 -12
  54. package/src/cli/ai-setup/apply.ts +65 -23
  55. package/src/cli/ai-setup/catalog.ts +1316 -35
  56. package/src/cli/ai-setup/detect-ollama.ts +47 -0
  57. package/src/cli/ai-setup/index.ts +44 -7
  58. package/src/cli/ai-setup/prompts.ts +430 -486
  59. package/src/cli/ai-setup/recommend.test.ts +150 -50
  60. package/src/cli/ai-setup/recommend.ts +118 -101
  61. package/src/cli/ask-seed.test.ts +68 -0
  62. package/src/cli/ask-seed.ts +79 -0
  63. package/src/cli/db-seed-redact.test.ts +25 -0
  64. package/src/cli/db-seed.ts +36 -7
  65. package/src/cli/db.test.ts +1 -1
  66. package/src/cli/db.ts +2 -2
  67. package/src/cli/dev-controls.test.ts +107 -0
  68. package/src/cli/dev-controls.ts +354 -0
  69. package/src/cli/dev-schema-sync.ts +12 -4
  70. package/src/cli/dev.test.ts +50 -30
  71. package/src/cli/dev.ts +607 -66
  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/compiler/extract.ts +3 -3
  82. package/src/config/index.ts +3 -3
  83. package/src/console/server/claim.ts +3 -1
  84. package/src/console/ui/dist/assets/{index-ClX0ZCe5.js → index-D-9KFce5.js} +2 -2
  85. package/src/console/ui/dist/assets/panel-overview-CW0CQEc2.js +1 -0
  86. package/src/console/ui/dist/assets/panel-runs-BxuyDI3x.js +1 -0
  87. package/src/console/ui/dist/assets/{panel-signals-Dsxdu_AR.js → panel-signals-BXofbj7v.js} +1 -1
  88. package/src/console/ui/dist/assets/{panel-store-hy7O8HOs.js → panel-store-B8mH-arU.js} +1 -1
  89. package/src/console/ui/dist/assets/{panel-traces-CBiAe5go.js → panel-traces-DSA-ZWnf.js} +1 -1
  90. package/src/console/ui/dist/index.html +1 -1
  91. package/src/console/ui/overview/slo.ts +31 -3
  92. package/src/console/ui/overview/types.ts +6 -0
  93. package/src/console/ui/runs/errors.test.ts +53 -0
  94. package/src/console/ui/runs/errors.ts +88 -0
  95. package/src/console/ui/runs/index.ts +8 -0
  96. package/src/console/ui/runs/search.ts +16 -0
  97. package/src/console/ui/shell/panels/runs/RunsPanel.tsx +72 -1
  98. package/src/docker/ai-model-status.test.ts +101 -0
  99. package/src/docker/ai-model-status.ts +276 -0
  100. package/src/docker/compose-health.test.ts +115 -0
  101. package/src/docker/compose-health.ts +234 -0
  102. package/src/docker/compose.ts +24 -8
  103. package/src/docker/derive.ts +35 -9
  104. package/src/docker/docker.test.ts +304 -12
  105. package/src/docker/helpers.ts +56 -0
  106. package/src/docker/index.ts +52 -0
  107. package/src/docker/ollama-pull.ts +232 -0
  108. package/src/docker/recipes/cockroach.ts +24 -0
  109. package/src/docker/recipes/index.ts +32 -0
  110. package/src/docker/recipes/llama-cpp.ts +273 -0
  111. package/src/docker/recipes/ollama.ts +19 -23
  112. package/src/docker/recipes/pgdog.ts +4 -1
  113. package/src/docker/recipes/postgres.ts +5 -18
  114. package/src/docker/recipes/sglang.ts +55 -0
  115. package/src/docker/recipes/supabase.ts +21 -0
  116. package/src/docker/recipes/timescale.ts +18 -0
  117. package/src/docker/recipes/vllm.ts +44 -0
  118. package/src/docker/recipes/yugabyte.ts +23 -0
  119. package/src/docker/stack-id.test.ts +22 -0
  120. package/src/docker/stack-id.ts +38 -4
  121. package/src/docker/types.ts +15 -0
  122. package/src/drivers/ai-ollama.integration.test.ts +7 -4
  123. package/src/drivers/signal-engine.ts +10 -0
  124. package/src/drivers/signal-postgres.ts +24 -2
  125. package/src/drivers/signal-types.ts +10 -0
  126. package/src/elements/channel/fallback.test.ts +91 -0
  127. package/src/elements/channel/hard-bounce.test.ts +82 -0
  128. package/src/elements/channel/injection.test.ts +153 -0
  129. package/src/elements/channel/locale-ar.test.ts +102 -0
  130. package/src/elements/channel/otp-delivery.ts +5 -2
  131. package/src/elements/channel/receipts-ledger.test.ts +100 -0
  132. package/src/elements/channel/runtime.ts +24 -1
  133. package/src/elements/channel/test-helpers.ts +60 -0
  134. package/src/elements/channel/whatsapp-compliance.test.ts +124 -0
  135. package/src/elements/store/cache.ts +1 -1
  136. package/src/elements/store/emit-drizzle.ts +43 -9
  137. package/src/elements/store/runtime.ts +1 -1
  138. package/src/elements/store/schema-decl.test.ts +2 -0
  139. package/src/elements/store/schema-decl.ts +15 -0
  140. package/src/elements/store/seed.ts +1 -1
  141. package/src/index.ts +1 -0
  142. package/src/kernel/app.ts +118 -11
  143. package/src/kernel/boot-bind/signal.ts +6 -2
  144. package/src/kernel/boot-bind/store.ts +2 -1
  145. package/src/kernel/boot.test.ts +6 -5
  146. package/src/kernel/boot.ts +8 -3
  147. package/src/kernel/compensate.test.ts +131 -0
  148. package/src/kernel/correlation.test.ts +108 -0
  149. package/src/kernel/effect-timing.test.ts +82 -0
  150. package/src/kernel/flow.ts +38 -0
  151. package/src/kernel/fx-runs.test.ts +49 -0
  152. package/src/kernel/fx.test.ts +14 -0
  153. package/src/kernel/fx.ts +111 -1
  154. package/src/manifest/types.ts +8 -2
  155. package/src/plugins/auth-delivery.mailpit.integration.test.ts +1 -1
  156. package/src/plugins/auth-methods.security.test.ts +3 -3
  157. package/src/plugins/auth-methods.test.ts +1 -1
  158. package/src/plugins/index.ts +2 -2
  159. package/src/plugins/otp.test.ts +21 -19
  160. package/src/plugins/otp.ts +36 -34
  161. package/src/plugins/taqnyat.live.test.ts +3 -3
  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
@@ -0,0 +1,124 @@
1
+ /**
2
+ * WhatsApp Business characterization (Known gap freeze):
3
+ * - drivers pick template vs free-text by presence of message.template only
4
+ * - Channel runtime has no 24h session-window API
5
+ *
6
+ * Meta requires free-form session messages inside a customer-initiated 24h
7
+ * window and pre-approved templates outside it. OKE does not enforce that
8
+ * window today — this file freezes the current payload-shape behavior so the
9
+ * gap cannot silently look fixed without a real design.
10
+ */
11
+
12
+ import { describe, expect, test } from "bun:test";
13
+ import type {
14
+ ChannelDriver,
15
+ ChannelMessage,
16
+ ChannelSendResult,
17
+ } from "../../drivers/channel-types.ts";
18
+ import { channel, createChannelRuntime } from "../channel.ts";
19
+
20
+ type Captured =
21
+ | { kind: "template"; name: string; language: string; to: string }
22
+ | { kind: "text"; text: string; to: string };
23
+
24
+ function fakeWhatsAppDriver(captures: Captured[]): ChannelDriver {
25
+ const channelTransport = {
26
+ provider: "wa-cloud",
27
+ mediums: ["whatsapp" as const],
28
+ async send(message: ChannelMessage): Promise<ChannelSendResult> {
29
+ const templateName =
30
+ message.template ??
31
+ (typeof message.data?.template === "string" ? message.data.template : undefined);
32
+ const language =
33
+ message.locale ??
34
+ (typeof message.data?.language === "string" ? message.data.language : "en_US");
35
+
36
+ if (templateName) {
37
+ captures.push({
38
+ kind: "template",
39
+ name: templateName,
40
+ language,
41
+ to: message.to,
42
+ });
43
+ } else {
44
+ captures.push({
45
+ kind: "text",
46
+ text: message.text ?? "",
47
+ to: message.to,
48
+ });
49
+ }
50
+
51
+ return {
52
+ ok: true,
53
+ messageId: "wa-1",
54
+ driverId: "wa-cloud",
55
+ attempts: [{ driverId: "wa-cloud", ok: true, at: Date.now(), messageId: "wa-1" }],
56
+ };
57
+ },
58
+ };
59
+
60
+ return {
61
+ id: "wa-cloud",
62
+ channel: channelTransport,
63
+ whatsappTransport: {
64
+ provider: "wa-cloud",
65
+ async send() {
66
+ return { messageId: "x", to: "", status: "sent", response: "" };
67
+ },
68
+ },
69
+ };
70
+ }
71
+
72
+ describe("WhatsApp session vs template (characterization)", () => {
73
+ test("template name on the send path selects template payload shape", async () => {
74
+ const captures: Captured[] = [];
75
+ const runtime = createChannelRuntime({
76
+ templates: [channel.template("order-update", { medium: "whatsapp" })],
77
+ drivers: [fakeWhatsAppDriver(captures)],
78
+ catalog: {
79
+ "order-update": { en: { text: "Your order shipped" } },
80
+ },
81
+ });
82
+
83
+ await runtime.send("order-update", {
84
+ to: "+966500000000",
85
+ locale: "en_US",
86
+ });
87
+
88
+ expect(captures).toEqual([
89
+ {
90
+ kind: "template",
91
+ name: "order-update",
92
+ language: "en_US",
93
+ to: "+966500000000",
94
+ },
95
+ ]);
96
+ });
97
+
98
+ test("missing template name sends free-text (no session-window gate)", async () => {
99
+ const captures: Captured[] = [];
100
+ // Medium-agnostic path that still hits channel transport without a template
101
+ // name on ChannelMessage — simulate driver-level choice when template unset.
102
+ const driver = fakeWhatsAppDriver(captures);
103
+ const result = await driver.channel!.send({
104
+ medium: "whatsapp",
105
+ to: "+966500000000",
106
+ text: "Hello from session",
107
+ // no template field — free-form
108
+ });
109
+
110
+ expect(result.ok).toBe(true);
111
+ expect(captures).toEqual([{ kind: "text", text: "Hello from session", to: "+966500000000" }]);
112
+ });
113
+
114
+ test("ChannelRuntime has no session-window surface", () => {
115
+ const runtime = createChannelRuntime({});
116
+ const keys = Object.keys(runtime);
117
+ expect(keys).not.toContain("sessionWindow");
118
+ expect(keys).not.toContain("whatsappSession");
119
+ expect(keys).not.toContain("withinSessionWindow");
120
+ expect(typeof (runtime as { withinSessionWindow?: unknown }).withinSessionWindow).toBe(
121
+ "undefined",
122
+ );
123
+ });
124
+ });
@@ -53,7 +53,7 @@ export function tier1KeysForReads(
53
53
  effects: Effects,
54
54
  dimsByResource?: Readonly<Record<string, readonly string[]>>,
55
55
  ): string[] {
56
- const reads = effects.reads ?? [];
56
+ const reads = (effects.reads ?? []).filter((r): r is ResourceRef => r !== "runs");
57
57
  return reads.map((resource) => computedCacheKey(resource, dimsByResource?.[resource]));
58
58
  }
59
59
 
@@ -5,8 +5,9 @@
5
5
  * watch) when abstract decls exist — not a third schema CLI.
6
6
  */
7
7
 
8
+ import { existsSync } from "node:fs";
8
9
  import { mkdir, writeFile } from "node:fs/promises";
9
- import { dirname, join, resolve } from "node:path";
10
+ import { dirname, join } from "node:path";
10
11
  import type { TableContribution } from "../../kernel/plugin.ts";
11
12
  import {
12
13
  isFieldBuilder,
@@ -30,10 +31,16 @@ export type SqlDialect = "sqlite" | "postgres";
30
31
  export const GENERATED_SCHEMA_HEADER = "// generated by oke — do not edit";
31
32
 
32
33
  /** Default output path (relative to project root). */
33
- export const DEFAULT_GENERATED_SCHEMA_REL = "src/schema.generated.ts";
34
+ export const DEFAULT_GENERATED_SCHEMA_REL = "src/db/schema.generated.ts";
34
35
 
35
36
  /** Default declaration module path (relative to project root). */
36
- export const DEFAULT_DECLARE_SCHEMA_REL = "src/schema.decl.ts";
37
+ export const DEFAULT_DECLARE_SCHEMA_REL = "src/db/schema.decl.ts";
38
+
39
+ /** Legacy paths kept so pre-layout projects keep resolving. */
40
+ export const LEGACY_GENERATED_SCHEMA_REL = "src/schema.generated.ts";
41
+
42
+ /** @see LEGACY_GENERATED_SCHEMA_REL */
43
+ export const LEGACY_DECLARE_SCHEMA_REL = "src/schema.decl.ts";
37
44
 
38
45
  /**
39
46
  * Map a store.sql driver id to an emit dialect.
@@ -306,8 +313,16 @@ export async function maybeEmitDomainSchema(
306
313
  options: EmitDomainSchemaOptions,
307
314
  ): Promise<{ readonly emitted: boolean; readonly path?: string; readonly tableCount?: number }> {
308
315
  const cwd = options.cwd;
309
- const declareRel = options.declarePath ?? DEFAULT_DECLARE_SCHEMA_REL;
310
- const declareAbs = resolve(cwd, declareRel);
316
+ const resolved = resolveEmitPaths(cwd, {
317
+ ...(options.declarePath !== undefined ? { declare: options.declarePath } : {}),
318
+ ...(options.outPath !== undefined ? { generated: options.outPath } : {}),
319
+ });
320
+ const declareAbs = resolved.declarePath;
321
+ const outAbs = resolved.outPath;
322
+ const declareRel = declareAbs.startsWith(`${cwd}/`)
323
+ ? declareAbs.slice(cwd.length + 1)
324
+ : declareAbs;
325
+ const outRel = outAbs.startsWith(`${cwd}/`) ? outAbs.slice(cwd.length + 1) : outAbs;
311
326
  const declareFile = Bun.file(declareAbs);
312
327
 
313
328
  let appTables: SchemaTableDecl[] = [];
@@ -332,8 +347,6 @@ export async function maybeEmitDomainSchema(
332
347
  }
333
348
 
334
349
  const tables = mergeSchemaTables(appTables, fromPlugins);
335
- const outRel = options.outPath ?? DEFAULT_GENERATED_SCHEMA_REL;
336
- const outAbs = resolve(cwd, outRel);
337
350
  const source = emitDrizzleSource(tables, options.dialect, { relations: appRelations });
338
351
  await mkdir(dirname(outAbs), { recursive: true });
339
352
  await writeFile(outAbs, source, "utf8");
@@ -345,6 +358,7 @@ export async function maybeEmitDomainSchema(
345
358
 
346
359
  /**
347
360
  * Resolve declare/out paths from an optional db config slice.
361
+ * Prefers explicit config, then new `src/db/*` defaults, then legacy `src/*`.
348
362
  *
349
363
  * @param cwd - Project root
350
364
  * @param db - Config `db` block
@@ -353,9 +367,29 @@ export function resolveEmitPaths(
353
367
  cwd: string,
354
368
  db?: { readonly declare?: string; readonly generated?: string },
355
369
  ): { readonly declarePath: string; readonly outPath: string } {
370
+ let declareRel = db?.declare;
371
+ if (!declareRel) {
372
+ if (existsSync(join(cwd, DEFAULT_DECLARE_SCHEMA_REL))) {
373
+ declareRel = DEFAULT_DECLARE_SCHEMA_REL;
374
+ } else if (existsSync(join(cwd, LEGACY_DECLARE_SCHEMA_REL))) {
375
+ declareRel = LEGACY_DECLARE_SCHEMA_REL;
376
+ } else {
377
+ declareRel = DEFAULT_DECLARE_SCHEMA_REL;
378
+ }
379
+ }
380
+
381
+ let outRel = db?.generated;
382
+ if (!outRel) {
383
+ const preferLegacy =
384
+ declareRel === LEGACY_DECLARE_SCHEMA_REL ||
385
+ (!existsSync(join(cwd, DEFAULT_DECLARE_SCHEMA_REL)) &&
386
+ existsSync(join(cwd, LEGACY_GENERATED_SCHEMA_REL)));
387
+ outRel = preferLegacy ? LEGACY_GENERATED_SCHEMA_REL : DEFAULT_GENERATED_SCHEMA_REL;
388
+ }
389
+
356
390
  return {
357
- declarePath: join(cwd, db?.declare ?? DEFAULT_DECLARE_SCHEMA_REL),
358
- outPath: join(cwd, db?.generated ?? DEFAULT_GENERATED_SCHEMA_REL),
391
+ declarePath: join(cwd, declareRel),
392
+ outPath: join(cwd, outRel),
359
393
  };
360
394
  }
361
395
 
@@ -473,7 +473,7 @@ export function createStoreRuntime(options: CreateStoreRuntimeOptions): StoreRun
473
473
  },
474
474
  putTier1(effects, value, dimsByResource) {
475
475
  const keys = tier1KeysForReads(effects, dimsByResource);
476
- const resources = effects.reads ?? [];
476
+ const resources = (effects.reads ?? []).filter((r): r is ResourceRef => r !== "runs");
477
477
  for (const key of keys) {
478
478
  cache.set({
479
479
  tier: 1,
@@ -39,6 +39,8 @@ describe("store.schema.table + field.*", () => {
39
39
  expect(notes.columns.createdAt.sqlType).toBe("integer");
40
40
  expect(notes.columns.id.defaultFnKind).toBe("id");
41
41
  expect(notes.columns.createdAt.defaultFnKind).toBe("now");
42
+ expect(typeof notes.id.getSQL).toBe("function");
43
+ expect(() => notes.id.getSQL()).toThrow(/type bridge/);
42
44
  });
43
45
 
44
46
  test("PII masks via declaration path without Drizzle metadata", async () => {
@@ -34,6 +34,10 @@ export interface ColumnReference {
34
34
  /**
35
35
  * Finalized column declaration — also satisfies {@link ColumnDef} so
36
36
  * {@link classificationsFromTable} / runtime masking work without Drizzle.
37
+ *
38
+ * `getSQL` is a structural bridge so drizzle-orm operators (`eq`, `isNull`, …)
39
+ * accept abstract columns in TypeScript. OKE’s SQL compiler never calls it —
40
+ * it reads {@link SchemaColumnDecl.sqlName} / metadata directly.
37
41
  */
38
42
  export interface SchemaColumnDecl extends ColumnDef {
39
43
  /** JS object key (e.g. `createdAt`). */
@@ -57,6 +61,12 @@ export interface SchemaColumnDecl extends ColumnDef {
57
61
  readonly references?: ColumnReference;
58
62
  /** Optional human description for Console / docs (falls back to the JS key). */
59
63
  readonly description?: string;
64
+ /**
65
+ * Drizzle `SQLWrapper` structural match — never invoked at runtime.
66
+ *
67
+ * @returns never (throws if called)
68
+ */
69
+ getSQL(): never;
60
70
  }
61
71
 
62
72
  /** Table from {@link store.schema.table} — extends {@link TableHandle}. */
@@ -171,6 +181,11 @@ function createBuilder(state: FieldState): FieldBuilder {
171
181
  ...(state.classification ? { classification: state.classification } : {}),
172
182
  ...(state.description !== undefined ? { description: state.description } : {}),
173
183
  ...(state.references ? { references: state.references } : {}),
184
+ getSQL(): never {
185
+ throw new Error(
186
+ `SchemaColumnDecl.getSQL("${sqlName}") is a type bridge — OKE compiles columns directly`,
187
+ );
188
+ },
174
189
  };
175
190
  },
176
191
  };
@@ -30,7 +30,7 @@ export interface SeedDef {
30
30
  export type SeedCategory = "dev" | "prod";
31
31
 
32
32
  /**
33
- * Freeze a seed declaration for `src/seed/index.ts` default export.
33
+ * Freeze a seed declaration for `src/db/seed/index.ts` default export.
34
34
  *
35
35
  * @param def - Essential / dev / prod function bags
36
36
  */
package/src/index.ts CHANGED
@@ -141,6 +141,7 @@ export {
141
141
  channel,
142
142
  createChannelRuntime,
143
143
  createConsentStore,
144
+ createSuppressionStore,
144
145
  createReceiptLedger,
145
146
  type ChannelTemplateDecl,
146
147
  type ChannelRuntime,
package/src/kernel/app.ts CHANGED
@@ -59,7 +59,9 @@ import {
59
59
  logDevRequest,
60
60
  shouldLogDevRequests,
61
61
  } from "../runtime/dev-request-log.ts";
62
+ import { fail, type FlowFailure } from "./errors.ts";
62
63
  import {
64
+ isFlowFailure,
63
65
  mergeHooks,
64
66
  runPipeline,
65
67
  type HookFn,
@@ -359,6 +361,10 @@ export interface OkeApp<D extends Record<string, unknown> = {}, R extends AppRou
359
361
  readonly operator?: FxOperator;
360
362
  /** Pre-resolved principal (either plane). */
361
363
  readonly principal?: ResolvedPrincipal;
364
+ /** Parent WideEvent id when this execution was caused by another. */
365
+ readonly parentId?: string;
366
+ /** Explicit run / WideEvent id (defaults to a new UUID). */
367
+ readonly runId?: string;
362
368
  },
363
369
  ): Promise<ExecuteResult>;
364
370
  /**
@@ -381,8 +387,13 @@ export interface OkeApp<D extends Record<string, unknown> = {}, R extends AppRou
381
387
  *
382
388
  * @param signal - Signal name or handle
383
389
  * @param payload - Payload
390
+ * @param meta - Optional envelope from the bus (producer run id)
384
391
  */
385
- dispatchSignal(signal: NamedRef, payload?: unknown): Promise<ExecuteResult[]>;
392
+ dispatchSignal(
393
+ signal: NamedRef,
394
+ payload?: unknown,
395
+ meta?: { readonly parentRunId?: string; readonly messageId?: string },
396
+ ): Promise<ExecuteResult[]>;
386
397
  /**
387
398
  * Invoke all flows bound to an `every` interval.
388
399
  *
@@ -637,8 +648,12 @@ export function oke(options: OkeOptions): OkeApp {
637
648
  }
638
649
  }
639
650
 
640
- async function handleSignalFire(name: string, payload: unknown): Promise<void> {
641
- await app.dispatchSignal(name, payload);
651
+ async function handleSignalFire(
652
+ name: string,
653
+ payload: unknown,
654
+ meta?: { readonly parentRunId?: string; readonly messageId?: string },
655
+ ): Promise<void> {
656
+ await app.dispatchSignal(name, payload, meta);
642
657
  }
643
658
 
644
659
  async function handleDurableResume(): Promise<void> {
@@ -844,7 +859,7 @@ export function oke(options: OkeOptions): OkeApp {
844
859
  };
845
860
  const result = await bootApplication(merged);
846
861
  bootResult = result;
847
- // otp() Tier 1 / Tier 2 capability — fail loud at boot, never silent downgrade.
862
+ // otp() provider / app mode capability — fail loud at boot, never silent downgrade.
848
863
  if (result.channel) {
849
864
  const { assertOtpPluginCapability } = await import("../auth/otp-capability.ts");
850
865
  for (const entry of pluginRegistry.installed) {
@@ -918,6 +933,10 @@ export function oke(options: OkeOptions): OkeApp {
918
933
  readonly originPrincipal?: FxPrincipal;
919
934
  /** Explicit locale override for {@link Fx.t} / channel sends. */
920
935
  readonly locale?: string;
936
+ /** Parent WideEvent id when this execution was caused by another. */
937
+ readonly parentId?: string;
938
+ /** Explicit run / WideEvent id (defaults to a new UUID). */
939
+ readonly runId?: string;
921
940
  },
922
941
  ): Promise<ExecuteResult> {
923
942
  registerFlow(flowDef);
@@ -957,6 +976,8 @@ export function oke(options: OkeOptions): OkeApp {
957
976
  readonly principal?: ResolvedPrincipal;
958
977
  readonly originPrincipal?: FxPrincipal;
959
978
  readonly locale?: string;
979
+ readonly parentId?: string;
980
+ readonly runId?: string;
960
981
  };
961
982
  readonly resolvedLocale: string;
962
983
  readonly defaultLocale: string;
@@ -991,6 +1012,15 @@ export function oke(options: OkeOptions): OkeApp {
991
1012
  const now = options.fx?.now ?? booted?.clock?.now ?? (() => Date.now());
992
1013
  const startedAt = now();
993
1014
  const telemetry = createRunTelemetry();
1015
+ const inboundId =
1016
+ extras?.request?.headers.get("x-request-id")?.trim() ||
1017
+ extras?.request?.headers.get("x-correlation-id")?.trim() ||
1018
+ undefined;
1019
+ const runId =
1020
+ extras?.runId ??
1021
+ (inboundId && inboundId.length > 0 ? inboundId : undefined) ??
1022
+ crypto.randomUUID();
1023
+ const parentId = extras?.parentId;
994
1024
 
995
1025
  // Element pipeline wiring only kicks in once booted — `autoBoot: false`
996
1026
  // keeps intentional pre-boot unit tests ungated (no elements either).
@@ -1057,6 +1087,7 @@ export function oke(options: OkeOptions): OkeApp {
1057
1087
  const journal = createJournal({
1058
1088
  store,
1059
1089
  now,
1090
+ id: () => runId,
1060
1091
  // Hold the run lease for the request's lifetime — a crash mid-run
1061
1092
  // leaves an expired lease another instance can reclaim and resume.
1062
1093
  ...(hasJournalLease(store) ? { lease: { instanceId, leaseMs } } : {}),
@@ -1071,6 +1102,7 @@ export function oke(options: OkeOptions): OkeApp {
1071
1102
  flow: flowDef.name,
1072
1103
  effects: flowDef.effects,
1073
1104
  runTelemetry: telemetry,
1105
+ runId,
1074
1106
  now,
1075
1107
  i18n: {
1076
1108
  locale: resolvedLocale,
@@ -1090,6 +1122,7 @@ export function oke(options: OkeOptions): OkeApp {
1090
1122
  aiRuntime: booted.ai,
1091
1123
  }
1092
1124
  : {}),
1125
+ runsRuntime: booted?.runs ?? (isRunsRuntimeLike(options.runs) ? options.runs : undefined),
1093
1126
  ...(journalSession ? { journal: journalSession, durable: true } : {}),
1094
1127
  callHandler: async (name, callInput) => {
1095
1128
  const target = flowsByName.get(name);
@@ -1103,6 +1136,7 @@ export function oke(options: OkeOptions): OkeApp {
1103
1136
  {
1104
1137
  originPrincipal: freezePrincipal(fx.principal),
1105
1138
  locale: resolvedLocale,
1139
+ parentId: runId,
1106
1140
  },
1107
1141
  );
1108
1142
  if (inner.failure) return inner.failure;
@@ -1174,10 +1208,32 @@ export function oke(options: OkeOptions): OkeApp {
1174
1208
  input: ctx.input,
1175
1209
  wakeAt: sleeping.wakeAt,
1176
1210
  });
1177
- } else if (!sleeping && result.failure) {
1178
- await journalSession.commit("failed", {
1179
- error: result.failure.error.code,
1180
- });
1211
+ } else if (!sleeping && isTerminalFailure(result)) {
1212
+ const terminalErr = result.failure ?? result.ctx.error;
1213
+ if (flowDef.compensate) {
1214
+ try {
1215
+ const completedSteps = journalSession.run.entries
1216
+ .filter((e) => e.kind === "step")
1217
+ .map((e) => e.name);
1218
+ await flowDef.compensate(
1219
+ {
1220
+ input: ctx.input as never,
1221
+ error: terminalErr,
1222
+ completedSteps,
1223
+ },
1224
+ fx,
1225
+ );
1226
+ } catch (compErr) {
1227
+ await journalSession.commit("failed", {
1228
+ error: `compensate:${failureCodeOf(compErr)}`,
1229
+ });
1230
+ }
1231
+ }
1232
+ if (journalSession.run.status === "running") {
1233
+ await journalSession.commit("failed", {
1234
+ error: failureCodeOf(terminalErr),
1235
+ });
1236
+ }
1181
1237
  } else if (!sleeping) {
1182
1238
  await journalSession.commit("completed", { output: result.output });
1183
1239
  }
@@ -1187,6 +1243,10 @@ export function oke(options: OkeOptions): OkeApp {
1187
1243
  booted?.runs ?? (isRunsRuntimeLike(options.runs) ? options.runs : undefined);
1188
1244
  if (runsRuntime) {
1189
1245
  const archiveCleartext = archiveFromInput(ctx.input, options.archiveInputFields);
1246
+ const replayInput = redactArchivedFields(ctx.input, options.archiveInputFields);
1247
+ const recordFailure =
1248
+ result.failure ??
1249
+ (isTerminalFailure(result) ? failureFromUnknown(result.ctx.error) : undefined);
1190
1250
  await runsRuntime.record(
1191
1251
  {
1192
1252
  flow: flowDef,
@@ -1196,7 +1256,10 @@ export function oke(options: OkeOptions): OkeApp {
1196
1256
  telemetry,
1197
1257
  startedAt,
1198
1258
  endedAt,
1199
- failure: result.failure,
1259
+ failure: recordFailure,
1260
+ id: runId,
1261
+ input: replayInput,
1262
+ ...(parentId !== undefined ? { parentId } : {}),
1200
1263
  },
1201
1264
  archiveCleartext,
1202
1265
  );
@@ -1480,12 +1543,16 @@ export function oke(options: OkeOptions): OkeApp {
1480
1543
 
1481
1544
  return respond(encodeExecuteResult(result));
1482
1545
  },
1483
- async dispatchSignal(signal, payload) {
1546
+ async dispatchSignal(signal, payload, meta) {
1484
1547
  const name = resolveName(signal);
1485
1548
  const results: ExecuteResult[] = [];
1486
1549
  for (const b of adopted) {
1487
1550
  if (b.trigger.kind === "signal" && b.trigger.name === name) {
1488
- results.push(await execute(b.flow, payload, b.trigger as SignalAsTrigger));
1551
+ results.push(
1552
+ await execute(b.flow, payload, b.trigger as SignalAsTrigger, {
1553
+ ...(meta?.parentRunId !== undefined ? { parentId: meta.parentRunId } : {}),
1554
+ }),
1555
+ );
1489
1556
  }
1490
1557
  }
1491
1558
  return results;
@@ -1555,6 +1622,26 @@ function compileHttpBinding(binding: Binding, aot: boolean): CompiledRoute {
1555
1622
  );
1556
1623
  }
1557
1624
 
1625
+ /** True when the pipeline ended in a terminal failure (not sleep park). */
1626
+ function isTerminalFailure(result: PipelineResult): boolean {
1627
+ if (result.failure) return true;
1628
+ return result.ctx.error !== undefined && !isJournalSuspend(result.ctx.error);
1629
+ }
1630
+
1631
+ /** Machine-readable code for journal / Runs failure fields. */
1632
+ function failureCodeOf(err: unknown): string {
1633
+ if (isFlowFailure(err)) return err.error.code;
1634
+ if (err instanceof Error && err.name && err.name !== "Error") return err.name;
1635
+ if (err instanceof Error && err.message) return err.message.slice(0, 120);
1636
+ return "Error";
1637
+ }
1638
+
1639
+ /** Coerce a thrown value into a FlowFailure for Runs wide events. */
1640
+ function failureFromUnknown(err: unknown): FlowFailure {
1641
+ if (isFlowFailure(err)) return err;
1642
+ return fail(failureCodeOf(err), {});
1643
+ }
1644
+
1558
1645
  /**
1559
1646
  * Pull personal fields from validated input for crypto-shred archival.
1560
1647
  *
@@ -1576,6 +1663,26 @@ function archiveFromInput(
1576
1663
  return Object.keys(out).length > 0 ? out : undefined;
1577
1664
  }
1578
1665
 
1666
+ /**
1667
+ * Clone input with archived personal fields replaced by shred markers so the
1668
+ * replay snapshot never retains cleartext for those keys.
1669
+ *
1670
+ * @param input - Validated flow input
1671
+ * @param fields - Property names that go to {@link archiveFromInput}
1672
+ */
1673
+ function redactArchivedFields(input: unknown, fields: readonly string[] | undefined): unknown {
1674
+ if (input === undefined) return undefined;
1675
+ if (!fields || fields.length === 0) return input;
1676
+ if (!input || typeof input !== "object" || Array.isArray(input)) return input;
1677
+ const obj = { ...(input as Record<string, unknown>) };
1678
+ for (const name of fields) {
1679
+ if (typeof obj[name] === "string" && (obj[name] as string).length > 0) {
1680
+ obj[name] = "[archived]";
1681
+ }
1682
+ }
1683
+ return obj;
1684
+ }
1685
+
1579
1686
  /**
1580
1687
  * Deep-merge channel template catalogs (later parts win per locale).
1581
1688
  *
@@ -7,6 +7,7 @@ import { memorySignalDriver } from "../../drivers/signal-memory.ts";
7
7
  import { createBunSignalRedisClient, redisSignalDriver } from "../../drivers/signal-redis.ts";
8
8
  import type { SignalRedisClientLike } from "../../drivers/signal-types.ts";
9
9
  import { createSignalRuntime, type SignalRuntime } from "../../elements/signal.ts";
10
+ import { emitBootWarn } from "../../runtime/boot-warn.ts";
10
11
  import type { BootOptions } from "../boot.ts";
11
12
 
12
13
  /**
@@ -35,7 +36,7 @@ let signalRedisWarned = false;
35
36
  function warnSignalRedisProcessLocal(): void {
36
37
  if (signalRedisWarned) return;
37
38
  signalRedisWarned = true;
38
- console.warn(
39
+ emitBootWarn(
39
40
  "oke boot: drivers.signal redis — emit relays to Redis, but consume/live/drain use a " +
40
41
  "process-local outbox (not multi-instance competing consumers). Prefer a shared durable " +
41
42
  "outbox path for multi-process tests, or a single consumer instance, until Redis Streams " +
@@ -129,7 +130,10 @@ export async function bindSignal(
129
130
  if (seen.has(name)) continue;
130
131
  seen.add(name);
131
132
  await bus.subscribe(name, `oke:${name}`, async (msg) => {
132
- await handler(name, msg.payload);
133
+ await handler(name, msg.payload, {
134
+ ...(msg.parentRunId !== undefined ? { parentRunId: msg.parentRunId } : {}),
135
+ messageId: msg.id,
136
+ });
133
137
  });
134
138
  }
135
139
  }
@@ -16,6 +16,7 @@ import { sqliteDriver } from "../../drivers/sqlite.ts";
16
16
  import type { FilesDriver, IndexDriver, KvDriver, SqlDriver } from "../../drivers/types.ts";
17
17
  import { createStoreRuntime, type StoreRuntime } from "../../elements/store.ts";
18
18
  import type { StoreDecl } from "../../elements/store/declare.ts";
19
+ import { emitBootWarn } from "../../runtime/boot-warn.ts";
19
20
  import type { BootOptions } from "../boot.ts"; // type-only — no cycle at runtime
20
21
 
21
22
  /**
@@ -43,7 +44,7 @@ export function resetFilesFsWarnForTests(): void {
43
44
  function warnFilesFsMultiInstance(): void {
44
45
  if (filesFsWarned) return;
45
46
  filesFsWarned = true;
46
- console.warn(
47
+ emitBootWarn(
47
48
  'oke boot: drivers.store.files "fs" is single-host — each instance sees its own ' +
48
49
  "filesystem (temp root when unbound). For horizontal scale use drivers.store.files s3 " +
49
50
  "(or an explicitly shared volume root, still single-host semantics).",
@@ -79,7 +79,7 @@ describe("boot — lazy element needs", () => {
79
79
  expect(needs.signal).toBe(false);
80
80
  });
81
81
 
82
- test("oke() Store-only graph stays under the prior 53 kB baseline", async () => {
82
+ test("oke() Store-only graph stays under the prior 54 kB baseline", async () => {
83
83
  const dir = await mkdtemp(join(tmpdir(), "oke-store-only-"));
84
84
  const entry = join(dir, "entry.ts");
85
85
  const appPath = join(import.meta.dir, "app.ts");
@@ -117,10 +117,11 @@ describe("boot — lazy element needs", () => {
117
117
  total += Bun.gzipSync(new Uint8Array(raw)).byteLength;
118
118
  }
119
119
  // Rebased after fx.sendOtp/verifyOtp on the shared fx surface (~50.1 kB
120
- // gzip), durable-journal lease surface (~51.2 kB), then fx.deliverOtp +
121
- // unified otp() plugin wiring on the shared surface (~50.9 kB → 53 kB
122
- // cap). Clock/channel/journal drivers stay lazy-bound; far below eager bind.
123
- expect(total).toBeLessThan(53_000);
120
+ // gzip), durable-journal lease surface (~51.2 kB), fx.deliverOtp +
121
+ // unified otp() (~50.9 kB → 53 kB), then fx.runs / SLO window on the
122
+ // shared surface (~53.3 kB → 54 kB cap). Clock/channel/journal drivers
123
+ // stay lazy-bound; far below eager bind.
124
+ expect(total).toBeLessThan(54_000);
124
125
  } finally {
125
126
  await rm(dir, { recursive: true, force: true });
126
127
  }
@@ -115,8 +115,13 @@ export interface BootOptions {
115
115
  *
116
116
  * @param signal - Signal name
117
117
  * @param payload - Payload
118
+ * @param meta - Optional envelope (producer run id for trace chains)
118
119
  */
119
- readonly onSignal?: (signal: string, payload: unknown) => void | Promise<void>;
120
+ readonly onSignal?: (
121
+ signal: string,
122
+ payload: unknown,
123
+ meta?: { readonly parentRunId?: string; readonly messageId?: string },
124
+ ) => void | Promise<void>;
120
125
  /**
121
126
  * Resume due durable runs — called on every scheduler tick when any flow
122
127
  * declares `durable: true` (claimDueSleep on the shared journal store).
@@ -257,8 +262,8 @@ async function loadBind<T>(name: string): Promise<T> {
257
262
  export async function bootApplication(input: BootOptions = {}): Promise<BootResult> {
258
263
  const docker =
259
264
  input.docker === true || (input.docker !== false && process.env.OKE_DOCKER === "1");
260
- // `-d` always selects the `docker` driver profile — not a mix of test/local +
261
- // prod store overrides (templates often pin `env: "test"` for harnesses).
265
+ // `-d` always selects the `docker` driver profile — not a mix of local/test +
266
+ // prod store overrides when `$options.env` is unset.
262
267
  const env: ConfigEnv = docker ? "docker" : (input.env ?? "local");
263
268
  let config = input.config;
264
269
  if (config === undefined) {