okengine 0.23.1 → 0.23.2

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 (83) hide show
  1. package/package.json +1 -1
  2. package/site/content/docs/ai/mcp.mdx +3 -3
  3. package/site/content/docs/elements/signal/broadcast.mdx +4 -2
  4. package/site/content/docs/reference/configuration.mdx +16 -0
  5. package/site/content/docs/reference/errors.mdx +31 -19
  6. package/src/compiler/aot.test.ts +3 -2
  7. package/src/compiler/aot.ts +45 -1
  8. package/src/compiler/dynamic.ts +1 -1
  9. package/src/compiler/http-parse.test.ts +101 -0
  10. package/src/compiler/http-parse.ts +188 -9
  11. package/src/compiler/interpret.ts +25 -2
  12. package/src/console/ui-next/dist/assets/{access-page-C_N4qvai.js → access-page-zBMhZnFm.js} +1 -1
  13. package/src/console/ui-next/dist/assets/{agent-disclosure-Cq1TxniW.js → agent-disclosure-CBj9f7Ju.js} +1 -1
  14. package/src/console/ui-next/dist/assets/{cache-glyph-BaI4uxZz.js → cache-glyph-D3MY49h3.js} +1 -1
  15. package/src/console/ui-next/dist/assets/{call-pii-button-DNyK7IIY.js → call-pii-button-L9JxdBhp.js} +1 -1
  16. package/src/console/ui-next/dist/assets/{collapsible-CRhBJMZR.js → collapsible-BvFaX-Zt.js} +1 -1
  17. package/src/console/ui-next/dist/assets/{decisions-page-D9twj8Xy.js → decisions-page-DgB76m-X.js} +1 -1
  18. package/src/console/ui-next/dist/assets/{duration-tone-CsHWlVPU.js → duration-tone-l1DSJ2Vz.js} +1 -1
  19. package/src/console/ui-next/dist/assets/{flows-page-ZaTJAhR9.js → flows-page-Bb9t1lu6.js} +1 -1
  20. package/src/console/ui-next/dist/assets/{highlighted-json-DQ-EMAfz.js → highlighted-json-DMvsi3Ti.js} +1 -1
  21. package/src/console/ui-next/dist/assets/{http-method-Cn_Fl79s.js → http-method-BwhJBTcQ.js} +1 -1
  22. package/src/console/ui-next/dist/assets/{index-CmRFeWdT.js → index-RhV2jT_7.js} +3 -3
  23. package/src/console/ui-next/dist/assets/{observability-page-Cn6R_dXz.js → observability-page-BRfUILBq.js} +1 -1
  24. package/src/console/ui-next/dist/assets/{replica-lag-Bx6EZhSe.js → replica-lag-BsceAzIG.js} +1 -1
  25. package/src/console/ui-next/dist/assets/{request-meta-DmdiUrfh.js → request-meta-C6lTyCXp.js} +1 -1
  26. package/src/console/ui-next/dist/assets/{store-page-8P6vGXnn.js → store-page-YmE806bJ.js} +1 -1
  27. package/src/console/ui-next/dist/assets/{trace-detail-sheet-BfGvpFSv.js → trace-detail-sheet-Di4irS49.js} +1 -1
  28. package/src/console/ui-next/dist/assets/{tree-expand-toggle-CWdUHbc0.js → tree-expand-toggle-DYeL6Rye.js} +1 -1
  29. package/src/console/ui-next/dist/assets/{units-page-CZOFZzay.js → units-page-07agasRA.js} +1 -1
  30. package/src/console/ui-next/dist/assets/{vault-page-ki_ishmX.js → vault-page-CHBck4n_.js} +1 -1
  31. package/src/console/ui-next/dist/index.html +1 -1
  32. package/src/console/ui-next/seed-parked-approval.ts +5 -1
  33. package/src/drivers/journal-postgres.test.ts +84 -1
  34. package/src/drivers/journal-postgres.ts +282 -32
  35. package/src/drivers/postgres.test.ts +60 -0
  36. package/src/drivers/postgres.ts +79 -5
  37. package/src/drivers/signal-compete.test.ts +6 -2
  38. package/src/drivers/signal-postgres.test.ts +170 -0
  39. package/src/drivers/signal-postgres.ts +449 -196
  40. package/src/drivers/signal-redis.test.ts +224 -0
  41. package/src/drivers/signal-redis.ts +835 -150
  42. package/src/elements/ai/approval.test.ts +15 -3
  43. package/src/elements/ai/approval.ts +15 -9
  44. package/src/elements/channel/runtime.ts +31 -4
  45. package/src/elements/channel/sql-ledger.test.ts +404 -0
  46. package/src/elements/channel/sql-ledger.ts +177 -33
  47. package/src/elements/clock/chaos-child.ts +12 -2
  48. package/src/elements/clock/durable.ts +11 -0
  49. package/src/elements/store/cache-bus.test.ts +121 -0
  50. package/src/elements/store/cache-bus.ts +278 -0
  51. package/src/elements/store/cache.test.ts +59 -0
  52. package/src/elements/store/cache.ts +157 -12
  53. package/src/elements/store/runtime.ts +44 -2
  54. package/src/elements/store/sql-errors.test.ts +131 -0
  55. package/src/elements/store/sql-errors.ts +39 -16
  56. package/src/elements/store/sql-nested-tx.test.ts +344 -0
  57. package/src/elements/store/sql-session.ts +183 -3
  58. package/src/kernel/app.ts +176 -28
  59. package/src/kernel/auto-registry.test.ts +5 -1
  60. package/src/kernel/boot-bind/signal.ts +13 -2
  61. package/src/kernel/boot-bind/store.ts +16 -1
  62. package/src/kernel/boot.test.ts +2 -2
  63. package/src/kernel/boot.ts +41 -11
  64. package/src/kernel/boundary-contract.ts +7 -0
  65. package/src/kernel/builtin-errors.ts +2 -0
  66. package/src/kernel/call.test.ts +1 -0
  67. package/src/kernel/fx-sql-handle.ts +7 -1
  68. package/src/kernel/horizontal-child.ts +54 -1
  69. package/src/kernel/horizontal.integration.test.ts +131 -7
  70. package/src/kernel/idempotency.test.ts +5 -6
  71. package/src/kernel/journal-boot.test.ts +83 -6
  72. package/src/kernel/journal.test.ts +44 -0
  73. package/src/kernel/journal.ts +171 -26
  74. package/src/mcp/authorization.ts +5 -11
  75. package/src/mcp/confirmation.ts +203 -38
  76. package/src/mcp/docs-server.ts +7 -2
  77. package/src/mcp/index.ts +4 -0
  78. package/src/mcp/mcp.test.ts +321 -33
  79. package/src/mcp/server.ts +8 -3
  80. package/src/mcp/session.ts +7 -0
  81. package/src/mcp/tools.ts +68 -19
  82. package/src/runtime/bun.ts +5 -0
  83. package/src/runtime/types.ts +5 -0
@@ -87,6 +87,80 @@ export class SharedPostgresPausedError extends Error {
87
87
  }
88
88
  }
89
89
 
90
+ /**
91
+ * Bun.SQL rejected with a value that is not an {@link Error}.
92
+ *
93
+ * {@link Error.cause} is the original rejection. `code`, `errno`,
94
+ * `constraint`, `table`, and `column` are copied when the driver sent them
95
+ * so SQLSTATE mapping can read a plain object the same way it reads an Error.
96
+ */
97
+ export class PostgresDriverError extends Error {
98
+ /** Driver `code` (`ERR_POSTGRES_*` or a five-character SQLSTATE). */
99
+ readonly code?: string;
100
+ /** SQLSTATE string, or a numeric errno when the driver sent one. */
101
+ readonly errno?: string | number;
102
+ /** Constraint name, when the driver sent one. */
103
+ readonly constraint?: string;
104
+ /** Table name, when the driver sent one. */
105
+ readonly table?: string;
106
+ /** Column name, when the driver sent one. */
107
+ readonly column?: string;
108
+
109
+ /**
110
+ * @param cause - Non-Error rejection from Bun.SQL
111
+ */
112
+ constructor(cause: unknown) {
113
+ super(postgresRejectionMessage(cause), { cause });
114
+ this.name = "PostgresDriverError";
115
+ if (cause === null || typeof cause !== "object") return;
116
+ const row = cause as Record<string, unknown>;
117
+ const code = readDriverString(row, "code");
118
+ const constraint = readDriverString(row, "constraint");
119
+ const table = readDriverString(row, "table");
120
+ const column = readDriverString(row, "column");
121
+ const errno = readDriverErrno(row);
122
+ if (code !== undefined) this.code = code;
123
+ if (errno !== undefined) this.errno = errno;
124
+ if (constraint !== undefined) this.constraint = constraint;
125
+ if (table !== undefined) this.table = table;
126
+ if (column !== undefined) this.column = column;
127
+ }
128
+ }
129
+
130
+ function postgresRejectionMessage(cause: unknown): string {
131
+ if (cause !== null && typeof cause === "object" && "message" in cause) {
132
+ const message = (cause as { message: unknown }).message;
133
+ if (typeof message === "string" && message.length > 0) return message;
134
+ }
135
+ return "Postgres query failed";
136
+ }
137
+
138
+ function readDriverString(row: Record<string, unknown>, key: string): string | undefined {
139
+ const value = row[key];
140
+ return typeof value === "string" && value.length > 0 ? value : undefined;
141
+ }
142
+
143
+ function readDriverErrno(row: Record<string, unknown>): string | number | undefined {
144
+ const value = row.errno;
145
+ if (typeof value === "number" && Number.isFinite(value)) return value;
146
+ if (typeof value === "string" && value.length > 0) return value;
147
+ return undefined;
148
+ }
149
+
150
+ /**
151
+ * Await a Bun.SQL call. Non-Error rejections become {@link PostgresDriverError}.
152
+ *
153
+ * @param query - Driver promise
154
+ */
155
+ async function settlePostgres<T>(query: PromiseLike<T>): Promise<T> {
156
+ try {
157
+ return await query;
158
+ } catch (err) {
159
+ if (err instanceof Error) throw err;
160
+ throw new PostgresDriverError(err);
161
+ }
162
+ }
163
+
90
164
  /**
91
165
  * Resolve the Postgres URL (injected, `DATABASE_URL`, then localhost).
92
166
  *
@@ -270,14 +344,14 @@ export async function withPinnedPostgres<T>(
270
344
  if (typeof client.reserve === "function") {
271
345
  const reserved = await client.reserve();
272
346
  try {
273
- await reserved.unsafe("BEGIN");
347
+ await settlePostgres(reserved.unsafe("BEGIN"));
274
348
  try {
275
349
  const result = await fn(reserved);
276
- await reserved.unsafe("COMMIT");
350
+ await settlePostgres(reserved.unsafe("COMMIT"));
277
351
  return result;
278
352
  } catch (err) {
279
353
  try {
280
- await reserved.unsafe("ROLLBACK");
354
+ await settlePostgres(reserved.unsafe("ROLLBACK"));
281
355
  } catch {
282
356
  // Already aborted.
283
357
  }
@@ -365,14 +439,14 @@ function wrapPostgresClient(
365
439
  // Shared wrappers outlive pause — fail soft before Bun.SQL reconnects.
366
440
  if (opts.shared) assertSharedPostgresReady();
367
441
  const pg = toPostgresParams(sql, params);
368
- const result = await client.unsafe(pg, [...params]);
442
+ const result = await settlePostgres(client.unsafe(pg, [...params]));
369
443
  if (Array.isArray(result)) return result as SqlRow[];
370
444
  return Array.from(result as ArrayLike<SqlRow>);
371
445
  },
372
446
  async exec(sql, params = []) {
373
447
  if (opts.shared) assertSharedPostgresReady();
374
448
  const pg = toPostgresParams(sql, params);
375
- const result = await client.unsafe(pg, [...params]);
449
+ const result = await settlePostgres(client.unsafe(pg, [...params]));
376
450
  if (
377
451
  result &&
378
452
  typeof result === "object" &&
@@ -13,6 +13,7 @@ import {
13
13
  createBunSignalRedisClient,
14
14
  createSignalRedisFake,
15
15
  openRedisSignal,
16
+ SIGNAL_REDIS_MIN_IDLE_MS,
16
17
  } from "./signal-redis.ts";
17
18
 
18
19
  function decls(name: string, retries = 3) {
@@ -88,9 +89,11 @@ describe("redis streams competing consumers", () => {
88
89
  });
89
90
 
90
91
  test("another consumer reclaims an unacked entry", async () => {
91
- const redis = createSignalRedisFake();
92
+ let nowMs = 0;
93
+ const now = () => nowMs;
94
+ const redis = createSignalRedisFake({ now });
92
95
  const name = "handoff";
93
- const shared = { signals: decls(name, 3), redis, compete: true as const };
96
+ const shared = { signals: decls(name, 3), redis, compete: true as const, now };
94
97
  const a = await openRedisSignal({ ...shared, consumerId: "a" });
95
98
  const b = await openRedisSignal({ ...shared, consumerId: "b" });
96
99
  let aCalls = 0;
@@ -104,6 +107,7 @@ describe("redis streams competing consumers", () => {
104
107
  });
105
108
  await a.emit(name, { id: "1" });
106
109
  await a.drain();
110
+ nowMs += SIGNAL_REDIS_MIN_IDLE_MS;
107
111
  await b.drain();
108
112
  expect(aCalls).toBe(1);
109
113
  expect(got).toEqual(["b"]);
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Postgres broadcast/live fan-out: shared table, per-instance cursor.
3
+ */
4
+
5
+ import { describe, expect, test } from "bun:test";
6
+ import { signal } from "../elements/signal.ts";
7
+ import type { SignalDecl } from "../elements/signal/declare.ts";
8
+ import {
9
+ POSTGRES_SIGNAL_FANOUT_SQL,
10
+ createPostgresSignalFake,
11
+ openPostgresSignal,
12
+ postgresSignalFakeBranches,
13
+ } from "./signal-postgres.ts";
14
+
15
+ const legacyInsert = `INSERT INTO oke_signal_messages (id, signal, payload, ordering_key, delivery, attempts, failures, created_at, available_at, status, locked_by, lease_expires_at, delivered_to, parent_run_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
16
+
17
+ async function expectRejects(pending: Promise<unknown>, message: string): Promise<void> {
18
+ let err: unknown;
19
+ try {
20
+ await pending;
21
+ } catch (caught) {
22
+ err = caught;
23
+ }
24
+ if (!(err instanceof Error)) {
25
+ throw new Error("expected the statement to throw");
26
+ }
27
+ expect(err.message).toContain(message);
28
+ }
29
+
30
+ describe("postgres signal fake SQL branches", () => {
31
+ test("an unmatched statement throws", async () => {
32
+ const sql = createPostgresSignalFake();
33
+ await expectRejects(sql.query("SELECT 1"), "unsupported query");
34
+ await expectRejects(sql.exec("VACUUM oke_signal_messages"), "unsupported exec");
35
+ });
36
+
37
+ test("broadcast/live statements each hit exactly one branch", async () => {
38
+ const sql = createPostgresSignalFake({ now: () => 4_200 });
39
+ expect(postgresSignalFakeBranches(POSTGRES_SIGNAL_FANOUT_SQL.insert)).toEqual(["insert-db-at"]);
40
+ expect(postgresSignalFakeBranches(POSTGRES_SIGNAL_FANOUT_SQL.poll)).toEqual(["fanout-poll"]);
41
+ expect(postgresSignalFakeBranches(POSTGRES_SIGNAL_FANOUT_SQL.backfill)).toEqual([
42
+ "backfill-db-at",
43
+ ]);
44
+ expect(postgresSignalFakeBranches(POSTGRES_SIGNAL_FANOUT_SQL.index)).toEqual([
45
+ "index-delivery-db-at",
46
+ ]);
47
+ expect(postgresSignalFakeBranches(legacyInsert)).toEqual(["insert-message"]);
48
+
49
+ await sql.exec(legacyInsert, [
50
+ "old",
51
+ "s",
52
+ "null",
53
+ null,
54
+ "broadcast",
55
+ 0,
56
+ "[]",
57
+ 50,
58
+ 50,
59
+ "pending",
60
+ null,
61
+ null,
62
+ "[]",
63
+ null,
64
+ ]);
65
+ await sql.exec(POSTGRES_SIGNAL_FANOUT_SQL.backfill);
66
+ await sql.exec(POSTGRES_SIGNAL_FANOUT_SQL.index);
67
+ await sql.exec(POSTGRES_SIGNAL_FANOUT_SQL.insert, [
68
+ "new",
69
+ "s",
70
+ "null",
71
+ null,
72
+ "broadcast",
73
+ 0,
74
+ "[]",
75
+ 1,
76
+ 1,
77
+ "pending",
78
+ null,
79
+ null,
80
+ "[]",
81
+ null,
82
+ ]);
83
+
84
+ const rows = await sql.query(POSTGRES_SIGNAL_FANOUT_SQL.poll, [0]);
85
+ expect(rows.find((row) => row.id === "old")?.db_at).toBe(50);
86
+ const fresh = rows.find((row) => row.id === "new");
87
+ expect(fresh?.db_at).toBe(4_200);
88
+ expect(fresh?.db_now).toBe(4_200);
89
+ });
90
+ });
91
+
92
+ describe("postgres broadcast and live fan-out", () => {
93
+ test("a broadcast and a live message on A reach B", async () => {
94
+ const sql = createPostgresSignalFake();
95
+ const news = signal.broadcast("news");
96
+ const feed = signal.live("feed", { optional: true });
97
+ const signals = new Map<string, SignalDecl>([
98
+ [news.name, news],
99
+ [feed.name, feed],
100
+ ]);
101
+ const a = await openPostgresSignal({ signals, sql });
102
+ const b = await openPostgresSignal({ signals, sql });
103
+ const got: string[] = [];
104
+ await a.subscribe("news", "a", async () => {
105
+ got.push("a");
106
+ });
107
+ await b.subscribe("news", "b", async () => {
108
+ got.push("b");
109
+ });
110
+
111
+ const iterA = a.live("feed")[Symbol.asyncIterator]();
112
+ const iterB = b.live("feed")[Symbol.asyncIterator]();
113
+ const nextA = iterA.next();
114
+ const nextB = iterB.next();
115
+ await Bun.sleep(10);
116
+
117
+ await a.emit("news", { id: "1" });
118
+ await a.emit("feed", { n: 1 });
119
+ await a.drain();
120
+ await b.drain();
121
+
122
+ expect(got.slice().sort()).toEqual(["a", "b"]);
123
+ const [liveA, liveB] = await Promise.all([nextA, nextB]);
124
+ expect(liveA.value?.payload).toEqual({ n: 1 });
125
+ expect(liveB.value?.payload).toEqual({ n: 1 });
126
+ await iterA.return?.();
127
+ await iterB.return?.();
128
+ await a.close();
129
+ await b.close();
130
+ });
131
+
132
+ test("cursor starts at head so a row written before boot is not delivered", async () => {
133
+ let t = 5_000;
134
+ const sql = createPostgresSignalFake({ now: () => t });
135
+ const news = signal.broadcast("news", { optional: true });
136
+ const signals = new Map<string, SignalDecl>([[news.name, news]]);
137
+ const writer = await openPostgresSignal({ signals, sql, lagMs: 30_000 });
138
+ await writer.emit("news", { n: 1 });
139
+ await writer.close();
140
+
141
+ t = 6_000;
142
+ const reader = await openPostgresSignal({ signals, sql, lagMs: 30_000 });
143
+ const got: unknown[] = [];
144
+ await reader.subscribe("news", "b", async (message) => {
145
+ got.push(message.payload);
146
+ });
147
+ await reader.drain();
148
+ expect(got).toEqual([]);
149
+ await reader.close();
150
+ });
151
+
152
+ test("lagMs dedupes the same id across overlapping polls", async () => {
153
+ let t = 10_000;
154
+ const sql = createPostgresSignalFake({ now: () => t });
155
+ const news = signal.broadcast("news");
156
+ const signals = new Map<string, SignalDecl>([[news.name, news]]);
157
+ const bus = await openPostgresSignal({ signals, sql, lagMs: 30_000 });
158
+ const got: unknown[] = [];
159
+ await bus.subscribe("news", "a", async (message) => {
160
+ got.push(message.payload);
161
+ });
162
+ await bus.emit("news", { n: 1 });
163
+ t = 15_000;
164
+ await bus.drain();
165
+ t = 20_000;
166
+ await bus.drain();
167
+ expect(got).toEqual([{ n: 1 }]);
168
+ await bus.close();
169
+ });
170
+ });