@opengeni/api-router 0.23.1 → 0.26.1

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 (107) hide show
  1. package/dist/app.d.ts +2 -2
  2. package/dist/app.js +1 -1
  3. package/dist/browser-controller-authority.d.ts +43 -0
  4. package/dist/browser-state-authority.d.ts +27 -0
  5. package/dist/{chunk-T4T2PGU4.js → chunk-JIKNR5YL.js} +26181 -14408
  6. package/dist/chunk-JIKNR5YL.js.map +1 -0
  7. package/dist/codemode.d.ts +23 -0
  8. package/dist/editable-artifact-live-hints.d.ts +11 -0
  9. package/dist/editable-artifact-native-kernel.d.ts +37 -0
  10. package/dist/editable-artifact-office-import.d.ts +22 -0
  11. package/dist/editable-artifact-production.d.ts +29 -0
  12. package/dist/editable-artifact-websocket.d.ts +49 -0
  13. package/dist/editable-artifact-workspace-files.d.ts +23 -0
  14. package/dist/github-browser-flow.d.ts +6 -0
  15. package/dist/http/cors.d.ts +1 -0
  16. package/dist/index.d.ts +3 -2
  17. package/dist/index.js +1796 -22
  18. package/dist/index.js.map +1 -1
  19. package/dist/integrations/api-integrations.d.ts +24 -0
  20. package/dist/integrations/atlassian.d.ts +176 -0
  21. package/dist/integrations/github-skill-source.d.ts +5 -0
  22. package/dist/integrations/google-drive.d.ts +85 -0
  23. package/dist/integrations/oauth-client.d.ts +22 -1
  24. package/dist/integrations/provider-oauth.d.ts +19 -0
  25. package/dist/integrations/slack-bot.d.ts +4 -0
  26. package/dist/integrations/slack-interactions.d.ts +8 -2
  27. package/dist/integrations/social-api.d.ts +2 -1
  28. package/dist/mcp/editable-artifact-query-schema.d.ts +4 -0
  29. package/dist/mcp/editable-artifacts.d.ts +13 -0
  30. package/dist/mcp/scheduled-task-view.d.ts +168 -0
  31. package/dist/mcp/server.d.ts +14 -3
  32. package/dist/memory-slack-delivery.d.ts +9 -0
  33. package/dist/routes/api-integrations.d.ts +8 -0
  34. package/dist/routes/browser-identities.d.ts +5 -0
  35. package/dist/routes/browser-sessions.d.ts +6 -0
  36. package/dist/routes/company-profile.d.ts +3 -0
  37. package/dist/routes/computer-sessions.d.ts +6 -0
  38. package/dist/routes/editable-artifacts.d.ts +44 -0
  39. package/dist/routes/integration-features.d.ts +3 -0
  40. package/dist/routes/memory-slack-publications.d.ts +6 -0
  41. package/dist/routes/plugins.d.ts +8 -0
  42. package/dist/routes/sessions.d.ts +17 -2
  43. package/dist/routes/skills.d.ts +6 -0
  44. package/dist/routes/video-generation.d.ts +3 -0
  45. package/dist/sandbox/channel-a.d.ts +55 -2
  46. package/dist/sandbox/metrics-ingestion.d.ts +6 -1
  47. package/dist/sandbox/viewer.d.ts +4 -2
  48. package/dist/temporal-schedule-cleanup.d.ts +26 -0
  49. package/package.json +19 -14
  50. package/src/app.ts +274 -37
  51. package/src/browser-controller-authority.ts +137 -0
  52. package/src/browser-state-authority.ts +236 -0
  53. package/src/codemode.ts +186 -0
  54. package/src/editable-artifact-live-hints.ts +64 -0
  55. package/src/editable-artifact-native-kernel.ts +659 -0
  56. package/src/editable-artifact-office-import.ts +230 -0
  57. package/src/editable-artifact-production.ts +419 -0
  58. package/src/editable-artifact-websocket.ts +311 -0
  59. package/src/editable-artifact-workspace-files.ts +186 -0
  60. package/src/github-browser-flow.ts +35 -6
  61. package/src/http/auth.ts +2 -0
  62. package/src/http/cors.ts +3 -0
  63. package/src/index.ts +119 -20
  64. package/src/integrations/api-integrations.ts +350 -0
  65. package/src/integrations/atlassian.ts +1621 -0
  66. package/src/integrations/github-skill-source.ts +142 -0
  67. package/src/integrations/google-drive.ts +1000 -64
  68. package/src/integrations/oauth-client.ts +123 -33
  69. package/src/integrations/provider-oauth.ts +777 -0
  70. package/src/integrations/slack-bot.ts +31 -2
  71. package/src/integrations/slack-interactions.ts +487 -27
  72. package/src/integrations/social-api.ts +11 -0
  73. package/src/mcp/documents.ts +32 -1
  74. package/src/mcp/editable-artifact-query-schema.ts +236 -0
  75. package/src/mcp/editable-artifacts.ts +448 -0
  76. package/src/mcp/scheduled-task-view.ts +34 -0
  77. package/src/mcp/server.ts +923 -145
  78. package/src/memory-slack-delivery.ts +209 -0
  79. package/src/routes/api-integrations.ts +407 -0
  80. package/src/routes/browser-identities.ts +136 -0
  81. package/src/routes/browser-sessions.ts +2543 -0
  82. package/src/routes/company-profile.ts +255 -0
  83. package/src/routes/computer-sessions.ts +1247 -0
  84. package/src/routes/connections.ts +284 -99
  85. package/src/routes/documents.ts +22 -3
  86. package/src/routes/editable-artifacts.ts +1159 -0
  87. package/src/routes/files.ts +106 -4
  88. package/src/routes/github.ts +18 -2
  89. package/src/routes/install.ts +7 -1
  90. package/src/routes/integration-features.ts +258 -0
  91. package/src/routes/memory-slack-publications.ts +216 -0
  92. package/src/routes/packs.ts +437 -7
  93. package/src/routes/plugins.ts +751 -0
  94. package/src/routes/rigs.ts +77 -20
  95. package/src/routes/scheduled-tasks.ts +67 -40
  96. package/src/routes/sessions.ts +421 -245
  97. package/src/routes/skills.ts +174 -0
  98. package/src/routes/video-generation.ts +132 -0
  99. package/src/routes/workspaces.ts +46 -24
  100. package/src/sandbox/channel-a.ts +723 -116
  101. package/src/sandbox/metrics-ingestion.ts +121 -3
  102. package/src/sandbox/rematerialize.ts +35 -47
  103. package/src/sandbox/viewer.ts +29 -9
  104. package/src/temporal-schedule-cleanup.ts +135 -0
  105. package/dist/chunk-T4T2PGU4.js.map +0 -1
  106. package/dist/mcp/toolspace.d.ts +0 -71
  107. package/src/mcp/toolspace.ts +0 -1190
@@ -0,0 +1,174 @@
1
+ import {
2
+ InstallSkillRequest,
3
+ InstalledSkill,
4
+ PreviewSkillImportRequest,
5
+ SkillImportPreview,
6
+ SkillUninstallPreview,
7
+ UninstallSkillRequest,
8
+ UninstallSkillResult,
9
+ } from "@opengeni/contracts";
10
+ import {
11
+ portableSkillCapabilityId,
12
+ portableSkillPluginKey,
13
+ requireAccessGrant,
14
+ resolveSkillImport,
15
+ type ApiRouteDeps,
16
+ type GitHubSkillSourceClient,
17
+ } from "@opengeni/core";
18
+ import {
19
+ getPortableSkillUninstallPreview,
20
+ installPortableSkill,
21
+ PortableSkillInstallationVersionConflictError,
22
+ PortableSkillInstallationVersionRequiredError,
23
+ uninstallPortableSkill,
24
+ } from "@opengeni/db";
25
+ import { HTTPException } from "hono/http-exception";
26
+ import type { Hono } from "hono";
27
+
28
+ import { createGitHubSkillSourceClient } from "../integrations/github-skill-source";
29
+
30
+ export type SkillRouteOverrides = Readonly<{
31
+ github?: GitHubSkillSourceClient;
32
+ }>;
33
+
34
+ export function registerSkillRoutes(
35
+ app: Hono,
36
+ deps: ApiRouteDeps,
37
+ overrides: SkillRouteOverrides = {},
38
+ ): void {
39
+ const github = overrides.github ?? createGitHubSkillSourceClient(deps.settings);
40
+
41
+ app.post("/v1/workspaces/:workspaceId/skills/preview", async (c) => {
42
+ const workspaceId = c.req.param("workspaceId");
43
+ await requireAccessGrant(c, deps, workspaceId, "workspace:read");
44
+ const payload = PreviewSkillImportRequest.parse(await c.req.json());
45
+ const resolved = await resolveForRoute(payload.url, github);
46
+ const installed = await getPortableSkillUninstallPreview(
47
+ deps.db,
48
+ workspaceId,
49
+ portableSkillCapabilityId(resolved.preview),
50
+ );
51
+ return c.json(
52
+ SkillImportPreview.parse({
53
+ ...resolved.preview,
54
+ installed: installed.installed,
55
+ installationVersion: installed.installationVersion,
56
+ }),
57
+ );
58
+ });
59
+
60
+ app.post("/v1/workspaces/:workspaceId/skills/install", async (c) => {
61
+ const workspaceId = c.req.param("workspaceId");
62
+ const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
63
+ const payload = InstallSkillRequest.parse(await c.req.json());
64
+ const resolved = await resolveForRoute(payload.url, github);
65
+ if (
66
+ resolved.preview.sourceCommit !== payload.expectedSourceCommit ||
67
+ resolved.preview.contentSha256 !== payload.expectedContentSha256
68
+ ) {
69
+ throw new HTTPException(409, {
70
+ message:
71
+ "The Skill source changed after preview. Review the new commit and contents before installing.",
72
+ });
73
+ }
74
+ const fileSummaryByPath = new Map(
75
+ resolved.preview.files.map((file) => [file.path, file] as const),
76
+ );
77
+ try {
78
+ const installed = await installPortableSkill(deps.db, {
79
+ accountId: grant.accountId,
80
+ workspaceId,
81
+ subjectId: grant.subjectId,
82
+ capabilityId: portableSkillCapabilityId(resolved.preview),
83
+ pluginKey: portableSkillPluginKey(resolved.preview),
84
+ source: resolved.preview.source,
85
+ sourceUrl: resolved.preview.sourceUrl,
86
+ repositoryUrl: resolved.preview.repositoryUrl,
87
+ sourceCommit: resolved.preview.sourceCommit,
88
+ sourcePath: resolved.preview.sourcePath,
89
+ name: resolved.preview.name,
90
+ description: resolved.preview.description,
91
+ contentSha256: resolved.preview.contentSha256,
92
+ totalBytes: resolved.preview.totalBytes,
93
+ ...(payload.expectedInstallationVersion !== undefined
94
+ ? { expectedInstallationVersion: payload.expectedInstallationVersion }
95
+ : {}),
96
+ files: resolved.files.map((file) => {
97
+ const summary = fileSummaryByPath.get(file.path);
98
+ if (!summary) throw new Error(`Skill preview omitted ${file.path}`);
99
+ return {
100
+ path: file.path,
101
+ content: file.content,
102
+ byteSize: summary.byteSize,
103
+ contentSha256: summary.contentSha256,
104
+ };
105
+ }),
106
+ });
107
+ return c.json(
108
+ InstalledSkill.parse({ ...installed, status: "installed" }),
109
+ payload.expectedInstallationVersion === undefined ? 201 : 200,
110
+ );
111
+ } catch (error) {
112
+ if (error instanceof PortableSkillInstallationVersionRequiredError) {
113
+ throw new HTTPException(400, { message: error.message });
114
+ }
115
+ if (error instanceof PortableSkillInstallationVersionConflictError) {
116
+ throw new HTTPException(409, {
117
+ message: "The Skill changed after preview. Review the current installation again.",
118
+ });
119
+ }
120
+ throw error;
121
+ }
122
+ });
123
+
124
+ app.get("/v1/workspaces/:workspaceId/skills/:capabilityId/uninstall-preview", async (c) => {
125
+ const workspaceId = c.req.param("workspaceId");
126
+ await requireAccessGrant(c, deps, workspaceId, "workspace:read");
127
+ const capabilityId = decodeURIComponent(c.req.param("capabilityId"));
128
+ return c.json(
129
+ SkillUninstallPreview.parse(
130
+ await getPortableSkillUninstallPreview(deps.db, workspaceId, capabilityId),
131
+ ),
132
+ );
133
+ });
134
+
135
+ app.delete("/v1/workspaces/:workspaceId/skills/:capabilityId", async (c) => {
136
+ const workspaceId = c.req.param("workspaceId");
137
+ const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
138
+ const capabilityId = decodeURIComponent(c.req.param("capabilityId"));
139
+ const payload = UninstallSkillRequest.parse(await c.req.json());
140
+ try {
141
+ return c.json(
142
+ UninstallSkillResult.parse(
143
+ await uninstallPortableSkill(deps.db, {
144
+ accountId: grant.accountId,
145
+ workspaceId,
146
+ capabilityId,
147
+ expectedInstallationVersion: payload.expectedInstallationVersion,
148
+ }),
149
+ ),
150
+ );
151
+ } catch (error) {
152
+ if (error instanceof PortableSkillInstallationVersionConflictError) {
153
+ throw new HTTPException(409, {
154
+ message: "The Skill changed after preview. Review uninstall impact again.",
155
+ });
156
+ }
157
+ throw error;
158
+ }
159
+ });
160
+ }
161
+
162
+ async function resolveForRoute(
163
+ url: string,
164
+ github: Parameters<typeof resolveSkillImport>[1],
165
+ ): ReturnType<typeof resolveSkillImport> {
166
+ try {
167
+ return await resolveSkillImport(url, github);
168
+ } catch (error) {
169
+ if (error instanceof HTTPException) throw error;
170
+ throw new HTTPException(502, {
171
+ message: error instanceof Error ? error.message : "The Skill source could not be read",
172
+ });
173
+ }
174
+ }
@@ -0,0 +1,132 @@
1
+ import {
2
+ UpdateVideoGenerationPolicyRequest,
3
+ VideoGenerationOperationSummary,
4
+ VideoGenerationPolicy,
5
+ WorkspaceVideoGenerationSettings,
6
+ } from "@opengeni/contracts";
7
+ import {
8
+ getVideoGenerationOperationSummary,
9
+ getWorkspaceVercelAiGatewayConnectionMetadata,
10
+ getWorkspaceVideoGenerationPolicy,
11
+ updateWorkspaceVideoGenerationPolicy,
12
+ VideoGenerationConflictError,
13
+ } from "@opengeni/db";
14
+ import {
15
+ requireAccessGrant,
16
+ VIDEO_GENERATION_MODEL_CATALOG,
17
+ videoGenerationCapabilitiesForPolicy,
18
+ type ApiRouteDeps,
19
+ } from "@opengeni/core";
20
+ import type { Hono } from "hono";
21
+ import { HTTPException } from "hono/http-exception";
22
+
23
+ export function registerVideoGenerationRoutes(app: Hono, deps: ApiRouteDeps): void {
24
+ app.get("/v1/workspaces/:workspaceId/video-generation", async (c) => {
25
+ const workspaceId = c.req.param("workspaceId");
26
+ await requireAccessGrant(c, deps, workspaceId, "workspace:read");
27
+ const [policy, connection] = await Promise.all([
28
+ getWorkspaceVideoGenerationPolicy(deps.db, workspaceId),
29
+ getWorkspaceVercelAiGatewayConnectionMetadata(deps.db, workspaceId),
30
+ ]);
31
+ const fundingOptions = videoGenerationFundingOptions({
32
+ managedConfigured: managedVideoGenerationConfigured(deps),
33
+ workspaceGatewayConfigured: connection !== null,
34
+ });
35
+ const selectedFunding = fundingOptions.find((option) => option.source === policy.fundingSource);
36
+ const capabilities =
37
+ selectedFunding?.available && policy.defaultModelId && policy.enabledModelIds.length > 0
38
+ ? videoGenerationCapabilitiesForPolicy({
39
+ policy,
40
+ credentialVersion:
41
+ policy.fundingSource === "workspace_gateway" ? (connection?.version ?? 0) : 1,
42
+ })
43
+ : null;
44
+ return c.json(
45
+ WorkspaceVideoGenerationSettings.parse({
46
+ schemaVersion: 1,
47
+ policy,
48
+ fundingOptions,
49
+ availableModels: VIDEO_GENERATION_MODEL_CATALOG,
50
+ capabilities,
51
+ }),
52
+ );
53
+ });
54
+
55
+ app.put("/v1/workspaces/:workspaceId/video-generation/policy", async (c) => {
56
+ const workspaceId = c.req.param("workspaceId");
57
+ const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
58
+ const payload = UpdateVideoGenerationPolicyRequest.parse(await c.req.json());
59
+ const connection = await getWorkspaceVercelAiGatewayConnectionMetadata(deps.db, workspaceId);
60
+ const fundingOptions = videoGenerationFundingOptions({
61
+ managedConfigured: managedVideoGenerationConfigured(deps),
62
+ workspaceGatewayConfigured: connection !== null,
63
+ });
64
+ const selectedFunding = fundingOptions.find(
65
+ (option) => option.source === payload.fundingSource,
66
+ );
67
+ if (payload.enabledModelIds.length > 0 && !selectedFunding?.available) {
68
+ throw new HTTPException(422, {
69
+ message: selectedFunding?.unavailableReason ?? "Video generation funding is unavailable",
70
+ });
71
+ }
72
+ try {
73
+ const policy = await updateWorkspaceVideoGenerationPolicy(deps.db, {
74
+ accountId: grant.accountId,
75
+ workspaceId,
76
+ subjectId: grant.subjectId,
77
+ ...payload,
78
+ });
79
+ return c.json(VideoGenerationPolicy.parse(policy));
80
+ } catch (error) {
81
+ if (error instanceof VideoGenerationConflictError) {
82
+ throw new HTTPException(409, { message: error.message });
83
+ }
84
+ if (error instanceof Error && error.message.startsWith("Unknown video generation model:")) {
85
+ throw new HTTPException(422, { message: error.message });
86
+ }
87
+ throw error;
88
+ }
89
+ });
90
+
91
+ app.get("/v1/workspaces/:workspaceId/video-generation/operations/:operationId", async (c) => {
92
+ const workspaceId = c.req.param("workspaceId");
93
+ await requireAccessGrant(c, deps, workspaceId, "workspace:read");
94
+ const summary = await getVideoGenerationOperationSummary(
95
+ deps.db,
96
+ workspaceId,
97
+ c.req.param("operationId"),
98
+ );
99
+ if (!summary) throw new HTTPException(404, { message: "video generation operation not found" });
100
+ return c.json(VideoGenerationOperationSummary.parse(summary));
101
+ });
102
+ }
103
+
104
+ function managedVideoGenerationConfigured(deps: ApiRouteDeps): boolean {
105
+ return Boolean(deps.settings.vercelAiGatewayApiKey && deps.settings.environmentsEncryptionKey);
106
+ }
107
+
108
+ function videoGenerationFundingOptions(input: {
109
+ managedConfigured: boolean;
110
+ workspaceGatewayConfigured: boolean;
111
+ }) {
112
+ return [
113
+ {
114
+ source: "opengeni_credits" as const,
115
+ label: "OpenGeni",
116
+ description: "Uses OpenGeni credits through the managed Gateway route.",
117
+ available: input.managedConfigured,
118
+ unavailableReason: input.managedConfigured
119
+ ? null
120
+ : "OpenGeni-managed video generation is not configured.",
121
+ },
122
+ {
123
+ source: "workspace_gateway" as const,
124
+ label: "Your Gateway",
125
+ description: "Uses your workspace Vercel AI Gateway key.",
126
+ available: input.workspaceGatewayConfigured,
127
+ unavailableReason: input.workspaceGatewayConfigured
128
+ ? null
129
+ : "Connect a workspace Vercel AI Gateway key first.",
130
+ },
131
+ ];
132
+ }
@@ -19,14 +19,11 @@ import {
19
19
  } from "@opengeni/contracts";
20
20
  import {
21
21
  allWorkspacePermissions,
22
- countActiveSessionsForWorkspace,
23
- countWorkspacesForAccount,
24
22
  createWorkspace,
25
- deleteWorkspace,
23
+ deleteWorkspaceIfQuiescent,
26
24
  getManagedUserByEmail,
27
25
  getWorkspaceModelPolicy,
28
26
  grantWorkspaceAccess,
29
- listScheduledTasks,
30
27
  listWorkspaceMembers,
31
28
  listWorkspaceControlEvents,
32
29
  listWorkspacesForSubject,
@@ -47,7 +44,6 @@ import { hasPermission, requireAccessContext, requireAccessGrant } from "@openge
47
44
  import { requireLimit } from "@opengeni/core";
48
45
  import type { ApiRouteDeps } from "@opengeni/core";
49
46
  import {
50
- assertWorkspaceDeletable,
51
47
  assertWorkspaceMemberRemovable,
52
48
  controlHumanWorkspace,
53
49
  resolveMemberSubjectId,
@@ -55,6 +51,7 @@ import {
55
51
  import { boundedLimit } from "../http/common";
56
52
  import { sseWorkspaceControlStream } from "../http/sse";
57
53
  import { buildWorkspaceModelCatalog } from "../model-catalog";
54
+ import { processTemporalScheduleCleanupClaims } from "../temporal-schedule-cleanup";
58
55
  import {
59
56
  AI_GATEWAY_REALTIME_MODELS,
60
57
  CODEX_REALTIME_MODEL_ID,
@@ -330,26 +327,51 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
330
327
  app.delete("/v1/workspaces/:workspaceId", async (c) => {
331
328
  const workspaceId = c.req.param("workspaceId");
332
329
  const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
333
- // Refuse before any external/DB mutation: never delete the account's only
334
- // workspace, and never delete while a session could still be running in
335
- // Temporal (there is no clean per-session terminate to call, so deleting
336
- // the row would orphan the workflow the operator must stop them first).
337
- const [workspaceCountForAccount, activeSessionCount] = await Promise.all([
338
- countWorkspacesForAccount(deps.db, grant.accountId),
339
- countActiveSessionsForWorkspace(deps.db, workspaceId),
340
- ]);
341
- assertWorkspaceDeletable({ workspaceCountForAccount, activeSessionCount });
342
- // Clean external Temporal state the FK cascade can't reach: every scheduled
343
- // task's schedule (best-effort, mirroring the scheduled-task delete path).
344
- const tasks = await listScheduledTasks(deps.db, workspaceId, 1000);
345
- await Promise.all(
346
- tasks.map((task) =>
347
- deps.workflowClient
348
- .deleteScheduledTaskSchedule({ temporalScheduleId: task.temporalScheduleId })
349
- .catch(() => undefined),
350
- ),
330
+ // The DB transaction locks the account/workspace and every existing
331
+ // session/lease before checking runtime quiescence, then returns the exact
332
+ // external schedules removed by the cascade. A racing cold->warm transition
333
+ // can therefore never erase the only provider/capture ownership receipt.
334
+ const deleted = await deleteWorkspaceIfQuiescent(deps.db, {
335
+ accountId: grant.accountId,
336
+ workspaceId,
337
+ });
338
+ if (deleted.status === "not_found") {
339
+ throw new HTTPException(404, { message: "workspace not found" });
340
+ }
341
+ if (deleted.status === "only_workspace") {
342
+ throw new HTTPException(409, { message: "cannot delete the account's only workspace" });
343
+ }
344
+ if (deleted.status === "active_sessions") {
345
+ throw new HTTPException(409, {
346
+ message: "stop the workspace's running sessions before deleting it",
347
+ });
348
+ }
349
+ if (deleted.status === "active_video_generations") {
350
+ throw new HTTPException(409, {
351
+ message: "wait for the workspace's active video generations to finish before deleting it",
352
+ });
353
+ }
354
+ if (deleted.status === "live_sandboxes") {
355
+ throw new HTTPException(409, {
356
+ message: "wait for the workspace's active sandboxes to finish draining before deleting it",
357
+ });
358
+ }
359
+ if (deleted.status !== "deleted") {
360
+ throw new Error(`Unhandled workspace deletion outcome: ${deleted.status}`);
361
+ }
362
+ // The cleanup claims were inserted in the same transaction as the cascade.
363
+ // Try them immediately; failures are released to the replica-safe outbox
364
+ // pump, so a process crash or Temporal outage cannot orphan the schedules.
365
+ await processTemporalScheduleCleanupClaims(
366
+ {
367
+ db: deps.db,
368
+ deleteSchedule: async (temporalScheduleId) => {
369
+ await deps.workflowClient.deleteScheduledTaskSchedule({ temporalScheduleId });
370
+ },
371
+ ...(deps.observability ? { observability: deps.observability } : {}),
372
+ },
373
+ deleted.temporalScheduleCleanups,
351
374
  );
352
- await deleteWorkspace(deps.db, workspaceId);
353
375
  return c.body(null, 204);
354
376
  });
355
377