okengine 0.9.1 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/AGENTS.md +1 -1
  2. package/manifest.v1.schema.json +9 -1
  3. package/package.json +3 -3
  4. package/site/content/docs/console/runs.mdx +10 -0
  5. package/site/content/docs/console/traces.mdx +4 -0
  6. package/site/content/docs/elements/ai.mdx +54 -32
  7. package/site/content/docs/elements/channel.mdx +97 -9
  8. package/site/content/docs/elements/clock.mdx +1 -0
  9. package/site/content/docs/elements/flow.mdx +42 -8
  10. package/site/content/docs/get-started/basic-usage.mdx +5 -1
  11. package/site/content/docs/get-started/installation.mdx +9 -5
  12. package/site/content/docs/meta.json +2 -0
  13. package/site/content/docs/providers/azure-redis.mdx +85 -0
  14. package/site/content/docs/providers/cockroachdb.mdx +86 -0
  15. package/site/content/docs/providers/digitalocean-caching.mdx +88 -0
  16. package/site/content/docs/providers/dragonfly-cloud.mdx +82 -0
  17. package/site/content/docs/providers/elasticache.mdx +92 -0
  18. package/site/content/docs/providers/index.mdx +101 -0
  19. package/site/content/docs/providers/memorystore.mdx +89 -0
  20. package/site/content/docs/providers/meta.json +20 -0
  21. package/site/content/docs/providers/neon.mdx +115 -0
  22. package/site/content/docs/providers/redis-cloud.mdx +93 -0
  23. package/site/content/docs/providers/supabase.mdx +117 -0
  24. package/site/content/docs/providers/upstash.mdx +91 -0
  25. package/site/content/docs/providers/yugabytedb.mdx +95 -0
  26. package/site/content/docs/recipes/caddy.mdx +128 -0
  27. package/site/content/docs/recipes/cockroachdb.mdx +127 -0
  28. package/site/content/docs/recipes/dragonfly.mdx +125 -0
  29. package/site/content/docs/recipes/index.mdx +146 -0
  30. package/site/content/docs/recipes/llama-cpp.mdx +153 -0
  31. package/site/content/docs/recipes/mailpit.mdx +136 -0
  32. package/site/content/docs/recipes/meilisearch.mdx +139 -0
  33. package/site/content/docs/recipes/meta.json +31 -0
  34. package/site/content/docs/recipes/ollama.mdx +142 -0
  35. package/site/content/docs/recipes/openbao.mdx +141 -0
  36. package/site/content/docs/recipes/pgdog.mdx +137 -0
  37. package/site/content/docs/recipes/postgres.mdx +136 -0
  38. package/site/content/docs/recipes/redis.mdx +130 -0
  39. package/site/content/docs/recipes/rustfs.mdx +148 -0
  40. package/site/content/docs/recipes/sglang.mdx +105 -0
  41. package/site/content/docs/recipes/supabase-docker.mdx +145 -0
  42. package/site/content/docs/recipes/timescale.mdx +129 -0
  43. package/site/content/docs/recipes/traefik.mdx +136 -0
  44. package/site/content/docs/recipes/valkey.mdx +121 -0
  45. package/site/content/docs/recipes/vllm.mdx +106 -0
  46. package/site/content/docs/recipes/yugabytedb.mdx +135 -0
  47. package/site/content/docs/reference/cli.md +42 -6
  48. package/site/content/docs/reference/configuration.mdx +5 -3
  49. package/site/content/docs/reference/environment-variables.mdx +12 -12
  50. package/site/content/docs/reference/fx.mdx +38 -4
  51. package/src/cli/ai-setup/ai-setup.test.ts +32 -12
  52. package/src/cli/ai-setup/apply.ts +65 -23
  53. package/src/cli/ai-setup/catalog.ts +1316 -35
  54. package/src/cli/ai-setup/detect-ollama.ts +47 -0
  55. package/src/cli/ai-setup/index.ts +44 -7
  56. package/src/cli/ai-setup/prompts.ts +430 -486
  57. package/src/cli/ai-setup/recommend.test.ts +150 -50
  58. package/src/cli/ai-setup/recommend.ts +118 -101
  59. package/src/cli/ask-seed.test.ts +68 -0
  60. package/src/cli/ask-seed.ts +79 -0
  61. package/src/cli/db-seed-redact.test.ts +25 -0
  62. package/src/cli/db-seed.ts +36 -7
  63. package/src/cli/db.test.ts +1 -1
  64. package/src/cli/db.ts +2 -2
  65. package/src/cli/dev-app-runner.ts +6 -0
  66. package/src/cli/dev-controls.test.ts +107 -0
  67. package/src/cli/dev-controls.ts +354 -0
  68. package/src/cli/dev-schema-sync.ts +12 -4
  69. package/src/cli/dev.test.ts +50 -30
  70. package/src/cli/dev.ts +633 -66
  71. package/src/cli/docker-cli.test.ts +15 -0
  72. package/src/cli/drizzle-env.test.ts +40 -6
  73. package/src/cli/drizzle-env.ts +22 -1
  74. package/src/cli/ensure-drizzle-config.ts +6 -4
  75. package/src/cli/hero-meta.test.ts +13 -1
  76. package/src/cli/hero-meta.ts +28 -6
  77. package/src/cli/index.ts +5 -0
  78. package/src/cli/registry.ts +24 -0
  79. package/src/cli/replay.test.ts +105 -0
  80. package/src/cli/replay.ts +206 -0
  81. package/src/cli/start.ts +12 -1
  82. package/src/compiler/effects-infer.ts +37 -8
  83. package/src/compiler/extract.test.ts +125 -0
  84. package/src/compiler/extract.ts +31 -6
  85. package/src/config/index.ts +3 -3
  86. package/src/console/server/claim.ts +3 -1
  87. package/src/console/ui/dist/assets/{index-ClX0ZCe5.js → index-D-9KFce5.js} +2 -2
  88. package/src/console/ui/dist/assets/panel-overview-CW0CQEc2.js +1 -0
  89. package/src/console/ui/dist/assets/panel-runs-BxuyDI3x.js +1 -0
  90. package/src/console/ui/dist/assets/{panel-signals-Dsxdu_AR.js → panel-signals-BXofbj7v.js} +1 -1
  91. package/src/console/ui/dist/assets/{panel-store-hy7O8HOs.js → panel-store-B8mH-arU.js} +1 -1
  92. package/src/console/ui/dist/assets/{panel-traces-CBiAe5go.js → panel-traces-DSA-ZWnf.js} +1 -1
  93. package/src/console/ui/dist/index.html +1 -1
  94. package/src/console/ui/overview/slo.ts +31 -3
  95. package/src/console/ui/overview/types.ts +6 -0
  96. package/src/console/ui/runs/errors.test.ts +53 -0
  97. package/src/console/ui/runs/errors.ts +88 -0
  98. package/src/console/ui/runs/index.ts +8 -0
  99. package/src/console/ui/runs/search.ts +16 -0
  100. package/src/console/ui/shell/panels/runs/RunsPanel.tsx +72 -1
  101. package/src/docker/ai-model-status.test.ts +101 -0
  102. package/src/docker/ai-model-status.ts +276 -0
  103. package/src/docker/compose-health.test.ts +115 -0
  104. package/src/docker/compose-health.ts +234 -0
  105. package/src/docker/compose.ts +24 -8
  106. package/src/docker/derive.ts +35 -9
  107. package/src/docker/docker.test.ts +304 -12
  108. package/src/docker/helpers.ts +56 -0
  109. package/src/docker/index.ts +52 -0
  110. package/src/docker/ollama-pull.ts +232 -0
  111. package/src/docker/recipes/cockroach.ts +24 -0
  112. package/src/docker/recipes/index.ts +32 -0
  113. package/src/docker/recipes/llama-cpp.ts +273 -0
  114. package/src/docker/recipes/ollama.ts +19 -23
  115. package/src/docker/recipes/pgdog.ts +4 -1
  116. package/src/docker/recipes/postgres.ts +5 -18
  117. package/src/docker/recipes/sglang.ts +55 -0
  118. package/src/docker/recipes/supabase.ts +21 -0
  119. package/src/docker/recipes/timescale.ts +18 -0
  120. package/src/docker/recipes/vllm.ts +44 -0
  121. package/src/docker/recipes/yugabyte.ts +23 -0
  122. package/src/docker/stack-id.test.ts +22 -0
  123. package/src/docker/stack-id.ts +38 -4
  124. package/src/docker/types.ts +15 -0
  125. package/src/drivers/ai-ollama.integration.test.ts +7 -4
  126. package/src/drivers/signal-engine.ts +10 -0
  127. package/src/drivers/signal-postgres.ts +24 -2
  128. package/src/drivers/signal-types.ts +10 -0
  129. package/src/elements/channel/fallback.test.ts +91 -0
  130. package/src/elements/channel/hard-bounce.test.ts +82 -0
  131. package/src/elements/channel/injection.test.ts +153 -0
  132. package/src/elements/channel/locale-ar.test.ts +102 -0
  133. package/src/elements/channel/otp-delivery.ts +5 -2
  134. package/src/elements/channel/receipts-ledger.test.ts +100 -0
  135. package/src/elements/channel/runtime.ts +24 -1
  136. package/src/elements/channel/test-helpers.ts +60 -0
  137. package/src/elements/channel/whatsapp-compliance.test.ts +124 -0
  138. package/src/elements/store/cache.test.ts +28 -0
  139. package/src/elements/store/cache.ts +1 -1
  140. package/src/elements/store/emit-drizzle.ts +43 -9
  141. package/src/elements/store/runtime.ts +1 -1
  142. package/src/elements/store/schema-decl.test.ts +2 -0
  143. package/src/elements/store/schema-decl.ts +15 -0
  144. package/src/elements/store/seed.ts +1 -1
  145. package/src/index.ts +1 -0
  146. package/src/kernel/app.ts +131 -10
  147. package/src/kernel/boot-bind/signal.ts +6 -2
  148. package/src/kernel/boot-bind/store.ts +2 -1
  149. package/src/kernel/boot.test.ts +6 -5
  150. package/src/kernel/boot.ts +113 -10
  151. package/src/kernel/compensate.test.ts +131 -0
  152. package/src/kernel/correlation.test.ts +108 -0
  153. package/src/kernel/effect-timing.test.ts +82 -0
  154. package/src/kernel/effects-stamping.test.ts +207 -0
  155. package/src/kernel/errors.ts +11 -0
  156. package/src/kernel/flow.ts +38 -0
  157. package/src/kernel/fx-runs.test.ts +49 -0
  158. package/src/kernel/fx.test.ts +14 -0
  159. package/src/kernel/fx.ts +153 -14
  160. package/src/manifest/sql-resource.ts +41 -0
  161. package/src/manifest/types.ts +8 -2
  162. package/src/runs/collect.ts +3 -0
  163. package/src/runs/export-otlp.ts +64 -0
  164. package/src/runs/index.ts +16 -0
  165. package/src/runs/parquet.ts +9 -0
  166. package/src/runs/types.ts +5 -0
  167. package/src/runs/window.test.ts +68 -0
  168. package/src/runs/window.ts +148 -0
  169. package/src/runtime/boot-warn.test.ts +45 -0
  170. package/src/runtime/boot-warn.ts +17 -0
  171. package/src/runtime/dev-request-log.ts +5 -5
  172. package/src/term.test.ts +95 -10
  173. package/src/term.ts +369 -14
  174. package/src/console/ui/dist/assets/panel-overview-DONDzBd2.js +0 -1
  175. package/src/console/ui/dist/assets/panel-runs-D5zf-D9c.js +0 -1
@@ -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
+ });
@@ -43,4 +43,32 @@ describe("tier-1 cache — exact per-resource invalidation (path b)", () => {
43
43
  expect(cache.invalidateFromEffects({ writes: ["sql:links"] }).keys).toEqual(keys);
44
44
  expect(cache.get(keys[0]!)).toBeUndefined();
45
45
  });
46
+
47
+ test("write to table A does not invalidate a cached read of table B in the same sql store", () => {
48
+ // Two tables under one `store.sql("app", …)` — the exact shape Direction
49
+ // B's per-table kernel resolution (fx.ts `gatedTable`) now produces:
50
+ // `sql:notes` / `sql:orders`, not the old coarse `sql:app` for both.
51
+ // This precision was already correct in cache.ts before Direction B —
52
+ // it simply never received per-table refs to prove it with. Confirmed
53
+ // here with the exact naming the kernel now emits.
54
+ const cache = createStoreCache();
55
+ const notesRead: Effects = { reads: ["sql:notes"] };
56
+ const keys = tier1KeysForReads(notesRead);
57
+
58
+ cache.set({
59
+ tier: 1,
60
+ key: keys[0]!,
61
+ value: [{ id: "n1" }],
62
+ resources: ["sql:notes"],
63
+ expiresAt: null,
64
+ });
65
+
66
+ // A write to "orders" — a different table, same store — must not touch it.
67
+ expect(cache.invalidateFromEffects({ writes: ["sql:orders"] }).keys).toEqual([]);
68
+ expect(cache.get(keys[0]!)).toBeDefined();
69
+
70
+ // A write to "notes" itself still invalidates.
71
+ expect(cache.invalidateFromEffects({ writes: ["sql:notes"] }).keys).toEqual(keys);
72
+ expect(cache.get(keys[0]!)).toBeUndefined();
73
+ });
46
74
  });
@@ -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,
@@ -160,6 +162,18 @@ export interface OkeOptions {
160
162
  readonly docker?: BootOptions["docker"];
161
163
  /** Optional `oke.config.ts` document consumed at boot. */
162
164
  readonly config?: BootOptions["config"];
165
+ /**
166
+ * Compiled Manifest for flows with no hand-declared `effects` — highest
167
+ * priority source for the boot-time capability stamp (see
168
+ * {@link BootOptions.manifest}).
169
+ */
170
+ readonly manifest?: BootOptions["manifest"];
171
+ /**
172
+ * Project root for a lazy, best-effort effects extraction when a flow has
173
+ * no hand-declared `effects` and no {@link manifest} was given (see
174
+ * {@link BootOptions.rootDir}).
175
+ */
176
+ readonly rootDir?: BootOptions["rootDir"];
163
177
  /** Pre-built element runtimes (skip construction at boot when present). */
164
178
  readonly elements?: BootOptions["elements"];
165
179
  /** Vault create options (chain / allowDevFallbacks / secrets). */
@@ -359,6 +373,10 @@ export interface OkeApp<D extends Record<string, unknown> = {}, R extends AppRou
359
373
  readonly operator?: FxOperator;
360
374
  /** Pre-resolved principal (either plane). */
361
375
  readonly principal?: ResolvedPrincipal;
376
+ /** Parent WideEvent id when this execution was caused by another. */
377
+ readonly parentId?: string;
378
+ /** Explicit run / WideEvent id (defaults to a new UUID). */
379
+ readonly runId?: string;
362
380
  },
363
381
  ): Promise<ExecuteResult>;
364
382
  /**
@@ -381,8 +399,13 @@ export interface OkeApp<D extends Record<string, unknown> = {}, R extends AppRou
381
399
  *
382
400
  * @param signal - Signal name or handle
383
401
  * @param payload - Payload
402
+ * @param meta - Optional envelope from the bus (producer run id)
384
403
  */
385
- dispatchSignal(signal: NamedRef, payload?: unknown): Promise<ExecuteResult[]>;
404
+ dispatchSignal(
405
+ signal: NamedRef,
406
+ payload?: unknown,
407
+ meta?: { readonly parentRunId?: string; readonly messageId?: string },
408
+ ): Promise<ExecuteResult[]>;
386
409
  /**
387
410
  * Invoke all flows bound to an `every` interval.
388
411
  *
@@ -637,8 +660,12 @@ export function oke(options: OkeOptions): OkeApp {
637
660
  }
638
661
  }
639
662
 
640
- async function handleSignalFire(name: string, payload: unknown): Promise<void> {
641
- await app.dispatchSignal(name, payload);
663
+ async function handleSignalFire(
664
+ name: string,
665
+ payload: unknown,
666
+ meta?: { readonly parentRunId?: string; readonly messageId?: string },
667
+ ): Promise<void> {
668
+ await app.dispatchSignal(name, payload, meta);
642
669
  }
643
670
 
644
671
  async function handleDurableResume(): Promise<void> {
@@ -812,6 +839,8 @@ export function oke(options: OkeOptions): OkeApp {
812
839
  env: bootEnv,
813
840
  docker: overrides?.docker ?? options.docker,
814
841
  config: overrides?.config ?? options.config,
842
+ manifest: overrides?.manifest ?? options.manifest,
843
+ rootDir: overrides?.rootDir ?? options.rootDir,
815
844
  elements: overrides?.elements ?? options.elements,
816
845
  secrets: [...baseSecrets, ...pluginSecrets],
817
846
  vault: overrides?.vault ?? options.vault,
@@ -918,6 +947,10 @@ export function oke(options: OkeOptions): OkeApp {
918
947
  readonly originPrincipal?: FxPrincipal;
919
948
  /** Explicit locale override for {@link Fx.t} / channel sends. */
920
949
  readonly locale?: string;
950
+ /** Parent WideEvent id when this execution was caused by another. */
951
+ readonly parentId?: string;
952
+ /** Explicit run / WideEvent id (defaults to a new UUID). */
953
+ readonly runId?: string;
921
954
  },
922
955
  ): Promise<ExecuteResult> {
923
956
  registerFlow(flowDef);
@@ -957,6 +990,8 @@ export function oke(options: OkeOptions): OkeApp {
957
990
  readonly principal?: ResolvedPrincipal;
958
991
  readonly originPrincipal?: FxPrincipal;
959
992
  readonly locale?: string;
993
+ readonly parentId?: string;
994
+ readonly runId?: string;
960
995
  };
961
996
  readonly resolvedLocale: string;
962
997
  readonly defaultLocale: string;
@@ -991,6 +1026,15 @@ export function oke(options: OkeOptions): OkeApp {
991
1026
  const now = options.fx?.now ?? booted?.clock?.now ?? (() => Date.now());
992
1027
  const startedAt = now();
993
1028
  const telemetry = createRunTelemetry();
1029
+ const inboundId =
1030
+ extras?.request?.headers.get("x-request-id")?.trim() ||
1031
+ extras?.request?.headers.get("x-correlation-id")?.trim() ||
1032
+ undefined;
1033
+ const runId =
1034
+ extras?.runId ??
1035
+ (inboundId && inboundId.length > 0 ? inboundId : undefined) ??
1036
+ crypto.randomUUID();
1037
+ const parentId = extras?.parentId;
994
1038
 
995
1039
  // Element pipeline wiring only kicks in once booted — `autoBoot: false`
996
1040
  // keeps intentional pre-boot unit tests ungated (no elements either).
@@ -1057,6 +1101,7 @@ export function oke(options: OkeOptions): OkeApp {
1057
1101
  const journal = createJournal({
1058
1102
  store,
1059
1103
  now,
1104
+ id: () => runId,
1060
1105
  // Hold the run lease for the request's lifetime — a crash mid-run
1061
1106
  // leaves an expired lease another instance can reclaim and resume.
1062
1107
  ...(hasJournalLease(store) ? { lease: { instanceId, leaseMs } } : {}),
@@ -1071,6 +1116,7 @@ export function oke(options: OkeOptions): OkeApp {
1071
1116
  flow: flowDef.name,
1072
1117
  effects: flowDef.effects,
1073
1118
  runTelemetry: telemetry,
1119
+ runId,
1074
1120
  now,
1075
1121
  i18n: {
1076
1122
  locale: resolvedLocale,
@@ -1090,6 +1136,7 @@ export function oke(options: OkeOptions): OkeApp {
1090
1136
  aiRuntime: booted.ai,
1091
1137
  }
1092
1138
  : {}),
1139
+ runsRuntime: booted?.runs ?? (isRunsRuntimeLike(options.runs) ? options.runs : undefined),
1093
1140
  ...(journalSession ? { journal: journalSession, durable: true } : {}),
1094
1141
  callHandler: async (name, callInput) => {
1095
1142
  const target = flowsByName.get(name);
@@ -1103,6 +1150,7 @@ export function oke(options: OkeOptions): OkeApp {
1103
1150
  {
1104
1151
  originPrincipal: freezePrincipal(fx.principal),
1105
1152
  locale: resolvedLocale,
1153
+ parentId: runId,
1106
1154
  },
1107
1155
  );
1108
1156
  if (inner.failure) return inner.failure;
@@ -1174,10 +1222,32 @@ export function oke(options: OkeOptions): OkeApp {
1174
1222
  input: ctx.input,
1175
1223
  wakeAt: sleeping.wakeAt,
1176
1224
  });
1177
- } else if (!sleeping && result.failure) {
1178
- await journalSession.commit("failed", {
1179
- error: result.failure.error.code,
1180
- });
1225
+ } else if (!sleeping && isTerminalFailure(result)) {
1226
+ const terminalErr = result.failure ?? result.ctx.error;
1227
+ if (flowDef.compensate) {
1228
+ try {
1229
+ const completedSteps = journalSession.run.entries
1230
+ .filter((e) => e.kind === "step")
1231
+ .map((e) => e.name);
1232
+ await flowDef.compensate(
1233
+ {
1234
+ input: ctx.input as never,
1235
+ error: terminalErr,
1236
+ completedSteps,
1237
+ },
1238
+ fx,
1239
+ );
1240
+ } catch (compErr) {
1241
+ await journalSession.commit("failed", {
1242
+ error: `compensate:${failureCodeOf(compErr)}`,
1243
+ });
1244
+ }
1245
+ }
1246
+ if (journalSession.run.status === "running") {
1247
+ await journalSession.commit("failed", {
1248
+ error: failureCodeOf(terminalErr),
1249
+ });
1250
+ }
1181
1251
  } else if (!sleeping) {
1182
1252
  await journalSession.commit("completed", { output: result.output });
1183
1253
  }
@@ -1187,6 +1257,10 @@ export function oke(options: OkeOptions): OkeApp {
1187
1257
  booted?.runs ?? (isRunsRuntimeLike(options.runs) ? options.runs : undefined);
1188
1258
  if (runsRuntime) {
1189
1259
  const archiveCleartext = archiveFromInput(ctx.input, options.archiveInputFields);
1260
+ const replayInput = redactArchivedFields(ctx.input, options.archiveInputFields);
1261
+ const recordFailure =
1262
+ result.failure ??
1263
+ (isTerminalFailure(result) ? failureFromUnknown(result.ctx.error) : undefined);
1190
1264
  await runsRuntime.record(
1191
1265
  {
1192
1266
  flow: flowDef,
@@ -1196,7 +1270,10 @@ export function oke(options: OkeOptions): OkeApp {
1196
1270
  telemetry,
1197
1271
  startedAt,
1198
1272
  endedAt,
1199
- failure: result.failure,
1273
+ failure: recordFailure,
1274
+ id: runId,
1275
+ input: replayInput,
1276
+ ...(parentId !== undefined ? { parentId } : {}),
1200
1277
  },
1201
1278
  archiveCleartext,
1202
1279
  );
@@ -1480,12 +1557,16 @@ export function oke(options: OkeOptions): OkeApp {
1480
1557
 
1481
1558
  return respond(encodeExecuteResult(result));
1482
1559
  },
1483
- async dispatchSignal(signal, payload) {
1560
+ async dispatchSignal(signal, payload, meta) {
1484
1561
  const name = resolveName(signal);
1485
1562
  const results: ExecuteResult[] = [];
1486
1563
  for (const b of adopted) {
1487
1564
  if (b.trigger.kind === "signal" && b.trigger.name === name) {
1488
- results.push(await execute(b.flow, payload, b.trigger as SignalAsTrigger));
1565
+ results.push(
1566
+ await execute(b.flow, payload, b.trigger as SignalAsTrigger, {
1567
+ ...(meta?.parentRunId !== undefined ? { parentId: meta.parentRunId } : {}),
1568
+ }),
1569
+ );
1489
1570
  }
1490
1571
  }
1491
1572
  return results;
@@ -1555,6 +1636,26 @@ function compileHttpBinding(binding: Binding, aot: boolean): CompiledRoute {
1555
1636
  );
1556
1637
  }
1557
1638
 
1639
+ /** True when the pipeline ended in a terminal failure (not sleep park). */
1640
+ function isTerminalFailure(result: PipelineResult): boolean {
1641
+ if (result.failure) return true;
1642
+ return result.ctx.error !== undefined && !isJournalSuspend(result.ctx.error);
1643
+ }
1644
+
1645
+ /** Machine-readable code for journal / Runs failure fields. */
1646
+ function failureCodeOf(err: unknown): string {
1647
+ if (isFlowFailure(err)) return err.error.code;
1648
+ if (err instanceof Error && err.name && err.name !== "Error") return err.name;
1649
+ if (err instanceof Error && err.message) return err.message.slice(0, 120);
1650
+ return "Error";
1651
+ }
1652
+
1653
+ /** Coerce a thrown value into a FlowFailure for Runs wide events. */
1654
+ function failureFromUnknown(err: unknown): FlowFailure {
1655
+ if (isFlowFailure(err)) return err;
1656
+ return fail(failureCodeOf(err), {});
1657
+ }
1658
+
1558
1659
  /**
1559
1660
  * Pull personal fields from validated input for crypto-shred archival.
1560
1661
  *
@@ -1576,6 +1677,26 @@ function archiveFromInput(
1576
1677
  return Object.keys(out).length > 0 ? out : undefined;
1577
1678
  }
1578
1679
 
1680
+ /**
1681
+ * Clone input with archived personal fields replaced by shred markers so the
1682
+ * replay snapshot never retains cleartext for those keys.
1683
+ *
1684
+ * @param input - Validated flow input
1685
+ * @param fields - Property names that go to {@link archiveFromInput}
1686
+ */
1687
+ function redactArchivedFields(input: unknown, fields: readonly string[] | undefined): unknown {
1688
+ if (input === undefined) return undefined;
1689
+ if (!fields || fields.length === 0) return input;
1690
+ if (!input || typeof input !== "object" || Array.isArray(input)) return input;
1691
+ const obj = { ...(input as Record<string, unknown>) };
1692
+ for (const name of fields) {
1693
+ if (typeof obj[name] === "string" && (obj[name] as string).length > 0) {
1694
+ obj[name] = "[archived]";
1695
+ }
1696
+ }
1697
+ return obj;
1698
+ }
1699
+
1579
1700
  /**
1580
1701
  * Deep-merge channel template catalogs (later parts win per locale).
1581
1702
  *
@@ -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).",