@opengeni/api-router 0.5.4 → 0.5.6

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/src/app.ts CHANGED
@@ -3,7 +3,13 @@ import {
3
3
  configuredAllowedReasoningEfforts,
4
4
  configuredModels,
5
5
  } from "@opengeni/config";
6
- import { ClientConfig, resolveWorkspaceMemoryEnabled, type AccessGrant } from "@opengeni/contracts";
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, { message: "object storage is not configured" });
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({ method: c.req.method, route, status, durationSeconds });
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({ method: c.req.method, route, status, durationSeconds });
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.json(
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({ enableJsonResponse: true });
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 { mode: "deploymentKey" as const, headerName: "x-opengeni-access-key" as const };
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
- { ok: false, error: error instanceof Error ? error.message : String(error) },
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
- { pattern: /^\/v1\/workspaces\/[^/]+\/codex$/, label: "/v1/workspaces/:workspaceId/codex" },
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
- { pattern: /^\/v1\/billing\/entitlements$/, label: "/v1/billing/entitlements" },
533
+ {
534
+ pattern: /^\/v1\/billing\/entitlements$/,
535
+ label: "/v1/billing/entitlements",
536
+ },
463
537
  { pattern: /^\/v1\/webhooks\/stripe$/, label: "/v1/webhooks/stripe" },
464
- { pattern: /^\/v1\/workspaces\/[^/]+\/mcp$/, label: "/v1/workspaces/:workspaceId/mcp" },
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
- { pattern: /^\/v1\/workspaces\/[^/]+\/sessions$/, label: "/v1/workspaces/:workspaceId/sessions" },
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\/[^/]+\/turns\/reorder$/,
488
- label: "/v1/workspaces/:workspaceId/sessions/:id/turns/reorder",
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\/[^/]+\/turns\/[^/]+$/,
492
- label: "/v1/workspaces/:workspaceId/sessions/:id/turns/:turnId",
583
+ pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/queue$/,
584
+ label: "/v1/workspaces/:workspaceId/sessions/:id/queue",
493
585
  },
494
586
  {
495
- pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/turns$/,
496
- label: "/v1/workspaces/:workspaceId/sessions/:id/turns",
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
- { pattern: /^\/v1\/workspaces\/[^/]+\/api-keys$/, label: "/v1/workspaces/:workspaceId/api-keys" },
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
- { pattern: /^\/v1\/workspaces\/[^/]+\/packs$/, label: "/v1/workspaces/:workspaceId/packs" },
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
- { pattern: /^\/v1\/integrations\/oauth\/callback$/, label: "/v1/integrations/oauth/callback" },
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
- { pattern: /^\/v1\/enrollments\/device\/start$/, label: "/v1/enrollments/device/start" },
691
- { pattern: /^\/v1\/enrollments\/device\/poll$/, label: "/v1/enrollments/device/poll" },
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
- { pattern: /^\/v1\/workspaces\/[^/]+\/machines$/, label: "/v1/workspaces/:workspaceId/machines" },
709
- { pattern: /^\/v1\/github\/app-manifest\/callback$/, label: "/v1/github/app-manifest/callback" },
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
- { pattern: /^\/v1\/github\/install\/callback$/, label: "/v1/github/install/callback" },
712
- { pattern: /^\/v1\/github\/oauth\/callback$/, label: "/v1/github/oauth/callback" },
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);