@opengeni/api-router 0.5.4 → 0.5.5
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/dist/app.d.ts +9 -1
- package/dist/app.js +7 -1
- package/dist/{chunk-DO2G3JSB.js → chunk-HBEJMWD3.js} +528 -270
- package/dist/chunk-HBEJMWD3.js.map +1 -0
- package/dist/index.js +8 -27
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/app.ts +180 -31
- package/src/http/sse.ts +57 -2
- package/src/index.ts +8 -27
- package/src/mcp/server.ts +167 -85
- package/src/routes/sessions.ts +141 -109
- package/src/routes/workspaces.ts +30 -43
- package/dist/chunk-DO2G3JSB.js.map +0 -1
package/src/app.ts
CHANGED
|
@@ -3,7 +3,13 @@ import {
|
|
|
3
3
|
configuredAllowedReasoningEfforts,
|
|
4
4
|
configuredModels,
|
|
5
5
|
} from "@opengeni/config";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
ClientConfig,
|
|
8
|
+
OPENGENI_API_CONTRACT_HEADER,
|
|
9
|
+
OPENGENI_API_CONTRACT_REVISION,
|
|
10
|
+
resolveWorkspaceMemoryEnabled,
|
|
11
|
+
type AccessGrant,
|
|
12
|
+
} from "@opengeni/contracts";
|
|
7
13
|
import {
|
|
8
14
|
createDocumentServices,
|
|
9
15
|
indexDocumentNow,
|
|
@@ -14,6 +20,7 @@ import { createObservability } from "@opengeni/observability";
|
|
|
14
20
|
import { createObjectStorage } from "@opengeni/storage";
|
|
15
21
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
16
22
|
import { Hono } from "hono";
|
|
23
|
+
import { bodyLimit } from "hono/body-limit";
|
|
17
24
|
import { cors } from "hono/cors";
|
|
18
25
|
import { HTTPException } from "hono/http-exception";
|
|
19
26
|
import type { ApiRouteDeps, AppDependencies } from "@opengeni/core";
|
|
@@ -62,7 +69,9 @@ export {
|
|
|
62
69
|
withDefaultEnabledCapabilityMcpTools,
|
|
63
70
|
} from "@opengeni/core";
|
|
64
71
|
export { workflowIdForSession } from "@opengeni/core";
|
|
65
|
-
export { replaySessionEvents, sseSessionStream } from "./http/sse";
|
|
72
|
+
export { replaySessionEvents, sseSessionStream, sseWorkspaceControlStream } from "./http/sse";
|
|
73
|
+
|
|
74
|
+
export const API_MAX_REQUEST_BODY_BYTES = 8 * 1024 * 1024;
|
|
66
75
|
|
|
67
76
|
export function createApp(deps: AppDependencies): Hono {
|
|
68
77
|
const managedAuth = deps.managedAuth ?? createManagedAuth(deps.settings, deps.db);
|
|
@@ -84,7 +93,9 @@ export function createApp(deps: AppDependencies): Hono {
|
|
|
84
93
|
documentId: string;
|
|
85
94
|
}) => {
|
|
86
95
|
if (!objectStorage) {
|
|
87
|
-
throw new HTTPException(503, {
|
|
96
|
+
throw new HTTPException(503, {
|
|
97
|
+
message: "object storage is not configured",
|
|
98
|
+
});
|
|
88
99
|
}
|
|
89
100
|
return await indexDocumentNow(
|
|
90
101
|
deps.db,
|
|
@@ -130,6 +141,15 @@ export function createApp(deps: AppDependencies): Hono {
|
|
|
130
141
|
"*",
|
|
131
142
|
cors({
|
|
132
143
|
credentials: true,
|
|
144
|
+
allowHeaders: [
|
|
145
|
+
"Accept",
|
|
146
|
+
"Authorization",
|
|
147
|
+
"Content-Type",
|
|
148
|
+
"X-OpenGeni-Access-Key",
|
|
149
|
+
"X-OpenGeni-Api-Contract",
|
|
150
|
+
"X-OpenGeni-Subject",
|
|
151
|
+
],
|
|
152
|
+
exposeHeaders: ["X-OpenGeni-Api-Contract"],
|
|
133
153
|
origin: (origin) => {
|
|
134
154
|
if (!origin) {
|
|
135
155
|
return null;
|
|
@@ -139,6 +159,15 @@ export function createApp(deps: AppDependencies): Hono {
|
|
|
139
159
|
}),
|
|
140
160
|
);
|
|
141
161
|
|
|
162
|
+
app.use(
|
|
163
|
+
"*",
|
|
164
|
+
bodyLimit({
|
|
165
|
+
maxSize: API_MAX_REQUEST_BODY_BYTES,
|
|
166
|
+
onError: (c) =>
|
|
167
|
+
c.json({ code: "PAYLOAD_TOO_LARGE", message: "Request body is too large." }, 413),
|
|
168
|
+
}),
|
|
169
|
+
);
|
|
170
|
+
|
|
142
171
|
app.use("*", async (c, next) => {
|
|
143
172
|
const url = new URL(c.req.url);
|
|
144
173
|
const route = routeLabel(url.pathname);
|
|
@@ -152,7 +181,12 @@ export function createApp(deps: AppDependencies): Hono {
|
|
|
152
181
|
await next();
|
|
153
182
|
const status = c.res.status || 200;
|
|
154
183
|
const durationSeconds = (performance.now() - start) / 1000;
|
|
155
|
-
observability.recordHttpRequest({
|
|
184
|
+
observability.recordHttpRequest({
|
|
185
|
+
method: c.req.method,
|
|
186
|
+
route,
|
|
187
|
+
status,
|
|
188
|
+
durationSeconds,
|
|
189
|
+
});
|
|
156
190
|
span.end({
|
|
157
191
|
attributes: {
|
|
158
192
|
"http.response.status_code": status,
|
|
@@ -170,7 +204,12 @@ export function createApp(deps: AppDependencies): Hono {
|
|
|
170
204
|
} catch (error) {
|
|
171
205
|
const status = httpStatusForError(error);
|
|
172
206
|
const durationSeconds = (performance.now() - start) / 1000;
|
|
173
|
-
observability.recordHttpRequest({
|
|
207
|
+
observability.recordHttpRequest({
|
|
208
|
+
method: c.req.method,
|
|
209
|
+
route,
|
|
210
|
+
status,
|
|
211
|
+
durationSeconds,
|
|
212
|
+
});
|
|
174
213
|
span.end({
|
|
175
214
|
attributes: {
|
|
176
215
|
"http.response.status_code": status,
|
|
@@ -193,6 +232,25 @@ export function createApp(deps: AppDependencies): Hono {
|
|
|
193
232
|
|
|
194
233
|
app.use("*", requireAccessKey(deps.settings));
|
|
195
234
|
|
|
235
|
+
app.use("/v1/*", async (c, next) => {
|
|
236
|
+
c.header(OPENGENI_API_CONTRACT_HEADER, OPENGENI_API_CONTRACT_REVISION);
|
|
237
|
+
if (
|
|
238
|
+
deps.settings.environment !== "test" &&
|
|
239
|
+
isApiContractProtectedMutation(c.req.method, new URL(c.req.url).pathname) &&
|
|
240
|
+
c.req.header(OPENGENI_API_CONTRACT_HEADER) !== OPENGENI_API_CONTRACT_REVISION
|
|
241
|
+
) {
|
|
242
|
+
return c.json(
|
|
243
|
+
{
|
|
244
|
+
code: "API_CONTRACT_CHANGED",
|
|
245
|
+
message: "OpenGeni updated. Reload this client before changing state.",
|
|
246
|
+
apiContractRevision: OPENGENI_API_CONTRACT_REVISION,
|
|
247
|
+
},
|
|
248
|
+
409,
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
await next();
|
|
252
|
+
});
|
|
253
|
+
|
|
196
254
|
if (managedAuth) {
|
|
197
255
|
app.on(["GET", "POST"], "/v1/auth/*", (c) => managedAuth.handler(c.req.raw));
|
|
198
256
|
}
|
|
@@ -218,10 +276,12 @@ export function createApp(deps: AppDependencies): Hono {
|
|
|
218
276
|
}),
|
|
219
277
|
);
|
|
220
278
|
|
|
221
|
-
app.get("/v1/config/client", (c) =>
|
|
222
|
-
c.
|
|
279
|
+
app.get("/v1/config/client", (c) => {
|
|
280
|
+
c.header("cache-control", "no-store");
|
|
281
|
+
return c.json(
|
|
223
282
|
ClientConfig.parse({
|
|
224
283
|
deploymentRevision: deps.settings.deploymentRevision,
|
|
284
|
+
apiContractRevision: OPENGENI_API_CONTRACT_REVISION,
|
|
225
285
|
...(deps.settings.serverVersion ? { serverVersion: deps.settings.serverVersion } : {}),
|
|
226
286
|
defaultModel: deps.settings.openaiModel,
|
|
227
287
|
allowedModels: configuredAllowedModels(deps.settings),
|
|
@@ -256,8 +316,8 @@ export function createApp(deps: AppDependencies): Hono {
|
|
|
256
316
|
// Per-session availability is still negotiated on /stream-capabilities.
|
|
257
317
|
structuredServices: structuredServicesHint(deps.settings.sandboxBackend),
|
|
258
318
|
}),
|
|
259
|
-
)
|
|
260
|
-
);
|
|
319
|
+
);
|
|
320
|
+
});
|
|
261
321
|
|
|
262
322
|
app.all("/v1/workspaces/:workspaceId/mcp", async (c) => {
|
|
263
323
|
const workspaceId = c.req.param("workspaceId");
|
|
@@ -267,7 +327,9 @@ export function createApp(deps: AppDependencies): Hono {
|
|
|
267
327
|
: null;
|
|
268
328
|
const workspace = await getWorkspace(routeDeps.db, workspaceId);
|
|
269
329
|
const workspaceMemoryEnabled = resolveWorkspaceMemoryEnabled(workspace?.settings);
|
|
270
|
-
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
330
|
+
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
331
|
+
enableJsonResponse: true,
|
|
332
|
+
});
|
|
271
333
|
const mcp = buildOpenGeniMcpServer(routeDeps, grant, {
|
|
272
334
|
requestOrigin: new URL(c.req.url).origin,
|
|
273
335
|
toolspace,
|
|
@@ -332,7 +394,10 @@ function clientAuthConfig(settings: AppDependencies["settings"]) {
|
|
|
332
394
|
};
|
|
333
395
|
}
|
|
334
396
|
if (settings.authRequired) {
|
|
335
|
-
return {
|
|
397
|
+
return {
|
|
398
|
+
mode: "deploymentKey" as const,
|
|
399
|
+
headerName: "x-opengeni-access-key" as const,
|
|
400
|
+
};
|
|
336
401
|
}
|
|
337
402
|
return { mode: "none" as const };
|
|
338
403
|
}
|
|
@@ -399,7 +464,10 @@ async function runReadinessChecks(
|
|
|
399
464
|
} catch (error) {
|
|
400
465
|
return [
|
|
401
466
|
name,
|
|
402
|
-
{
|
|
467
|
+
{
|
|
468
|
+
ok: false,
|
|
469
|
+
error: error instanceof Error ? error.message : String(error),
|
|
470
|
+
},
|
|
403
471
|
] as const;
|
|
404
472
|
}
|
|
405
473
|
},
|
|
@@ -453,15 +521,24 @@ const routeLabelPatterns: Array<{ pattern: RegExp; label: string }> = [
|
|
|
453
521
|
pattern: /^\/v1\/workspaces\/[^/]+\/codex\/usage$/,
|
|
454
522
|
label: "/v1/workspaces/:workspaceId/codex/usage",
|
|
455
523
|
},
|
|
456
|
-
{
|
|
524
|
+
{
|
|
525
|
+
pattern: /^\/v1\/workspaces\/[^/]+\/codex$/,
|
|
526
|
+
label: "/v1/workspaces/:workspaceId/codex",
|
|
527
|
+
},
|
|
457
528
|
{ pattern: /^\/metrics$/, label: "/metrics" },
|
|
458
529
|
{ pattern: /^\/v1\/config\/client$/, label: "/v1/config/client" },
|
|
459
530
|
{ pattern: /^\/v1\/billing$/, label: "/v1/billing" },
|
|
460
531
|
{ pattern: /^\/v1\/billing\/checkout$/, label: "/v1/billing/checkout" },
|
|
461
532
|
{ pattern: /^\/v1\/billing\/usage$/, label: "/v1/billing/usage" },
|
|
462
|
-
{
|
|
533
|
+
{
|
|
534
|
+
pattern: /^\/v1\/billing\/entitlements$/,
|
|
535
|
+
label: "/v1/billing/entitlements",
|
|
536
|
+
},
|
|
463
537
|
{ pattern: /^\/v1\/webhooks\/stripe$/, label: "/v1/webhooks/stripe" },
|
|
464
|
-
{
|
|
538
|
+
{
|
|
539
|
+
pattern: /^\/v1\/workspaces\/[^/]+\/mcp$/,
|
|
540
|
+
label: "/v1/workspaces/:workspaceId/mcp",
|
|
541
|
+
},
|
|
465
542
|
{
|
|
466
543
|
pattern: /^\/v1\/workspaces\/[^/]+\/mcp\/docs$/,
|
|
467
544
|
label: "/v1/workspaces/:workspaceId/mcp/docs",
|
|
@@ -470,7 +547,22 @@ const routeLabelPatterns: Array<{ pattern: RegExp; label: string }> = [
|
|
|
470
547
|
pattern: /^\/v1\/workspaces\/[^/]+\/default-rig$/,
|
|
471
548
|
label: "/v1/workspaces/:workspaceId/default-rig",
|
|
472
549
|
},
|
|
473
|
-
{
|
|
550
|
+
{
|
|
551
|
+
pattern: /^\/v1\/workspaces\/[^/]+\/sessions$/,
|
|
552
|
+
label: "/v1/workspaces/:workspaceId/sessions",
|
|
553
|
+
},
|
|
554
|
+
{
|
|
555
|
+
pattern: /^\/v1\/workspaces\/[^/]+\/control-events\/stream$/,
|
|
556
|
+
label: "/v1/workspaces/:workspaceId/control-events/stream",
|
|
557
|
+
},
|
|
558
|
+
{
|
|
559
|
+
pattern: /^\/v1\/workspaces\/[^/]+\/control-events$/,
|
|
560
|
+
label: "/v1/workspaces/:workspaceId/control-events",
|
|
561
|
+
},
|
|
562
|
+
{
|
|
563
|
+
pattern: /^\/v1\/workspaces\/[^/]+\/inference-control$/,
|
|
564
|
+
label: "/v1/workspaces/:workspaceId/inference-control",
|
|
565
|
+
},
|
|
474
566
|
{
|
|
475
567
|
pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/events\/stream$/,
|
|
476
568
|
label: "/v1/workspaces/:workspaceId/sessions/:id/events/stream",
|
|
@@ -484,16 +576,20 @@ const routeLabelPatterns: Array<{ pattern: RegExp; label: string }> = [
|
|
|
484
576
|
label: "/v1/workspaces/:workspaceId/sessions/:id/events",
|
|
485
577
|
},
|
|
486
578
|
{
|
|
487
|
-
pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/
|
|
488
|
-
label: "/v1/workspaces/:workspaceId/sessions/:id/
|
|
579
|
+
pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/queue\/[^/]+\/(move|edit|steer|delete)$/,
|
|
580
|
+
label: "/v1/workspaces/:workspaceId/sessions/:id/queue/:turnId/:action",
|
|
489
581
|
},
|
|
490
582
|
{
|
|
491
|
-
pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/
|
|
492
|
-
label: "/v1/workspaces/:workspaceId/sessions/:id/
|
|
583
|
+
pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/queue$/,
|
|
584
|
+
label: "/v1/workspaces/:workspaceId/sessions/:id/queue",
|
|
493
585
|
},
|
|
494
586
|
{
|
|
495
|
-
pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/
|
|
496
|
-
label: "/v1/workspaces/:workspaceId/sessions/:id/
|
|
587
|
+
pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/composer-draft$/,
|
|
588
|
+
label: "/v1/workspaces/:workspaceId/sessions/:id/composer-draft",
|
|
589
|
+
},
|
|
590
|
+
{
|
|
591
|
+
pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/(control|steer)$/,
|
|
592
|
+
label: "/v1/workspaces/:workspaceId/sessions/:id/:controlAction",
|
|
497
593
|
},
|
|
498
594
|
{
|
|
499
595
|
pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/stream-capabilities$/,
|
|
@@ -535,7 +631,10 @@ const routeLabelPatterns: Array<{ pattern: RegExp; label: string }> = [
|
|
|
535
631
|
pattern: /^\/v1\/workspaces\/[^/]+\/files\/[^/]+$/,
|
|
536
632
|
label: "/v1/workspaces/:workspaceId/files/:id",
|
|
537
633
|
},
|
|
538
|
-
{
|
|
634
|
+
{
|
|
635
|
+
pattern: /^\/v1\/workspaces\/[^/]+\/api-keys$/,
|
|
636
|
+
label: "/v1/workspaces/:workspaceId/api-keys",
|
|
637
|
+
},
|
|
539
638
|
{
|
|
540
639
|
pattern: /^\/v1\/workspaces\/[^/]+\/api-keys\/[^/]+$/,
|
|
541
640
|
label: "/v1/workspaces/:workspaceId/api-keys/:id",
|
|
@@ -644,7 +743,10 @@ const routeLabelPatterns: Array<{ pattern: RegExp; label: string }> = [
|
|
|
644
743
|
pattern: /^\/v1\/workspaces\/[^/]+\/environments\/[^/]+$/,
|
|
645
744
|
label: "/v1/workspaces/:workspaceId/environments/:id",
|
|
646
745
|
},
|
|
647
|
-
{
|
|
746
|
+
{
|
|
747
|
+
pattern: /^\/v1\/workspaces\/[^/]+\/packs$/,
|
|
748
|
+
label: "/v1/workspaces/:workspaceId/packs",
|
|
749
|
+
},
|
|
648
750
|
{
|
|
649
751
|
pattern: /^\/v1\/workspaces\/[^/]+\/packs\/installations$/,
|
|
650
752
|
label: "/v1/workspaces/:workspaceId/packs/installations",
|
|
@@ -682,13 +784,22 @@ const routeLabelPatterns: Array<{ pattern: RegExp; label: string }> = [
|
|
|
682
784
|
label: "/v1/workspaces/:workspaceId/connections/:connectionId",
|
|
683
785
|
},
|
|
684
786
|
{ pattern: /^\/v1\/catalog-assets\/.+$/, label: "/v1/catalog-assets/*" },
|
|
685
|
-
{
|
|
787
|
+
{
|
|
788
|
+
pattern: /^\/v1\/integrations\/oauth\/callback$/,
|
|
789
|
+
label: "/v1/integrations/oauth/callback",
|
|
790
|
+
},
|
|
686
791
|
{
|
|
687
792
|
pattern: /^\/v1\/integrations\/oauth\/client-metadata\.json$/,
|
|
688
793
|
label: "/v1/integrations/oauth/client-metadata.json",
|
|
689
794
|
},
|
|
690
|
-
{
|
|
691
|
-
|
|
795
|
+
{
|
|
796
|
+
pattern: /^\/v1\/enrollments\/device\/start$/,
|
|
797
|
+
label: "/v1/enrollments/device/start",
|
|
798
|
+
},
|
|
799
|
+
{
|
|
800
|
+
pattern: /^\/v1\/enrollments\/device\/poll$/,
|
|
801
|
+
label: "/v1/enrollments/device/poll",
|
|
802
|
+
},
|
|
692
803
|
{
|
|
693
804
|
pattern: /^\/v1\/workspaces\/[^/]+\/enrollments\/device\/approve$/,
|
|
694
805
|
label: "/v1/workspaces/:workspaceId/enrollments/device/approve",
|
|
@@ -705,11 +816,23 @@ const routeLabelPatterns: Array<{ pattern: RegExp; label: string }> = [
|
|
|
705
816
|
pattern: /^\/v1\/workspaces\/[^/]+\/machines\/[^/]+\/metrics\/series$/,
|
|
706
817
|
label: "/v1/workspaces/:workspaceId/machines/:enrollmentId/metrics/series",
|
|
707
818
|
},
|
|
708
|
-
{
|
|
709
|
-
|
|
819
|
+
{
|
|
820
|
+
pattern: /^\/v1\/workspaces\/[^/]+\/machines$/,
|
|
821
|
+
label: "/v1/workspaces/:workspaceId/machines",
|
|
822
|
+
},
|
|
823
|
+
{
|
|
824
|
+
pattern: /^\/v1\/github\/app-manifest\/callback$/,
|
|
825
|
+
label: "/v1/github/app-manifest/callback",
|
|
826
|
+
},
|
|
710
827
|
{ pattern: /^\/v1\/github\/setup$/, label: "/v1/github/setup" },
|
|
711
|
-
{
|
|
712
|
-
|
|
828
|
+
{
|
|
829
|
+
pattern: /^\/v1\/github\/install\/callback$/,
|
|
830
|
+
label: "/v1/github/install/callback",
|
|
831
|
+
},
|
|
832
|
+
{
|
|
833
|
+
pattern: /^\/v1\/github\/oauth\/callback$/,
|
|
834
|
+
label: "/v1/github/oauth/callback",
|
|
835
|
+
},
|
|
713
836
|
];
|
|
714
837
|
|
|
715
838
|
export function routeLabel(pathname: string): string {
|
|
@@ -719,3 +842,29 @@ export function routeLabel(pathname: string): string {
|
|
|
719
842
|
}
|
|
720
843
|
return pathname.startsWith("/v1/") ? "/v1/unknown" : "/unknown";
|
|
721
844
|
}
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* State-changing OpenGeni HTTP calls must never cross an incompatible rollout
|
|
848
|
+
* boundary. Standard third-party protocols and externally initiated callbacks
|
|
849
|
+
* are intentionally outside this product API contract.
|
|
850
|
+
*/
|
|
851
|
+
export function isApiContractProtectedMutation(method: string, pathname: string): boolean {
|
|
852
|
+
if (!new Set(["POST", "PUT", "PATCH", "DELETE"]).has(method.toUpperCase())) {
|
|
853
|
+
return false;
|
|
854
|
+
}
|
|
855
|
+
if (!pathname.startsWith("/v1/")) {
|
|
856
|
+
return false;
|
|
857
|
+
}
|
|
858
|
+
if (
|
|
859
|
+
pathname.startsWith("/v1/auth/") ||
|
|
860
|
+
pathname.startsWith("/v1/webhooks/") ||
|
|
861
|
+
pathname.startsWith("/v1/integrations/oauth/") ||
|
|
862
|
+
pathname.startsWith("/v1/github/") ||
|
|
863
|
+
pathname === "/v1/enrollments/device/start" ||
|
|
864
|
+
pathname === "/v1/enrollments/device/poll" ||
|
|
865
|
+
pathname === "/v1/enrollments/token/exchange"
|
|
866
|
+
) {
|
|
867
|
+
return false;
|
|
868
|
+
}
|
|
869
|
+
return !pathname.split("/").includes("mcp");
|
|
870
|
+
}
|
package/src/http/sse.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { SessionEvent } from "@opengeni/contracts";
|
|
2
|
-
import { listSessionEvents, type Database } from "@opengeni/db";
|
|
1
|
+
import type { SessionEvent, WorkspaceControlEvent } from "@opengeni/contracts";
|
|
2
|
+
import { listSessionEvents, listWorkspaceControlEvents, type Database } from "@opengeni/db";
|
|
3
3
|
import { formatSse, type EventBus } from "@opengeni/events";
|
|
4
4
|
|
|
5
5
|
export async function sseSessionStream(
|
|
@@ -108,3 +108,58 @@ export async function replaySessionEvents(
|
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
|
+
|
|
112
|
+
export async function sseWorkspaceControlStream(
|
|
113
|
+
db: Database,
|
|
114
|
+
bus: EventBus,
|
|
115
|
+
workspaceId: string,
|
|
116
|
+
after: number,
|
|
117
|
+
signal: AbortSignal,
|
|
118
|
+
): Promise<Response> {
|
|
119
|
+
const encoder = new TextEncoder();
|
|
120
|
+
let lastSent = after;
|
|
121
|
+
let replaying = true;
|
|
122
|
+
const buffered: WorkspaceControlEvent[] = [];
|
|
123
|
+
let unsubscribe: (() => void) | null = null;
|
|
124
|
+
|
|
125
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
126
|
+
start: async (controller) => {
|
|
127
|
+
const send = (event: WorkspaceControlEvent) => {
|
|
128
|
+
if (event.sequence <= lastSent) return;
|
|
129
|
+
controller.enqueue(encoder.encode(formatSse(event)));
|
|
130
|
+
lastSent = event.sequence;
|
|
131
|
+
};
|
|
132
|
+
unsubscribe = await bus.subscribeWorkspaceControl(workspaceId, async (event) => {
|
|
133
|
+
if (replaying) {
|
|
134
|
+
buffered.push(event);
|
|
135
|
+
} else {
|
|
136
|
+
send(event);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
let cursor = after;
|
|
140
|
+
while (true) {
|
|
141
|
+
const page = await listWorkspaceControlEvents(db, workspaceId, cursor, 1000);
|
|
142
|
+
for (const event of page) {
|
|
143
|
+
send(event);
|
|
144
|
+
cursor = Math.max(cursor, event.sequence);
|
|
145
|
+
}
|
|
146
|
+
if (page.length < 1000) break;
|
|
147
|
+
}
|
|
148
|
+
replaying = false;
|
|
149
|
+
for (const event of buffered.sort((left, right) => left.sequence - right.sequence)) {
|
|
150
|
+
send(event);
|
|
151
|
+
}
|
|
152
|
+
buffered.length = 0;
|
|
153
|
+
controller.enqueue(encoder.encode(": connected\n\n"));
|
|
154
|
+
},
|
|
155
|
+
cancel: () => unsubscribe?.(),
|
|
156
|
+
});
|
|
157
|
+
signal.addEventListener("abort", () => unsubscribe?.(), { once: true });
|
|
158
|
+
return new Response(stream, {
|
|
159
|
+
headers: {
|
|
160
|
+
"Content-Type": "text/event-stream; charset=utf-8",
|
|
161
|
+
"Cache-Control": "no-cache, no-transform",
|
|
162
|
+
Connection: "keep-alive",
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
import { createDb, markSessionWorkflowWakeDelivered, type Database } from "@opengeni/db";
|
|
15
15
|
import { createNatsEventBus, type ResponderConnection } from "@opengeni/events";
|
|
16
16
|
import { createObservability, logStartupDependencyRetry } from "@opengeni/observability";
|
|
17
|
+
import { SESSION_WORKFLOW_WAKE_DISPATCHER_SCHEDULE_ID } from "@opengeni/core";
|
|
17
18
|
import {
|
|
18
19
|
Connection,
|
|
19
20
|
Client as TemporalClient,
|
|
@@ -75,13 +76,14 @@ export async function createTemporalWorkflowClient(
|
|
|
75
76
|
sessionId,
|
|
76
77
|
workflowId,
|
|
77
78
|
wakeRevision,
|
|
79
|
+
interruptionRequested,
|
|
78
80
|
}) => {
|
|
79
81
|
await temporal.workflow.signalWithStart("sessionWorkflow", {
|
|
80
82
|
taskQueue: settings.temporalTaskQueue,
|
|
81
83
|
workflowId,
|
|
82
84
|
workflowIdReusePolicy: "ALLOW_DUPLICATE",
|
|
83
85
|
args: [{ accountId, workspaceId, sessionId }],
|
|
84
|
-
signal: "queueChanged",
|
|
86
|
+
signal: interruptionRequested ? "sessionControl" : "queueChanged",
|
|
85
87
|
});
|
|
86
88
|
await markSessionWorkflowWakeDelivered(db, {
|
|
87
89
|
accountId,
|
|
@@ -91,6 +93,11 @@ export async function createTemporalWorkflowClient(
|
|
|
91
93
|
wakeRevision,
|
|
92
94
|
});
|
|
93
95
|
},
|
|
96
|
+
requestSessionWorkflowWakeDispatch: async () => {
|
|
97
|
+
await temporal.schedule
|
|
98
|
+
.getHandle(SESSION_WORKFLOW_WAKE_DISPATCHER_SCHEDULE_ID)
|
|
99
|
+
.trigger(ScheduleOverlapPolicy.BUFFER_ONE);
|
|
100
|
+
},
|
|
94
101
|
signalCodexCapacity: async ({
|
|
95
102
|
accountId,
|
|
96
103
|
workspaceId,
|
|
@@ -139,32 +146,6 @@ export async function createTemporalWorkflowClient(
|
|
|
139
146
|
wakeRevision: workflowWakeRevision,
|
|
140
147
|
});
|
|
141
148
|
},
|
|
142
|
-
signalSessionControl: async ({
|
|
143
|
-
accountId,
|
|
144
|
-
workspaceId,
|
|
145
|
-
sessionId,
|
|
146
|
-
eventId,
|
|
147
|
-
workflowId,
|
|
148
|
-
workflowWakeRevision,
|
|
149
|
-
}) => {
|
|
150
|
-
// Start-or-signal: a control sent after the prior workflow returned idle
|
|
151
|
-
// starts a fresh run with the durable control already buffered.
|
|
152
|
-
await temporal.workflow.signalWithStart("sessionWorkflow", {
|
|
153
|
-
taskQueue: settings.temporalTaskQueue,
|
|
154
|
-
workflowId,
|
|
155
|
-
workflowIdReusePolicy: "ALLOW_DUPLICATE",
|
|
156
|
-
args: [{ accountId, workspaceId, sessionId }],
|
|
157
|
-
signal: "sessionControl",
|
|
158
|
-
signalArgs: [eventId],
|
|
159
|
-
});
|
|
160
|
-
await markSessionWorkflowWakeDelivered(db, {
|
|
161
|
-
accountId,
|
|
162
|
-
workspaceId,
|
|
163
|
-
sessionId,
|
|
164
|
-
temporalWorkflowId: workflowId,
|
|
165
|
-
wakeRevision: workflowWakeRevision,
|
|
166
|
-
});
|
|
167
|
-
},
|
|
168
149
|
syncScheduledTask: async ({ task }) => {
|
|
169
150
|
const schedule = temporal.schedule.getHandle(task.temporalScheduleId);
|
|
170
151
|
const options = temporalScheduleOptions(task, settings.temporalTaskQueue);
|