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
@@ -1,9 +1,11 @@
1
1
  /**
2
2
  * `postgres` signal driver — transactional emit (dual-write fix).
3
3
  *
4
- * `fx.emit` inserts into the outbox on the caller's connection. Consumers
5
- * claim with `FOR UPDATE SKIP LOCKED`. Wakeups use `LISTEN` / `NOTIFY` when
6
- * the client has them; otherwise the boot scheduler polls `drain`.
4
+ * `fx.emit` inserts into the outbox on the caller's connection. `once`
5
+ * consumers claim with `FOR UPDATE SKIP LOCKED`. Broadcast and live stay
6
+ * unread in the table; each instance polls `db_at` from an in-memory cursor.
7
+ * Wakeups use `LISTEN` / `NOTIFY` when the client has them; otherwise the
8
+ * boot scheduler polls `drain`.
7
9
  */
8
10
 
9
11
  import type { SignalDecl } from "../elements/signal/declare.ts";
@@ -52,6 +54,8 @@ interface MsgRow {
52
54
  lease_expires_at: number | null;
53
55
  delivered_to: string;
54
56
  parent_run_id: string | null;
57
+ /** Database clock at insert (`clock_timestamp`), not the application clock. */
58
+ db_at: number | null;
55
59
  }
56
60
 
57
61
  /** Minimal SQL + listen surface for the postgres signal driver. */
@@ -87,6 +91,215 @@ const CHANNEL = "oke_signal";
87
91
  /** Claim next once-row: pending/unlocked or lease-expired, blocked by same-key inflight. */
88
92
  const CLAIM_ONCE_SQL = `SELECT * FROM oke_signal_messages WHERE signal=? AND delivery='once' AND available_at<=? AND ((status='pending' AND locked_by IS NULL) OR (status='inflight' AND lease_expires_at IS NOT NULL AND lease_expires_at<=?)) AND (ordering_key IS NULL OR NOT EXISTS (SELECT 1 FROM oke_signal_messages h WHERE h.signal=oke_signal_messages.signal AND h.ordering_key=oke_signal_messages.ordering_key AND h.id<>oke_signal_messages.id AND h.status='inflight' AND h.lease_expires_at IS NOT NULL AND h.lease_expires_at>?)) ORDER BY created_at ASC LIMIT 1 FOR UPDATE SKIP LOCKED`;
89
93
 
94
+ /** `clock_timestamp()` in milliseconds. Stamped in SQL, not from the app clock. */
95
+ const DB_CLOCK_MS_SQL = `(extract(epoch from clock_timestamp())*1000)::bigint`;
96
+
97
+ /**
98
+ * Default overlap window for postgres broadcast and live polls (ms).
99
+ * A commit that lands more than this long after its `db_at` can be missed.
100
+ */
101
+ export const POSTGRES_SIGNAL_DEFAULT_LAG_MS = 30_000;
102
+
103
+ /**
104
+ * Broadcast/live statements the in-memory fake routes to one dedicated branch.
105
+ * Inserts stamp `db_at` from `clock_timestamp()`.
106
+ */
107
+ export const POSTGRES_SIGNAL_FANOUT_SQL = {
108
+ /** Insert including `db_at` from the database clock. */
109
+ insert: `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, db_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ${DB_CLOCK_MS_SQL})`,
110
+ /** Poll `db_at >=` and read the database clock from the same statement. */
111
+ poll: `SELECT m.id, m.signal, m.payload, m.ordering_key, m.delivery, m.attempts, m.failures, m.created_at, m.available_at, m.status, m.locked_by, m.lease_expires_at, m.delivered_to, m.parent_run_id, m.db_at, ${DB_CLOCK_MS_SQL} AS db_now FROM (SELECT ${DB_CLOCK_MS_SQL} AS db_now) AS clock LEFT JOIN oke_signal_messages m ON m.delivery IN ('broadcast', 'live') AND m.db_at >= ?`,
112
+ /** Backfill `db_at` from `created_at` (not 0). */
113
+ backfill: `UPDATE oke_signal_messages SET db_at = created_at WHERE db_at IS NULL`,
114
+ /** Supports the fan-out poll. */
115
+ index: `CREATE INDEX IF NOT EXISTS oke_signal_messages_delivery_db_at ON oke_signal_messages (signal, delivery, db_at)`,
116
+ } as const;
117
+
118
+ /** Drop broadcast rows only after the overlap window has closed. */
119
+ const BROADCAST_TTL_DELETE_SQL = `DELETE FROM oke_signal_messages WHERE delivery = 'broadcast' AND db_at < ?`;
120
+
121
+ function isOnceClaim(text: string): boolean {
122
+ return (
123
+ /FOR\s+UPDATE\s+SKIP\s+LOCKED/i.test(text) &&
124
+ /oke_signal_messages/i.test(text) &&
125
+ /delivery\s*=\s*'once'/i.test(text)
126
+ );
127
+ }
128
+
129
+ function isFanoutPoll(text: string): boolean {
130
+ return /^SELECT\b/i.test(text) && /db_at\s*>=/i.test(text) && /clock_timestamp\s*\(/i.test(text);
131
+ }
132
+
133
+ function isSelectStatus(text: string): boolean {
134
+ return /^SELECT\s+\*\s+FROM\s+oke_signal_messages\s+WHERE\s+signal\s*=\s*\?\s+AND\s+status\s*=\s*\?\s*$/i.test(
135
+ text,
136
+ );
137
+ }
138
+
139
+ function isSelectPending(text: string): boolean {
140
+ return /^SELECT\s+\*\s+FROM\s+oke_signal_messages\s+WHERE\s+status\s+IN\s*\('pending',\s*'inflight'\)\s*$/i.test(
141
+ text,
142
+ );
143
+ }
144
+
145
+ function isSelectSignal(text: string): boolean {
146
+ return /^SELECT\s+\*\s+FROM\s+oke_signal_messages\s+WHERE\s+signal\s*=\s*\?\s*$/i.test(text);
147
+ }
148
+
149
+ function isSelectWrite(text: string): boolean {
150
+ return /^SELECT\s+value\s+FROM\s+oke_signal_writes\s+WHERE\s+key\s*=\s*\?\s*$/i.test(text);
151
+ }
152
+
153
+ function isSelectId(text: string): boolean {
154
+ return /^SELECT\s+\*\s+FROM\s+oke_signal_messages\s+WHERE\s+id\s*=\s*\?\s*$/i.test(text);
155
+ }
156
+
157
+ function isSelectLive(text: string): boolean {
158
+ return /^SELECT\s+\*\s+FROM\s+oke_signal_messages\s+WHERE\s+signal\s*=\s*\?\s+AND\s+delivery\s*=\s*'live'\s+ORDER\s+BY\s+created_at\s+ASC\s*$/i.test(
159
+ text,
160
+ );
161
+ }
162
+
163
+ function isCreateTable(text: string): boolean {
164
+ return /^CREATE\s+TABLE/i.test(text);
165
+ }
166
+
167
+ function isDeliveryDbAtIndex(text: string): boolean {
168
+ return /^CREATE\s+INDEX\b/i.test(text) && /oke_signal_messages_delivery_db_at/i.test(text);
169
+ }
170
+
171
+ function isCreateIndex(text: string): boolean {
172
+ return /^CREATE\s+INDEX/i.test(text) && !isDeliveryDbAtIndex(text);
173
+ }
174
+
175
+ function isBroadcastTtlDelete(text: string): boolean {
176
+ return /^DELETE\s+FROM\s+oke_signal_messages\s+WHERE\s+delivery\s*=\s*'broadcast'\s+AND\s+db_at\s*<\s*\?\s*$/i.test(
177
+ text,
178
+ );
179
+ }
180
+
181
+ function isDeleteLive(text: string): boolean {
182
+ return /^DELETE\s+FROM\s+oke_signal_messages\s+WHERE\s+id\s*=\s*\?\s+AND\s+delivery\s*=\s*'live'\s*$/i.test(
183
+ text,
184
+ );
185
+ }
186
+
187
+ function isDeleteDead(text: string): boolean {
188
+ return /^DELETE\s+FROM\s+oke_signal_messages\s+WHERE\s+id\s*=\s*\?\s+AND\s+signal\s*=\s*\?\s+AND\s+status\s*=\s*'dead'\s*$/i.test(
189
+ text,
190
+ );
191
+ }
192
+
193
+ function isInsertDbAt(text: string): boolean {
194
+ return /^INSERT\s+INTO\s+oke_signal_messages\b/i.test(text) && /clock_timestamp\s*\(/i.test(text);
195
+ }
196
+
197
+ function isInsertMessage(text: string): boolean {
198
+ return (
199
+ /^INSERT\s+INTO\s+oke_signal_messages\s*\(([^)]+)\)\s*VALUES\s*\(([^)]+)\)\s*$/i.test(text) &&
200
+ !isInsertDbAt(text)
201
+ );
202
+ }
203
+
204
+ function isInsertWrite(text: string): boolean {
205
+ return /^INSERT\s+INTO\s+oke_signal_writes\s*\(key,\s*value\)\s*VALUES\s*\(\?,\s*\?\)\s*$/i.test(
206
+ text,
207
+ );
208
+ }
209
+
210
+ function isBackfillDbAt(text: string): boolean {
211
+ return /^UPDATE\s+oke_signal_messages\s+SET\s+db_at\s*=\s*created_at\s+WHERE\s+db_at\s+IS\s+NULL\s*$/i.test(
212
+ text,
213
+ );
214
+ }
215
+
216
+ function isUpdateById(text: string): boolean {
217
+ return (
218
+ /^UPDATE\s+oke_signal_messages\s+SET\s+(.+)\s+WHERE\s+id\s*=\s*\?\s*$/i.test(text) &&
219
+ !isBackfillDbAt(text)
220
+ );
221
+ }
222
+
223
+ function isAlterAddColumn(text: string): boolean {
224
+ return /^ALTER\s+TABLE\s+oke_signal_messages\s+ADD\s+COLUMN/i.test(text);
225
+ }
226
+
227
+ const POSTGRES_SIGNAL_FAKE_BRANCHES: ReadonlyArray<{
228
+ readonly name: string;
229
+ readonly test: (text: string) => boolean;
230
+ }> = [
231
+ { name: "once-claim", test: isOnceClaim },
232
+ { name: "fanout-poll", test: isFanoutPoll },
233
+ { name: "select-status", test: isSelectStatus },
234
+ { name: "select-pending", test: isSelectPending },
235
+ { name: "select-signal", test: isSelectSignal },
236
+ { name: "select-write", test: isSelectWrite },
237
+ { name: "select-id", test: isSelectId },
238
+ { name: "select-live", test: isSelectLive },
239
+ { name: "create-table", test: isCreateTable },
240
+ { name: "index-delivery-db-at", test: isDeliveryDbAtIndex },
241
+ { name: "create-index", test: isCreateIndex },
242
+ { name: "delete-broadcast-ttl", test: isBroadcastTtlDelete },
243
+ { name: "delete-live", test: isDeleteLive },
244
+ { name: "delete-dead", test: isDeleteDead },
245
+ { name: "insert-db-at", test: isInsertDbAt },
246
+ { name: "insert-message", test: isInsertMessage },
247
+ { name: "insert-write", test: isInsertWrite },
248
+ { name: "backfill-db-at", test: isBackfillDbAt },
249
+ { name: "update-by-id", test: isUpdateById },
250
+ { name: "alter-add-column", test: isAlterAddColumn },
251
+ ];
252
+
253
+ /**
254
+ * In-memory fake branches whose predicates match `sql`.
255
+ *
256
+ * Broadcast/live statements must match exactly one branch so a broader
257
+ * regex cannot swallow them.
258
+ *
259
+ * @param sql - Statement text
260
+ */
261
+ export function postgresSignalFakeBranches(sql: string): readonly string[] {
262
+ const text = sql.trim();
263
+ const names: string[] = [];
264
+ for (const branch of POSTGRES_SIGNAL_FAKE_BRANCHES) {
265
+ if (branch.test(text)) names.push(branch.name);
266
+ }
267
+ return names;
268
+ }
269
+
270
+ function fieldText(value: unknown, fallback: string): string {
271
+ if (typeof value === "string") return value;
272
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
273
+ return fallback;
274
+ }
275
+
276
+ function msgRowFromFields(row: Record<string, unknown>): MsgRow {
277
+ const orderingKey = row.ordering_key;
278
+ const parentRunId = row.parent_run_id;
279
+ return {
280
+ id: fieldText(row.id, ""),
281
+ signal: fieldText(row.signal, ""),
282
+ payload: fieldText(row.payload, "null"),
283
+ ordering_key:
284
+ orderingKey === undefined || orderingKey === null ? null : fieldText(orderingKey, ""),
285
+ delivery: row.delivery as SignalDelivery,
286
+ attempts: Number(row.attempts ?? 0),
287
+ failures: fieldText(row.failures, "[]"),
288
+ created_at: Number(row.created_at),
289
+ available_at: Number(row.available_at),
290
+ status: (row.status as MsgRow["status"]) ?? "pending",
291
+ locked_by: (row.locked_by as string | null) ?? null,
292
+ lease_expires_at:
293
+ row.lease_expires_at === undefined || row.lease_expires_at === null
294
+ ? null
295
+ : Number(row.lease_expires_at),
296
+ delivered_to: fieldText(row.delivered_to, "[]"),
297
+ parent_run_id:
298
+ parentRunId === undefined || parentRunId === null ? null : fieldText(parentRunId, ""),
299
+ db_at: row.db_at === undefined || row.db_at === null ? null : Number(row.db_at),
300
+ };
301
+ }
302
+
90
303
  /**
91
304
  * In-memory Postgres-protocol fake with transactions, SKIP LOCKED, LISTEN/NOTIFY.
92
305
  */
@@ -97,6 +310,7 @@ export function createPostgresSignalFake(options?: {
97
310
  /** Force-kill mid-transaction (drops uncommitted state). */
98
311
  killActiveTransaction(): void;
99
312
  } {
313
+ const clock = options?.now ?? (() => Date.now());
100
314
  const listeners = new Map<string, Set<(payload: string) => void>>();
101
315
 
102
316
  type State = {
@@ -123,6 +337,7 @@ export function createPostgresSignalFake(options?: {
123
337
  ordering_key: m.ordering_key ?? null,
124
338
  lease_expires_at: m.lease_expires_at ?? null,
125
339
  parent_run_id: m.parent_run_id ?? null,
340
+ db_at: m.db_at ?? null,
126
341
  })),
127
342
  writes: new Map(snap.writes),
128
343
  };
@@ -175,11 +390,7 @@ export function createPostgresSignalFake(options?: {
175
390
 
176
391
  // Claim: SELECT … FOR UPDATE SKIP LOCKED (pending or lease-expired inflight),
177
392
  // with per-key serialization when ordering_key is set.
178
- const isOnceClaim =
179
- /FOR\s+UPDATE\s+SKIP\s+LOCKED/i.test(text) &&
180
- /oke_signal_messages/i.test(text) &&
181
- /delivery\s*=\s*'once'/i.test(text);
182
- if (isOnceClaim) {
393
+ if (isOnceClaim(text)) {
183
394
  const signal = String(params[0]);
184
395
  const t = Number(params[1]);
185
396
  const leaseCutoff = Number(params[2]);
@@ -213,52 +424,47 @@ export function createPostgresSignalFake(options?: {
213
424
  return [{ ...row }];
214
425
  }
215
426
 
216
- const selMsg =
217
- /^SELECT\s+\*\s+FROM\s+oke_signal_messages\s+WHERE\s+signal\s*=\s*\?\s+AND\s+status\s*=\s*\?\s*$/i.exec(
218
- text,
427
+ // Before broader SELECTs: poll reads db_at and the database clock together.
428
+ if (isFanoutPoll(text)) {
429
+ const lower = Number(params[0]);
430
+ const dbNow = clock();
431
+ const matched = state.messages.filter(
432
+ (m) =>
433
+ (m.delivery === "broadcast" || m.delivery === "live") &&
434
+ m.db_at != null &&
435
+ m.db_at >= lower,
219
436
  );
220
- if (selMsg) {
437
+ if (matched.length === 0) return [{ db_now: dbNow }];
438
+ return matched.map((m) => ({ ...m, db_now: dbNow }));
439
+ }
440
+
441
+ if (isSelectStatus(text)) {
221
442
  return state.messages
222
443
  .filter((m) => m.signal === params[0] && m.status === params[1])
223
444
  .map((m) => ({ ...m }));
224
445
  }
225
446
 
226
- const selPending =
227
- /^SELECT\s+\*\s+FROM\s+oke_signal_messages\s+WHERE\s+status\s+IN\s*\('pending',\s*'inflight'\)\s*$/i.exec(
228
- text,
229
- );
230
- if (selPending) {
447
+ if (isSelectPending(text)) {
231
448
  return state.messages
232
449
  .filter((m) => m.status === "pending" || m.status === "inflight")
233
450
  .map((m) => ({ ...m }));
234
451
  }
235
452
 
236
- const selAll =
237
- /^SELECT\s+\*\s+FROM\s+oke_signal_messages\s+WHERE\s+signal\s*=\s*\?\s*$/i.exec(text);
238
- if (selAll) {
453
+ if (isSelectSignal(text)) {
239
454
  return state.messages.filter((m) => m.signal === params[0]).map((m) => ({ ...m }));
240
455
  }
241
456
 
242
- const selWrite =
243
- /^SELECT\s+value\s+FROM\s+oke_signal_writes\s+WHERE\s+key\s*=\s*\?\s*$/i.exec(text);
244
- if (selWrite) {
457
+ if (isSelectWrite(text)) {
245
458
  const v = state.writes.get(String(params[0]));
246
459
  if (v === undefined) return [];
247
460
  return [{ value: JSON.stringify(v) }];
248
461
  }
249
462
 
250
- const byId = /^SELECT\s+\*\s+FROM\s+oke_signal_messages\s+WHERE\s+id\s*=\s*\?\s*$/i.exec(
251
- text,
252
- );
253
- if (byId) {
463
+ if (isSelectId(text)) {
254
464
  return state.messages.filter((m) => m.id === params[0]).map((m) => ({ ...m }));
255
465
  }
256
466
 
257
- const selLive =
258
- /^SELECT\s+\*\s+FROM\s+oke_signal_messages\s+WHERE\s+signal\s*=\s*\?\s+AND\s+delivery\s*=\s*'live'\s+ORDER\s+BY\s+created_at\s+ASC\s*$/i.exec(
259
- text,
260
- );
261
- if (selLive) {
467
+ if (isSelectLive(text)) {
262
468
  return state.messages
263
469
  .filter((m) => m.signal === params[0] && m.delivery === "live")
264
470
  .map((m) => ({ ...m }));
@@ -271,14 +477,25 @@ export function createPostgresSignalFake(options?: {
271
477
  const text = sql.trim();
272
478
  const state = view();
273
479
 
274
- if (/^CREATE\s+TABLE/i.test(text)) return { changes: 0 };
275
- if (/^CREATE\s+INDEX/i.test(text)) return { changes: 0 };
480
+ if (isCreateTable(text)) return { changes: 0 };
481
+ // Dedicated index branch before the broader CREATE INDEX match.
482
+ if (isDeliveryDbAtIndex(text)) return { changes: 0 };
483
+ if (isCreateIndex(text)) return { changes: 0 };
276
484
 
277
- const delLive =
278
- /^DELETE\s+FROM\s+oke_signal_messages\s+WHERE\s+id\s*=\s*\?\s+AND\s+delivery\s*=\s*'live'\s*$/i.exec(
279
- text,
280
- );
281
- if (delLive) {
485
+ if (isBroadcastTtlDelete(text)) {
486
+ const cutoff = Number(params[0]);
487
+ let changes = 0;
488
+ for (let i = state.messages.length - 1; i >= 0; i--) {
489
+ const m = state.messages[i]!;
490
+ if (m.delivery === "broadcast" && m.db_at != null && m.db_at < cutoff) {
491
+ state.messages.splice(i, 1);
492
+ changes += 1;
493
+ }
494
+ }
495
+ return { changes };
496
+ }
497
+
498
+ if (isDeleteLive(text)) {
282
499
  let changes = 0;
283
500
  for (let i = state.messages.length - 1; i >= 0; i--) {
284
501
  const m = state.messages[i]!;
@@ -290,11 +507,7 @@ export function createPostgresSignalFake(options?: {
290
507
  return { changes };
291
508
  }
292
509
 
293
- const delDead =
294
- /^DELETE\s+FROM\s+oke_signal_messages\s+WHERE\s+id\s*=\s*\?\s+AND\s+signal\s*=\s*\?\s+AND\s+status\s*=\s*'dead'\s*$/i.exec(
295
- text,
296
- );
297
- if (delDead) {
510
+ if (isDeleteDead(text)) {
298
511
  let changes = 0;
299
512
  for (let i = state.messages.length - 1; i >= 0; i--) {
300
513
  const m = state.messages[i]!;
@@ -306,54 +519,60 @@ export function createPostgresSignalFake(options?: {
306
519
  return { changes };
307
520
  }
308
521
 
309
- const insertMsg =
310
- /^INSERT\s+INTO\s+oke_signal_messages\s*\(([^)]+)\)\s*VALUES\s*\(([^)]+)\)\s*$/i.exec(text);
522
+ // Before the generic INSERT: `db_at` is `clock_timestamp()`, not a placeholder.
523
+ if (isInsertDbAt(text)) {
524
+ const head =
525
+ /^INSERT\s+INTO\s+oke_signal_messages\s*\(([^)]+)\)\s*VALUES\s*\((.*)\)\s*$/i.exec(text);
526
+ if (!head) throw new Error(`postgres signal fake: unsupported exec: ${sql}`);
527
+ const cols = head[1]!.split(",").map((c) => c.trim());
528
+ const fields: Record<string, unknown> = {};
529
+ let pi = 0;
530
+ for (const col of cols) {
531
+ if (col === "db_at") {
532
+ fields.db_at = clock();
533
+ continue;
534
+ }
535
+ fields[col] = params[pi++];
536
+ }
537
+ state.messages.push(msgRowFromFields(fields));
538
+ return { changes: 1 };
539
+ }
540
+
541
+ const insertMsg = isInsertMessage(text)
542
+ ? /^INSERT\s+INTO\s+oke_signal_messages\s*\(([^)]+)\)\s*VALUES\s*\(([^)]+)\)\s*$/i.exec(
543
+ text,
544
+ )
545
+ : null;
311
546
  if (insertMsg) {
312
547
  const cols = insertMsg[1]!.split(",").map((c) => c.trim());
313
548
  const row: Record<string, unknown> = {};
314
549
  cols.forEach((c, i) => {
315
550
  row[c] = params[i];
316
551
  });
317
- state.messages.push({
318
- id: String(row.id),
319
- signal: String(row.signal),
320
- payload: String(row.payload),
321
- ordering_key:
322
- row.ordering_key === undefined || row.ordering_key === null
323
- ? null
324
- : String(row.ordering_key),
325
- delivery: row.delivery as SignalDelivery,
326
- attempts: Number(row.attempts ?? 0),
327
- failures: String(row.failures ?? "[]"),
328
- created_at: Number(row.created_at),
329
- available_at: Number(row.available_at),
330
- status: (row.status as MsgRow["status"]) ?? "pending",
331
- locked_by: (row.locked_by as string | null) ?? null,
332
- lease_expires_at:
333
- row.lease_expires_at === undefined || row.lease_expires_at === null
334
- ? null
335
- : Number(row.lease_expires_at),
336
- delivered_to: String(row.delivered_to ?? "[]"),
337
- parent_run_id:
338
- row.parent_run_id === undefined || row.parent_run_id === null
339
- ? null
340
- : String(row.parent_run_id),
341
- });
552
+ state.messages.push(msgRowFromFields(row));
342
553
  return { changes: 1 };
343
554
  }
344
555
 
345
- const insertWrite =
346
- /^INSERT\s+INTO\s+oke_signal_writes\s*\(key,\s*value\)\s*VALUES\s*\(\?,\s*\?\)\s*$/i.exec(
347
- text,
348
- );
349
- if (insertWrite) {
556
+ if (isInsertWrite(text)) {
350
557
  state.writes.set(String(params[0]), JSON.parse(String(params[1])));
351
558
  return { changes: 1 };
352
559
  }
353
560
 
354
- const upd = /^UPDATE\s+oke_signal_messages\s+SET\s+(.+)\s+WHERE\s+id\s*=\s*\?\s*$/i.exec(
355
- text,
356
- );
561
+ // Before the generic UPDATE: backfill `db_at` from `created_at`.
562
+ if (isBackfillDbAt(text)) {
563
+ let changes = 0;
564
+ for (const m of state.messages) {
565
+ if (m.db_at == null) {
566
+ m.db_at = m.created_at;
567
+ changes += 1;
568
+ }
569
+ }
570
+ return { changes };
571
+ }
572
+
573
+ const upd = isUpdateById(text)
574
+ ? /^UPDATE\s+oke_signal_messages\s+SET\s+(.+)\s+WHERE\s+id\s*=\s*\?\s*$/i.exec(text)
575
+ : null;
357
576
  if (upd) {
358
577
  const id = String(params[params.length - 1]);
359
578
  const row = state.messages.find((m) => m.id === id);
@@ -377,7 +596,7 @@ export function createPostgresSignalFake(options?: {
377
596
  return { changes: 1 };
378
597
  }
379
598
 
380
- if (/^ALTER\s+TABLE\s+oke_signal_messages\s+ADD\s+COLUMN/i.test(text)) {
599
+ if (isAlterAddColumn(text)) {
381
600
  return { changes: 0 };
382
601
  }
383
602
 
@@ -444,7 +663,8 @@ async function ensureSchema(sql: PostgresSignalSql): Promise<void> {
444
663
  locked_by TEXT,
445
664
  lease_expires_at BIGINT,
446
665
  delivered_to TEXT,
447
- parent_run_id TEXT
666
+ parent_run_id TEXT,
667
+ db_at BIGINT
448
668
  )`);
449
669
  // Existing tables created before leases / keys: add columns in place.
450
670
  try {
@@ -475,6 +695,21 @@ async function ensureSchema(sql: PostgresSignalSql): Promise<void> {
475
695
  } catch {
476
696
  /* fake / older engines without IF NOT EXISTS — ignore */
477
697
  }
698
+ try {
699
+ await sql.exec(`ALTER TABLE oke_signal_messages ADD COLUMN IF NOT EXISTS db_at BIGINT`);
700
+ } catch {
701
+ /* fake / older engines without IF NOT EXISTS — ignore */
702
+ }
703
+ try {
704
+ await sql.exec(POSTGRES_SIGNAL_FANOUT_SQL.backfill);
705
+ } catch {
706
+ /* fake / older engines — ignore */
707
+ }
708
+ try {
709
+ await sql.exec(POSTGRES_SIGNAL_FANOUT_SQL.index);
710
+ } catch {
711
+ /* fake / older engines without IF NOT EXISTS — ignore */
712
+ }
478
713
  }
479
714
 
480
715
  function rowToMessage(row: Record<string, unknown>): SignalMessage {
@@ -501,15 +736,31 @@ function rowToMessage(row: Record<string, unknown>): SignalMessage {
501
736
  };
502
737
  }
503
738
 
739
+ /**
740
+ * Options for {@link openPostgresSignal}.
741
+ */
742
+ export interface PostgresSignalOpenOptions extends SignalOpenOptions {
743
+ /**
744
+ * Overlap window (ms) for broadcast and live polls.
745
+ * Rows with `db_at >= cursor - lagMs` are re-read and deduped by id.
746
+ * Defaults to {@link POSTGRES_SIGNAL_DEFAULT_LAG_MS}.
747
+ */
748
+ readonly lagMs?: number;
749
+ }
750
+
504
751
  /**
505
752
  * Open a postgres-backed signal bus.
506
753
  *
507
- * @param options - Declarations / sql fake / clock
754
+ * @param options - Declarations / sql fake / clock / lag window
508
755
  */
509
- export async function openPostgresSignal(options: SignalOpenOptions): Promise<SignalBus> {
756
+ export async function openPostgresSignal(options: PostgresSignalOpenOptions): Promise<SignalBus> {
510
757
  const now = options.now ?? (() => Date.now());
511
758
  const signals = options.signals;
512
759
  const leaseMs = options.leaseMs ?? SIGNAL_DEFAULT_LEASE_MS;
760
+ const lagMs = options.lagMs ?? POSTGRES_SIGNAL_DEFAULT_LAG_MS;
761
+ // Broadcast retention stays strictly longer than the overlap window.
762
+ // Live retention stays on pruneLive (default unbounded).
763
+ const broadcastTtlMs = lagMs + 1_000;
513
764
  const sql =
514
765
  (options.sql as PostgresSignalSql | undefined) ??
515
766
  createPostgresSignalFake({
@@ -522,6 +773,37 @@ export async function openPostgresSignal(options: SignalOpenOptions): Promise<Si
522
773
  }
523
774
  await ensureSchema(sql);
524
775
 
776
+ // Best-effort within lagMs. A transaction that commits more than lagMs
777
+ // after its db_at can be missed. Not gap-free ordering. No persisted cursor table.
778
+ let fanoutCursor = 0;
779
+ const fanoutSeen = new Map<string, number>();
780
+
781
+ async function readFanout(lower: number): Promise<{
782
+ rows: Record<string, unknown>[];
783
+ dbNow: number;
784
+ }> {
785
+ const raw = await sql.query(POSTGRES_SIGNAL_FANOUT_SQL.poll, [lower]);
786
+ let dbNow = Number.NaN;
787
+ const rows: Record<string, unknown>[] = [];
788
+ for (const row of raw) {
789
+ if (Number.isNaN(dbNow)) dbNow = Number(row.db_now);
790
+ if (row.id == null) continue;
791
+ rows.push(row);
792
+ }
793
+ if (!Number.isFinite(dbNow)) {
794
+ throw new Error("postgres signal: fanout poll did not return db_now");
795
+ }
796
+ return { rows, dbNow };
797
+ }
798
+
799
+ // Start at the current database time so messages committed while this
800
+ // instance was offline are already behind the cursor.
801
+ const bootSnap = await readFanout(Number.MIN_SAFE_INTEGER);
802
+ fanoutCursor = bootSnap.dbNow;
803
+ for (const row of bootSnap.rows) {
804
+ fanoutSeen.set(String(row.id), bootSnap.dbNow);
805
+ }
806
+
525
807
  const consumers: Array<{
526
808
  signal: string;
527
809
  subscriberId: string;
@@ -619,25 +901,22 @@ export async function openPostgresSignal(options: SignalOpenOptions): Promise<Si
619
901
  typeof options?.parentRunId === "string" && options.parentRunId.length > 0
620
902
  ? options.parentRunId
621
903
  : null;
622
- await tx.exec(
623
- `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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
624
- [
625
- okid(),
626
- signal,
627
- JSON.stringify(payload ?? null),
628
- orderingKey,
629
- decl.delivery,
630
- 0,
631
- "[]",
632
- t,
633
- t,
634
- "pending",
635
- null,
636
- null,
637
- "[]",
638
- parentRunId,
639
- ],
640
- );
904
+ await tx.exec(POSTGRES_SIGNAL_FANOUT_SQL.insert, [
905
+ okid(),
906
+ signal,
907
+ JSON.stringify(payload ?? null),
908
+ orderingKey,
909
+ decl.delivery,
910
+ 0,
911
+ "[]",
912
+ t,
913
+ t,
914
+ "pending",
915
+ null,
916
+ null,
917
+ "[]",
918
+ parentRunId,
919
+ ]);
641
920
  }
642
921
 
643
922
  async function begin(): Promise<SignalTransaction> {
@@ -867,87 +1146,64 @@ export async function openPostgresSignal(options: SignalOpenOptions): Promise<Si
867
1146
  return progress;
868
1147
  }
869
1148
 
870
- async function drainBroadcastPass(): Promise<boolean> {
1149
+ async function drainFanoutPass(): Promise<boolean> {
871
1150
  let progress = false;
872
- for (const consumer of consumers) {
873
- const decl = signals.get(consumer.signal);
874
- if (decl?.delivery !== "broadcast") continue;
875
- const rows = await sql.query(`SELECT * FROM oke_signal_messages WHERE signal = ?`, [
876
- consumer.signal,
877
- ]);
878
- for (const row of rows) {
879
- if (row.status === "dead" || row.status === "delivered") continue;
880
- if (row.delivery !== "broadcast") continue;
881
- // Re-read delivered_to so concurrent drain (NOTIFY) cannot double-deliver.
882
- const fresh = await sql.query(`SELECT * FROM oke_signal_messages WHERE signal = ?`, [
883
- consumer.signal,
884
- ]);
885
- const current = fresh.find((r) => r.id === row.id);
886
- if (!current || current.status === "delivered") continue;
887
- const deliveredTo = new Set(JSON.parse(String(current.delivered_to ?? "[]")) as string[]);
888
- if (deliveredTo.has(consumer.subscriberId)) continue;
889
- // Claim this subscriber slot before invoking the handler.
890
- deliveredTo.add(consumer.subscriberId);
891
- const subs = consumers.filter((c) => c.signal === consumer.signal);
892
- const status = subs.every((s) => deliveredTo.has(s.subscriberId)) ? "delivered" : "pending";
893
- await sql.exec(
894
- `UPDATE oke_signal_messages SET delivered_to = ?, status = ?, attempts = ? WHERE id = ?`,
895
- [JSON.stringify([...deliveredTo]), status, Number(current.attempts) + 1, row.id],
896
- );
1151
+ const snap = await readFanout(fanoutCursor - lagMs);
1152
+ const liveSignals = new Set<string>();
1153
+ for (const row of snap.rows) {
1154
+ const id = String(row.id);
1155
+ if (fanoutSeen.has(id)) continue;
1156
+ const delivery = String(row.delivery);
1157
+ if (delivery === "broadcast") {
1158
+ const name = String(row.signal);
1159
+ const subs = consumers.filter((c) => c.signal === name);
1160
+ if (subs.length === 0) continue;
1161
+ fanoutSeen.set(id, snap.dbNow);
897
1162
  progress = true;
898
- const msg = rowToMessage(current);
899
- try {
900
- await consumer.handler(msg);
901
- noteDelivered();
902
- } catch (err) {
903
- const failures = [...msg.failures, failureFromError(err, Number(current.attempts) + 1)];
904
- const key = `${consumer.signal}::${consumer.subscriberId}`;
905
- subscriberErrors.set(key, (subscriberErrors.get(key) ?? 0) + 1);
906
- await sql.exec(`UPDATE oke_signal_messages SET failures = ? WHERE id = ?`, [
907
- JSON.stringify(failures),
908
- row.id,
909
- ]);
1163
+ const msg = rowToMessage(row);
1164
+ for (const consumer of subs) {
1165
+ try {
1166
+ await consumer.handler(msg);
1167
+ noteDelivered();
1168
+ } catch (err) {
1169
+ const failures = [...msg.failures, failureFromError(err, msg.attempts + 1)];
1170
+ const key = `${name}::${consumer.subscriberId}`;
1171
+ subscriberErrors.set(key, (subscriberErrors.get(key) ?? 0) + 1);
1172
+ await sql.exec(`UPDATE oke_signal_messages SET failures = ? WHERE id = ?`, [
1173
+ JSON.stringify(failures),
1174
+ id,
1175
+ ]);
1176
+ }
1177
+ }
1178
+ } else if (delivery === "live") {
1179
+ fanoutSeen.set(id, snap.dbNow);
1180
+ progress = true;
1181
+ const name = String(row.signal);
1182
+ liveSignals.add(name);
1183
+ const handlers = liveHandlers.get(name);
1184
+ const payload = JSON.parse(String(row.payload));
1185
+ if (handlers) {
1186
+ const event: LiveEvent = { id, payload };
1187
+ for (const h of handlers) await h(event);
910
1188
  }
1189
+ let list = recentLive.get(name);
1190
+ if (!list) {
1191
+ list = [];
1192
+ recentLive.set(name, list);
1193
+ }
1194
+ list.push(payload);
1195
+ while (list.length > 50) list.shift();
1196
+ noteDelivered();
911
1197
  }
912
1198
  }
913
- return progress;
914
- }
915
-
916
- async function drainLivePass(): Promise<boolean> {
917
- let progress = false;
918
- const pending = await sql.query(
919
- `SELECT * FROM oke_signal_messages WHERE status IN ('pending', 'inflight')`,
920
- );
921
- for (const row of pending) {
922
- if (row.delivery !== "live") continue;
923
- progress = true;
924
- const handlers = liveHandlers.get(String(row.signal));
925
- const payload = JSON.parse(String(row.payload));
926
- if (handlers) {
927
- const event: LiveEvent = { id: String(row.id), payload };
928
- for (const h of handlers) await h(event);
929
- }
930
- const sig = String(row.signal);
931
- let list = recentLive.get(sig);
932
- if (!list) {
933
- list = [];
934
- recentLive.set(sig, list);
935
- }
936
- list.push(payload);
937
- while (list.length > 50) list.shift();
938
- noteDelivered();
939
- await sql.exec(
940
- `UPDATE oke_signal_messages SET status = 'delivered', locked_by = NULL, lease_expires_at = NULL WHERE id = ?`,
941
- [row.id],
942
- );
1199
+ const pruneBefore = snap.dbNow - lagMs;
1200
+ for (const [id, seenAt] of fanoutSeen) {
1201
+ if (seenAt < pruneBefore) fanoutSeen.delete(id);
943
1202
  }
944
- const pruned = new Set<string>();
945
- for (const row of pending) {
946
- if (row.delivery !== "live") continue;
947
- const sig = String(row.signal);
948
- if (pruned.has(sig)) continue;
949
- pruned.add(sig);
950
- await pruneLive(sig);
1203
+ fanoutCursor = snap.dbNow;
1204
+ await sql.exec(BROADCAST_TTL_DELETE_SQL, [snap.dbNow - broadcastTtlMs]);
1205
+ for (const name of liveSignals) {
1206
+ await pruneLive(name);
951
1207
  }
952
1208
  return progress;
953
1209
  }
@@ -960,9 +1216,8 @@ export async function openPostgresSignal(options: SignalOpenOptions): Promise<Si
960
1216
  draining = (async () => {
961
1217
  for (let i = 0; i < 1000; i++) {
962
1218
  const a = await drainOncePass();
963
- const b = await drainBroadcastPass();
964
- const c = await drainLivePass();
965
- if (!a && !b && !c) break;
1219
+ const b = await drainFanoutPass();
1220
+ if (!a && !b) break;
966
1221
  }
967
1222
  })();
968
1223
  try {
@@ -1012,22 +1267,20 @@ export async function openPostgresSignal(options: SignalOpenOptions): Promise<Si
1012
1267
  }
1013
1268
  }
1014
1269
  const subs = consumers.filter((c) => c.signal === name);
1015
- const subscribers = await Promise.all(
1016
- subs.map(async (c) => {
1017
- let lag = 0;
1018
- for (const row of rows) {
1019
- if (row.delivery !== "broadcast") continue;
1020
- if (row.status === "dead" || row.status === "delivered") continue;
1021
- const deliveredTo = new Set(JSON.parse(String(row.delivered_to ?? "[]")) as string[]);
1022
- if (!deliveredTo.has(c.subscriberId)) lag += 1;
1023
- }
1024
- return {
1025
- id: c.subscriberId,
1026
- lag,
1027
- errorCount: subscriberErrors.get(`${name}::${c.subscriberId}`) ?? 0,
1028
- };
1029
- }),
1030
- );
1270
+ const subscribers = subs.map((c) => {
1271
+ let lag = 0;
1272
+ for (const row of rows) {
1273
+ if (row.delivery !== "broadcast") continue;
1274
+ if (row.status === "dead") continue;
1275
+ if (fanoutSeen.has(String(row.id))) continue;
1276
+ lag += 1;
1277
+ }
1278
+ return {
1279
+ id: c.subscriberId,
1280
+ lag,
1281
+ errorCount: subscriberErrors.get(`${name}::${c.subscriberId}`) ?? 0,
1282
+ };
1283
+ });
1031
1284
  return {
1032
1285
  signal: name,
1033
1286
  delivery: decl.delivery,