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.
- package/package.json +1 -1
- package/site/content/docs/ai/mcp.mdx +3 -3
- package/site/content/docs/elements/signal/broadcast.mdx +4 -2
- package/site/content/docs/reference/configuration.mdx +16 -0
- package/site/content/docs/reference/errors.mdx +31 -19
- package/src/compiler/aot.test.ts +3 -2
- package/src/compiler/aot.ts +45 -1
- package/src/compiler/dynamic.ts +1 -1
- package/src/compiler/http-parse.test.ts +101 -0
- package/src/compiler/http-parse.ts +188 -9
- package/src/compiler/interpret.ts +25 -2
- package/src/console/ui-next/dist/assets/{access-page-C_N4qvai.js → access-page-zBMhZnFm.js} +1 -1
- package/src/console/ui-next/dist/assets/{agent-disclosure-Cq1TxniW.js → agent-disclosure-CBj9f7Ju.js} +1 -1
- package/src/console/ui-next/dist/assets/{cache-glyph-BaI4uxZz.js → cache-glyph-D3MY49h3.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button-DNyK7IIY.js → call-pii-button-L9JxdBhp.js} +1 -1
- package/src/console/ui-next/dist/assets/{collapsible-CRhBJMZR.js → collapsible-BvFaX-Zt.js} +1 -1
- package/src/console/ui-next/dist/assets/{decisions-page-D9twj8Xy.js → decisions-page-DgB76m-X.js} +1 -1
- package/src/console/ui-next/dist/assets/{duration-tone-CsHWlVPU.js → duration-tone-l1DSJ2Vz.js} +1 -1
- package/src/console/ui-next/dist/assets/{flows-page-ZaTJAhR9.js → flows-page-Bb9t1lu6.js} +1 -1
- package/src/console/ui-next/dist/assets/{highlighted-json-DQ-EMAfz.js → highlighted-json-DMvsi3Ti.js} +1 -1
- package/src/console/ui-next/dist/assets/{http-method-Cn_Fl79s.js → http-method-BwhJBTcQ.js} +1 -1
- package/src/console/ui-next/dist/assets/{index-CmRFeWdT.js → index-RhV2jT_7.js} +3 -3
- package/src/console/ui-next/dist/assets/{observability-page-Cn6R_dXz.js → observability-page-BRfUILBq.js} +1 -1
- package/src/console/ui-next/dist/assets/{replica-lag-Bx6EZhSe.js → replica-lag-BsceAzIG.js} +1 -1
- package/src/console/ui-next/dist/assets/{request-meta-DmdiUrfh.js → request-meta-C6lTyCXp.js} +1 -1
- package/src/console/ui-next/dist/assets/{store-page-8P6vGXnn.js → store-page-YmE806bJ.js} +1 -1
- package/src/console/ui-next/dist/assets/{trace-detail-sheet-BfGvpFSv.js → trace-detail-sheet-Di4irS49.js} +1 -1
- package/src/console/ui-next/dist/assets/{tree-expand-toggle-CWdUHbc0.js → tree-expand-toggle-DYeL6Rye.js} +1 -1
- package/src/console/ui-next/dist/assets/{units-page-CZOFZzay.js → units-page-07agasRA.js} +1 -1
- package/src/console/ui-next/dist/assets/{vault-page-ki_ishmX.js → vault-page-CHBck4n_.js} +1 -1
- package/src/console/ui-next/dist/index.html +1 -1
- package/src/console/ui-next/seed-parked-approval.ts +5 -1
- package/src/drivers/journal-postgres.test.ts +84 -1
- package/src/drivers/journal-postgres.ts +282 -32
- package/src/drivers/postgres.test.ts +60 -0
- package/src/drivers/postgres.ts +79 -5
- package/src/drivers/signal-compete.test.ts +6 -2
- package/src/drivers/signal-postgres.test.ts +170 -0
- package/src/drivers/signal-postgres.ts +449 -196
- package/src/drivers/signal-redis.test.ts +224 -0
- package/src/drivers/signal-redis.ts +835 -150
- package/src/elements/ai/approval.test.ts +15 -3
- package/src/elements/ai/approval.ts +15 -9
- package/src/elements/channel/runtime.ts +31 -4
- package/src/elements/channel/sql-ledger.test.ts +404 -0
- package/src/elements/channel/sql-ledger.ts +177 -33
- package/src/elements/clock/chaos-child.ts +12 -2
- package/src/elements/clock/durable.ts +11 -0
- package/src/elements/store/cache-bus.test.ts +121 -0
- package/src/elements/store/cache-bus.ts +278 -0
- package/src/elements/store/cache.test.ts +59 -0
- package/src/elements/store/cache.ts +157 -12
- package/src/elements/store/runtime.ts +44 -2
- package/src/elements/store/sql-errors.test.ts +131 -0
- package/src/elements/store/sql-errors.ts +39 -16
- package/src/elements/store/sql-nested-tx.test.ts +344 -0
- package/src/elements/store/sql-session.ts +183 -3
- package/src/kernel/app.ts +176 -28
- package/src/kernel/auto-registry.test.ts +5 -1
- package/src/kernel/boot-bind/signal.ts +13 -2
- package/src/kernel/boot-bind/store.ts +16 -1
- package/src/kernel/boot.test.ts +2 -2
- package/src/kernel/boot.ts +41 -11
- package/src/kernel/boundary-contract.ts +7 -0
- package/src/kernel/builtin-errors.ts +2 -0
- package/src/kernel/call.test.ts +1 -0
- package/src/kernel/fx-sql-handle.ts +7 -1
- package/src/kernel/horizontal-child.ts +54 -1
- package/src/kernel/horizontal.integration.test.ts +131 -7
- package/src/kernel/idempotency.test.ts +5 -6
- package/src/kernel/journal-boot.test.ts +83 -6
- package/src/kernel/journal.test.ts +44 -0
- package/src/kernel/journal.ts +171 -26
- package/src/mcp/authorization.ts +5 -11
- package/src/mcp/confirmation.ts +203 -38
- package/src/mcp/docs-server.ts +7 -2
- package/src/mcp/index.ts +4 -0
- package/src/mcp/mcp.test.ts +321 -33
- package/src/mcp/server.ts +8 -3
- package/src/mcp/session.ts +7 -0
- package/src/mcp/tools.ts +68 -19
- package/src/runtime/bun.ts +5 -0
- package/src/runtime/types.ts +5 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redis signal driver — stream reply parsing, reclaim, and pub/sub fan-out.
|
|
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
|
+
createSignalRedisFake,
|
|
10
|
+
openRedisSignal,
|
|
11
|
+
parseXreadgroupReply,
|
|
12
|
+
SIGNAL_REDIS_MIN_IDLE_MS,
|
|
13
|
+
} from "./signal-redis.ts";
|
|
14
|
+
|
|
15
|
+
const entryFields = ["payload", '{"a":1}', "signal", "order"] as const;
|
|
16
|
+
|
|
17
|
+
describe("parseXreadgroupReply", () => {
|
|
18
|
+
test("reads Bun object replies and RESP arrays for XREADGROUP and XAUTOCLAIM", () => {
|
|
19
|
+
const expected = [{ id: "1-0", fields: { payload: '{"a":1}', signal: "order" } }];
|
|
20
|
+
const entries = [["1-0", [...entryFields]]];
|
|
21
|
+
|
|
22
|
+
expect(parseXreadgroupReply({ "oke:signal:order": entries })).toEqual(expected);
|
|
23
|
+
expect(parseXreadgroupReply([["oke:signal:order", entries]])).toEqual(expected);
|
|
24
|
+
expect(parseXreadgroupReply(["0-0", entries, []])).toEqual(expected);
|
|
25
|
+
expect(parseXreadgroupReply(null)).toEqual([]);
|
|
26
|
+
expect(parseXreadgroupReply({})).toEqual([]);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
function onceDecl(name: string, retries: number): Map<string, SignalDecl> {
|
|
31
|
+
const decl = signal.once(name, { retries, deadLetter: true });
|
|
32
|
+
return new Map([[decl.name, decl]]);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe("redis once reclaim", () => {
|
|
36
|
+
test("a pending entry past min-idle is delivered once", async () => {
|
|
37
|
+
let nowMs = 0;
|
|
38
|
+
const now = () => nowMs;
|
|
39
|
+
const redis = createSignalRedisFake({ now });
|
|
40
|
+
const name = "stuck-pending";
|
|
41
|
+
const shared = { signals: onceDecl(name, 3), redis, compete: true as const, now };
|
|
42
|
+
const a = await openRedisSignal({ ...shared, consumerId: "a" });
|
|
43
|
+
const b = await openRedisSignal({ ...shared, consumerId: "b" });
|
|
44
|
+
let aCalls = 0;
|
|
45
|
+
const got: string[] = [];
|
|
46
|
+
await a.subscribe(name, "a", async () => {
|
|
47
|
+
aCalls += 1;
|
|
48
|
+
throw new Error("stuck");
|
|
49
|
+
});
|
|
50
|
+
await b.subscribe(name, "b", async () => {
|
|
51
|
+
got.push("b");
|
|
52
|
+
});
|
|
53
|
+
await a.emit(name, { id: "1" });
|
|
54
|
+
await a.drain();
|
|
55
|
+
expect(aCalls).toBe(1);
|
|
56
|
+
await b.drain();
|
|
57
|
+
expect(got).toEqual([]);
|
|
58
|
+
nowMs += SIGNAL_REDIS_MIN_IDLE_MS;
|
|
59
|
+
await b.drain();
|
|
60
|
+
expect(got).toEqual(["b"]);
|
|
61
|
+
await a.drain();
|
|
62
|
+
await b.drain();
|
|
63
|
+
expect(aCalls).toBe(1);
|
|
64
|
+
expect(got).toEqual(["b"]);
|
|
65
|
+
await a.close();
|
|
66
|
+
await b.close();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("a slow handler whose idle is reset is not stolen", async () => {
|
|
70
|
+
let nowMs = 0;
|
|
71
|
+
const now = () => nowMs;
|
|
72
|
+
const redis = createSignalRedisFake({ now });
|
|
73
|
+
const name = "slow-handler";
|
|
74
|
+
const shared = { signals: onceDecl(name, 3), redis, compete: true as const, now };
|
|
75
|
+
const a = await openRedisSignal({ ...shared, consumerId: "a" });
|
|
76
|
+
const b = await openRedisSignal({ ...shared, consumerId: "b" });
|
|
77
|
+
let release = (): void => {};
|
|
78
|
+
const gate = new Promise<void>((resolve) => {
|
|
79
|
+
release = resolve;
|
|
80
|
+
});
|
|
81
|
+
let started = (): void => {};
|
|
82
|
+
const startedP = new Promise<void>((resolve) => {
|
|
83
|
+
started = resolve;
|
|
84
|
+
});
|
|
85
|
+
let calls = 0;
|
|
86
|
+
const stolen: string[] = [];
|
|
87
|
+
await a.subscribe(name, "a", async () => {
|
|
88
|
+
calls += 1;
|
|
89
|
+
started();
|
|
90
|
+
await gate;
|
|
91
|
+
});
|
|
92
|
+
await b.subscribe(name, "b", async () => {
|
|
93
|
+
stolen.push("b");
|
|
94
|
+
});
|
|
95
|
+
await a.emit(name, { id: "1" });
|
|
96
|
+
const draining = a.drain();
|
|
97
|
+
await startedP;
|
|
98
|
+
await a.drain();
|
|
99
|
+
expect(calls).toBe(1);
|
|
100
|
+
nowMs += SIGNAL_REDIS_MIN_IDLE_MS;
|
|
101
|
+
await redis.flushIdle();
|
|
102
|
+
nowMs += SIGNAL_REDIS_MIN_IDLE_MS - 1;
|
|
103
|
+
await b.drain();
|
|
104
|
+
expect(stolen).toEqual([]);
|
|
105
|
+
expect(calls).toBe(1);
|
|
106
|
+
release();
|
|
107
|
+
await draining;
|
|
108
|
+
await a.close();
|
|
109
|
+
await b.close();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("retries 2 dead-letters after exactly 3 runs across reclaim", async () => {
|
|
113
|
+
let nowMs = 0;
|
|
114
|
+
const now = () => nowMs;
|
|
115
|
+
const redis = createSignalRedisFake({ now });
|
|
116
|
+
const name = "always-fail";
|
|
117
|
+
const shared = { signals: onceDecl(name, 2), redis, compete: true as const, now };
|
|
118
|
+
const a = await openRedisSignal({ ...shared, consumerId: "a" });
|
|
119
|
+
const b = await openRedisSignal({ ...shared, consumerId: "b" });
|
|
120
|
+
let calls = 0;
|
|
121
|
+
const fail = async (): Promise<void> => {
|
|
122
|
+
calls += 1;
|
|
123
|
+
throw new Error("nope");
|
|
124
|
+
};
|
|
125
|
+
await a.subscribe(name, "a", fail);
|
|
126
|
+
await b.subscribe(name, "b", fail);
|
|
127
|
+
await a.emit(name, { id: "1" });
|
|
128
|
+
await a.drain();
|
|
129
|
+
expect(calls).toBe(1);
|
|
130
|
+
nowMs += SIGNAL_REDIS_MIN_IDLE_MS;
|
|
131
|
+
await b.drain();
|
|
132
|
+
expect(calls).toBe(2);
|
|
133
|
+
nowMs += SIGNAL_REDIS_MIN_IDLE_MS;
|
|
134
|
+
await a.drain();
|
|
135
|
+
expect(calls).toBe(3);
|
|
136
|
+
const dead = await a.deadLetters(name);
|
|
137
|
+
expect(dead).toHaveLength(1);
|
|
138
|
+
expect(dead[0]?.attempts).toBe(3);
|
|
139
|
+
nowMs += SIGNAL_REDIS_MIN_IDLE_MS;
|
|
140
|
+
await b.drain();
|
|
141
|
+
expect(calls).toBe(3);
|
|
142
|
+
await a.close();
|
|
143
|
+
await b.close();
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe("redis broadcast and live", () => {
|
|
148
|
+
test("delivers another origin, skips own origin, and accepts a legacy payload", async () => {
|
|
149
|
+
const redis = createSignalRedisFake();
|
|
150
|
+
const name = "news-relay";
|
|
151
|
+
const decl = signal.broadcast(name, { optional: true });
|
|
152
|
+
const bus = await openRedisSignal({
|
|
153
|
+
signals: new Map([[decl.name, decl]]),
|
|
154
|
+
redis,
|
|
155
|
+
consumerId: "self",
|
|
156
|
+
});
|
|
157
|
+
const got: unknown[] = [];
|
|
158
|
+
await bus.subscribe(name, "sub", async (msg) => {
|
|
159
|
+
got.push(msg.payload);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
await redis.publish(
|
|
163
|
+
`oke:signal:bcast:${name}`,
|
|
164
|
+
JSON.stringify({ __oke_env: 1, id: "m-other", origin: "other", payload: { n: 1 } }),
|
|
165
|
+
);
|
|
166
|
+
expect(got).toEqual([{ n: 1 }]);
|
|
167
|
+
|
|
168
|
+
await redis.publish(
|
|
169
|
+
`oke:signal:bcast:${name}`,
|
|
170
|
+
JSON.stringify({ __oke_env: 1, id: "m-self", origin: "self", payload: { n: 2 } }),
|
|
171
|
+
);
|
|
172
|
+
expect(got).toEqual([{ n: 1 }]);
|
|
173
|
+
|
|
174
|
+
await redis.publish(`oke:signal:bcast:${name}`, JSON.stringify({ n: 3 }));
|
|
175
|
+
expect(got).toEqual([{ n: 1 }, { n: 3 }]);
|
|
176
|
+
|
|
177
|
+
await bus.emit(name, { n: 4 });
|
|
178
|
+
await bus.drain();
|
|
179
|
+
expect(got).toEqual([{ n: 1 }, { n: 3 }, { n: 4 }]);
|
|
180
|
+
const last = redis.published.at(-1);
|
|
181
|
+
expect(last?.channel).toBe(`oke:signal:bcast:${name}`);
|
|
182
|
+
expect(JSON.parse(last?.message ?? "null")).toMatchObject({
|
|
183
|
+
__oke_env: 1,
|
|
184
|
+
origin: "self",
|
|
185
|
+
payload: { n: 4 },
|
|
186
|
+
});
|
|
187
|
+
await bus.close();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("live pubsub delivers another origin and a legacy payload, and skips own origin", async () => {
|
|
191
|
+
const redis = createSignalRedisFake();
|
|
192
|
+
const name = "seat-relay";
|
|
193
|
+
const decl = signal.live(name, { optional: true });
|
|
194
|
+
const bus = await openRedisSignal({
|
|
195
|
+
signals: new Map([[decl.name, decl]]),
|
|
196
|
+
redis,
|
|
197
|
+
consumerId: "self",
|
|
198
|
+
});
|
|
199
|
+
const iter = bus.live(name)[Symbol.asyncIterator]();
|
|
200
|
+
const pending = iter.next();
|
|
201
|
+
await Bun.sleep(0);
|
|
202
|
+
|
|
203
|
+
await redis.publish(
|
|
204
|
+
`oke:signal:live:${name}`,
|
|
205
|
+
JSON.stringify({ __oke_env: 1, id: "live-other", origin: "other", payload: { n: 1 } }),
|
|
206
|
+
);
|
|
207
|
+
const first = await pending;
|
|
208
|
+
expect(first.done).toBe(false);
|
|
209
|
+
if (!first.done) expect(first.value).toEqual({ id: "live-other", payload: { n: 1 } });
|
|
210
|
+
|
|
211
|
+
const skipped = iter.next();
|
|
212
|
+
await redis.publish(
|
|
213
|
+
`oke:signal:live:${name}`,
|
|
214
|
+
JSON.stringify({ __oke_env: 1, id: "live-self", origin: "self", payload: { n: 2 } }),
|
|
215
|
+
);
|
|
216
|
+
await redis.publish(`oke:signal:live:${name}`, JSON.stringify({ n: 3 }));
|
|
217
|
+
const third = await skipped;
|
|
218
|
+
expect(third.done).toBe(false);
|
|
219
|
+
if (!third.done) expect(third.value.payload).toEqual({ n: 3 });
|
|
220
|
+
|
|
221
|
+
await iter.return?.();
|
|
222
|
+
await bus.close();
|
|
223
|
+
});
|
|
224
|
+
});
|