@opengeni/api-router 0.23.1 → 0.30.0

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 (152) hide show
  1. package/dist/app.d.ts +3 -2
  2. package/dist/app.js +5 -3
  3. package/dist/auth/canonical-human-session-admission.d.ts +27 -0
  4. package/dist/browser-auth-broker.d.ts +20 -0
  5. package/dist/browser-controller-authority.d.ts +61 -0
  6. package/dist/browser-network-route.d.ts +8 -0
  7. package/dist/browser-state-authority.d.ts +27 -0
  8. package/dist/{chunk-T4T2PGU4.js → chunk-2PQMSRCB.js} +37891 -17463
  9. package/dist/chunk-2PQMSRCB.js.map +1 -0
  10. package/dist/codemode.d.ts +23 -0
  11. package/dist/controller-data-plane.d.ts +12 -0
  12. package/dist/editable-artifact-live-hints.d.ts +11 -0
  13. package/dist/editable-artifact-native-kernel.d.ts +37 -0
  14. package/dist/editable-artifact-office-import.d.ts +22 -0
  15. package/dist/editable-artifact-production.d.ts +29 -0
  16. package/dist/editable-artifact-websocket.d.ts +49 -0
  17. package/dist/editable-artifact-workspace-files.d.ts +23 -0
  18. package/dist/github-browser-flow.d.ts +6 -0
  19. package/dist/http/auth.d.ts +1 -1
  20. package/dist/http/cors.d.ts +1 -0
  21. package/dist/http/sse.d.ts +18 -1
  22. package/dist/index.d.ts +3 -2
  23. package/dist/index.js +1796 -22
  24. package/dist/index.js.map +1 -1
  25. package/dist/integrations/api-integrations.d.ts +24 -0
  26. package/dist/integrations/atlassian.d.ts +176 -0
  27. package/dist/integrations/fiken.d.ts +233 -0
  28. package/dist/integrations/github-skill-source.d.ts +5 -0
  29. package/dist/integrations/google-drive.d.ts +85 -0
  30. package/dist/integrations/oauth-client.d.ts +22 -1
  31. package/dist/integrations/provider-oauth.d.ts +17 -0
  32. package/dist/integrations/slack-bot.d.ts +175 -1
  33. package/dist/integrations/slack-interactions.d.ts +24 -6
  34. package/dist/integrations/social-api.d.ts +2 -1
  35. package/dist/interaction-holder-heartbeat.d.ts +17 -0
  36. package/dist/interaction-metrics.d.ts +11 -0
  37. package/dist/mcp/editable-artifact-query-schema.d.ts +4 -0
  38. package/dist/mcp/editable-artifacts.d.ts +13 -0
  39. package/dist/mcp/scheduled-task-view.d.ts +168 -0
  40. package/dist/mcp/server.d.ts +16 -4
  41. package/dist/memory-slack-delivery.d.ts +9 -0
  42. package/dist/model-catalog.d.ts +1 -0
  43. package/dist/routes/api-integrations.d.ts +8 -0
  44. package/dist/routes/browser-identities.d.ts +5 -0
  45. package/dist/routes/browser-sessions.d.ts +6 -0
  46. package/dist/routes/canonical-human-identities.d.ts +3 -0
  47. package/dist/routes/channels.d.ts +3 -0
  48. package/dist/routes/company-profile.d.ts +3 -0
  49. package/dist/routes/computer-sessions.d.ts +6 -0
  50. package/dist/routes/editable-artifacts.d.ts +44 -0
  51. package/dist/routes/integration-facets.d.ts +3 -0
  52. package/dist/routes/interaction-resources.d.ts +5 -0
  53. package/dist/routes/memory-slack-publications.d.ts +6 -0
  54. package/dist/routes/organization-memberships.d.ts +3 -0
  55. package/dist/routes/plugins.d.ts +8 -0
  56. package/dist/routes/sessions.d.ts +18 -2
  57. package/dist/routes/skills.d.ts +6 -0
  58. package/dist/routes/slack-task-policy.d.ts +3 -0
  59. package/dist/routes/supergrok.d.ts +3 -0
  60. package/dist/routes/video-generation.d.ts +3 -0
  61. package/dist/sandbox/channel-a.d.ts +62 -2
  62. package/dist/sandbox/machines.d.ts +2 -2
  63. package/dist/sandbox/metrics-ingestion.d.ts +6 -1
  64. package/dist/sandbox/viewer.d.ts +7 -3
  65. package/dist/slack-reaction-files.d.ts +27 -0
  66. package/dist/temporal-schedule-cleanup.d.ts +26 -0
  67. package/dist/transcription/providers/xai-subscription.d.ts +8 -0
  68. package/dist/xai-realtime.d.ts +26 -0
  69. package/dist/xai-subscription-auth.d.ts +23 -0
  70. package/package.json +20 -14
  71. package/src/app.ts +604 -64
  72. package/src/auth/canonical-human-session-admission.ts +116 -0
  73. package/src/auth/managed-auth.ts +101 -0
  74. package/src/browser-auth-broker.ts +155 -0
  75. package/src/browser-controller-authority.ts +186 -0
  76. package/src/browser-network-route.ts +34 -0
  77. package/src/browser-state-authority.ts +236 -0
  78. package/src/codemode.ts +186 -0
  79. package/src/controller-data-plane.ts +35 -0
  80. package/src/editable-artifact-live-hints.ts +64 -0
  81. package/src/editable-artifact-native-kernel.ts +659 -0
  82. package/src/editable-artifact-office-import.ts +230 -0
  83. package/src/editable-artifact-production.ts +419 -0
  84. package/src/editable-artifact-websocket.ts +311 -0
  85. package/src/editable-artifact-workspace-files.ts +186 -0
  86. package/src/github-browser-flow.ts +35 -6
  87. package/src/http/auth.ts +29 -9
  88. package/src/http/cors.ts +3 -0
  89. package/src/http/sse.ts +229 -5
  90. package/src/index.ts +119 -20
  91. package/src/integrations/api-integrations.ts +365 -0
  92. package/src/integrations/atlassian.ts +1621 -0
  93. package/src/integrations/fiken.ts +1230 -0
  94. package/src/integrations/github-skill-source.ts +142 -0
  95. package/src/integrations/google-drive.ts +1005 -64
  96. package/src/integrations/oauth-client.ts +123 -33
  97. package/src/integrations/provider-oauth.ts +756 -0
  98. package/src/integrations/slack-bot.ts +669 -11
  99. package/src/integrations/slack-interactions.ts +2063 -93
  100. package/src/integrations/social-api.ts +11 -0
  101. package/src/interaction-holder-heartbeat.ts +95 -0
  102. package/src/interaction-metrics.ts +159 -0
  103. package/src/mcp/documents.ts +146 -6
  104. package/src/mcp/editable-artifact-query-schema.ts +236 -0
  105. package/src/mcp/editable-artifacts.ts +448 -0
  106. package/src/mcp/scheduled-task-view.ts +34 -0
  107. package/src/mcp/server.ts +1756 -355
  108. package/src/memory-slack-delivery.ts +209 -0
  109. package/src/model-catalog.ts +19 -6
  110. package/src/routes/api-integrations.ts +410 -0
  111. package/src/routes/browser-identities.ts +160 -0
  112. package/src/routes/browser-sessions.ts +4482 -0
  113. package/src/routes/canonical-human-identities.ts +156 -0
  114. package/src/routes/channels.ts +90 -0
  115. package/src/routes/company-profile.ts +255 -0
  116. package/src/routes/computer-sessions.ts +1602 -0
  117. package/src/routes/connections.ts +421 -99
  118. package/src/routes/documents.ts +22 -3
  119. package/src/routes/editable-artifacts.ts +1159 -0
  120. package/src/routes/files.ts +106 -4
  121. package/src/routes/github.ts +18 -2
  122. package/src/routes/install.ts +7 -1
  123. package/src/routes/integration-facets.ts +258 -0
  124. package/src/routes/interaction-resources.ts +595 -0
  125. package/src/routes/memory-slack-publications.ts +216 -0
  126. package/src/routes/organization-memberships.ts +53 -0
  127. package/src/routes/packs.ts +437 -7
  128. package/src/routes/plugins.ts +752 -0
  129. package/src/routes/rigs.ts +77 -20
  130. package/src/routes/scheduled-tasks.ts +67 -40
  131. package/src/routes/sessions.ts +830 -278
  132. package/src/routes/skills.ts +256 -0
  133. package/src/routes/slack-task-policy.ts +115 -0
  134. package/src/routes/supergrok.ts +717 -0
  135. package/src/routes/transcription-recordings.ts +9 -2
  136. package/src/routes/transcriptions.ts +2 -1
  137. package/src/routes/video-generation.ts +162 -0
  138. package/src/routes/workspaces.ts +68 -28
  139. package/src/sandbox/channel-a.ts +750 -116
  140. package/src/sandbox/machines.ts +5 -5
  141. package/src/sandbox/metrics-ingestion.ts +121 -3
  142. package/src/sandbox/rematerialize.ts +35 -47
  143. package/src/sandbox/viewer.ts +39 -12
  144. package/src/slack-reaction-files.ts +181 -0
  145. package/src/temporal-schedule-cleanup.ts +135 -0
  146. package/src/transcription/providers/xai-subscription.ts +110 -0
  147. package/src/transcription/service.ts +17 -2
  148. package/src/xai-realtime.ts +216 -0
  149. package/src/xai-subscription-auth.ts +132 -0
  150. package/dist/chunk-T4T2PGU4.js.map +0 -1
  151. package/dist/mcp/toolspace.d.ts +0 -71
  152. package/src/mcp/toolspace.ts +0 -1190
package/src/http/sse.ts CHANGED
@@ -1,5 +1,14 @@
1
- import type { SessionEvent, WorkspaceControlEvent } from "@opengeni/contracts";
2
- import { listSessionEvents, listWorkspaceControlEvents, type Database } from "@opengeni/db";
1
+ import {
2
+ WorkspaceInteractionRevisionEvent,
3
+ type SessionEvent,
4
+ type WorkspaceControlEvent,
5
+ } from "@opengeni/contracts";
6
+ import {
7
+ getWorkspaceInteractionRevisionState,
8
+ listSessionEvents,
9
+ listWorkspaceControlEvents,
10
+ type Database,
11
+ } from "@opengeni/db";
3
12
  import {
4
13
  formatSessionEventSse,
5
14
  formatWorkspaceControlEventSse,
@@ -13,9 +22,11 @@ const WORKSPACE_CONTROL_REPLAY_PAGE_SIZE = 100;
13
22
  export const SSE_QUEUED_FRAME_MAX_COUNT = 1;
14
23
  export const SSE_WRITE_STALL_TIMEOUT_MS = 30_000;
15
24
  export const SSE_HEARTBEAT_INTERVAL_MS = 15_000;
16
- const activeSseStreams: Record<"session" | "workspace_control", number> = {
25
+ type SseStreamKind = "session" | "workspace_control" | "workspace_interaction";
26
+ const activeSseStreams: Record<SseStreamKind, number> = {
17
27
  session: 0,
18
28
  workspace_control: 0,
29
+ workspace_interaction: 0,
19
30
  };
20
31
 
21
32
  export type SseDeliveryBoundObservation = {
@@ -586,8 +597,196 @@ export async function sseWorkspaceControlStream(
586
597
  });
587
598
  }
588
599
 
600
+ export type WorkspaceInteractionSseOptions = SseDeliveryOptions & {
601
+ pollIntervalMs?: number | undefined;
602
+ };
603
+
604
+ /**
605
+ * One HTTP connection for the two workspace-wide invalidation domains used by
606
+ * every visible OpenGeni surface. Keeping these as separate HTTP/1 streams
607
+ * consumes all six per-origin browser connections with only two windows and
608
+ * starves ordinary mutations/terminal grants. The durable cursors remain
609
+ * independent; this function only multiplexes their already-bounded SSE frames.
610
+ */
611
+ export async function sseWorkspaceLiveStream(
612
+ db: Database,
613
+ bus: EventBus,
614
+ accountId: string,
615
+ workspaceId: string,
616
+ controlAfter: number,
617
+ interactionAfter: number,
618
+ signal: AbortSignal,
619
+ options: WorkspaceInteractionSseOptions = {},
620
+ ): Promise<Response> {
621
+ const upstream = new AbortController();
622
+ const channel = createByteBoundedSseStream({
623
+ maxQueuedBytes: options.maxQueuedBytes ?? SESSION_EVENT_SSE_FRAME_MAX_BYTES,
624
+ ...(options.stallTimeoutMs === undefined ? {} : { stallTimeoutMs: options.stallTimeoutMs }),
625
+ onObservation: sseObservationReporter("workspace_interaction", options),
626
+ onStop: () => {
627
+ signal.removeEventListener("abort", abort);
628
+ upstream.abort();
629
+ },
630
+ });
631
+ const abort = () => {
632
+ upstream.abort();
633
+ channel.close();
634
+ };
635
+ if (signal.aborted) abort();
636
+ else signal.addEventListener("abort", abort, { once: true });
637
+
638
+ let writeTail = Promise.resolve(true);
639
+ const write = (frame: string): Promise<boolean> => {
640
+ const pending = writeTail.then(() => channel.write(frame));
641
+ writeTail = pending.catch(() => false);
642
+ return pending;
643
+ };
644
+ const pump = async (response: Response): Promise<void> => {
645
+ if (!response.body) throw new TypeError("workspace live upstream omitted its body");
646
+ const reader = response.body.getReader();
647
+ const decoder = new TextDecoder();
648
+ try {
649
+ for (;;) {
650
+ const next = await reader.read();
651
+ if (next.done) {
652
+ const tail = decoder.decode();
653
+ if (tail) await write(tail);
654
+ return;
655
+ }
656
+ const frame = decoder.decode(next.value, { stream: true });
657
+ if (frame && !(await write(frame))) return;
658
+ }
659
+ } finally {
660
+ await reader.cancel().catch(() => undefined);
661
+ reader.releaseLock();
662
+ }
663
+ };
664
+
665
+ void (async () => {
666
+ try {
667
+ const [control, interaction] = await Promise.all([
668
+ sseWorkspaceControlStream(db, bus, workspaceId, controlAfter, upstream.signal, options),
669
+ sseWorkspaceInteractionRevisionStream(
670
+ db,
671
+ accountId,
672
+ workspaceId,
673
+ interactionAfter,
674
+ upstream.signal,
675
+ options,
676
+ ),
677
+ ]);
678
+ const pumps = [pump(control), pump(interaction)];
679
+ let termination: unknown = new TypeError("workspace live upstream ended unexpectedly");
680
+ try {
681
+ await Promise.race(pumps);
682
+ } catch (error) {
683
+ termination = error;
684
+ } finally {
685
+ // Either durable domain ending makes the multiplexed connection stale.
686
+ // Stop its sibling immediately so the SDK reconnects both independent
687
+ // cursors instead of silently losing one domain forever.
688
+ upstream.abort();
689
+ await Promise.allSettled(pumps);
690
+ }
691
+ if (!signal.aborted && !channel.stopped()) channel.fail(termination);
692
+ } catch (error) {
693
+ if (!upstream.signal.aborted && !channel.stopped()) channel.fail(error);
694
+ }
695
+ })();
696
+
697
+ return new Response(channel.stream, {
698
+ headers: {
699
+ "Content-Type": "text/event-stream; charset=utf-8",
700
+ "Cache-Control": "no-cache, no-transform",
701
+ Connection: "keep-alive",
702
+ },
703
+ });
704
+ }
705
+
706
+ /**
707
+ * Latest-wins interaction invalidation stream. The durable truth is one
708
+ * monotonic workspace row, not an ever-growing event log. Each poll reads only
709
+ * that row; reconnect immediately projects the newest revision after `after`.
710
+ */
711
+ export async function sseWorkspaceInteractionRevisionStream(
712
+ db: Database,
713
+ accountId: string,
714
+ workspaceId: string,
715
+ after: number,
716
+ signal: AbortSignal,
717
+ options: WorkspaceInteractionSseOptions = {},
718
+ ): Promise<Response> {
719
+ const heartbeatIntervalMs = resolveHeartbeatInterval(options.heartbeatIntervalMs);
720
+ const pollIntervalMs = resolveInteractionPollInterval(options.pollIntervalMs);
721
+ let lastSent = after;
722
+ let lastWriteAt = Date.now();
723
+ let stopRequested = false;
724
+ let detachAbortListener = () => {};
725
+ let closeMetrics = () => {};
726
+ const channel = createByteBoundedSseStream({
727
+ maxQueuedBytes: options.maxQueuedBytes ?? SESSION_EVENT_SSE_FRAME_MAX_BYTES,
728
+ ...(options.stallTimeoutMs === undefined ? {} : { stallTimeoutMs: options.stallTimeoutMs }),
729
+ onObservation: sseObservationReporter("workspace_interaction", options),
730
+ onStop: () => {
731
+ stopRequested = true;
732
+ closeMetrics();
733
+ detachAbortListener();
734
+ },
735
+ });
736
+ closeMetrics = observeSseConnection("workspace_interaction", after, options.observability);
737
+
738
+ const write = async (frame: string): Promise<boolean> => {
739
+ const accepted = await channel.write(frame);
740
+ if (accepted) lastWriteAt = Date.now();
741
+ return accepted;
742
+ };
743
+
744
+ void (async () => {
745
+ try {
746
+ if (!(await write(": connected\n\n"))) return;
747
+ for (;;) {
748
+ if (stopRequested || signal.aborted || channel.stopped()) return;
749
+ const state = await getWorkspaceInteractionRevisionState(db, { accountId, workspaceId });
750
+ if (state.revision > lastSent) {
751
+ const event = WorkspaceInteractionRevisionEvent.parse({
752
+ workspaceId,
753
+ sequence: state.revision,
754
+ revision: state.revision,
755
+ type: "workspace.interaction.changed",
756
+ occurredAt: (state.updatedAt ?? new Date()).toISOString(),
757
+ });
758
+ if (!(await write(formatWorkspaceInteractionRevisionSse(event)))) return;
759
+ lastSent = event.revision;
760
+ } else if (Date.now() - lastWriteAt >= heartbeatIntervalMs) {
761
+ if (!(await write(": heartbeat\n\n"))) return;
762
+ }
763
+ await abortableDelay(pollIntervalMs, signal);
764
+ }
765
+ } catch (error) {
766
+ if (!signal.aborted && !channel.stopped()) {
767
+ channel.fail(retryableSseFailure("workspace interaction stream delivery failed", error));
768
+ }
769
+ }
770
+ })();
771
+
772
+ const abort = () => channel.close();
773
+ if (signal.aborted) abort();
774
+ else {
775
+ signal.addEventListener("abort", abort, { once: true });
776
+ detachAbortListener = () => signal.removeEventListener("abort", abort);
777
+ }
778
+
779
+ return new Response(channel.stream, {
780
+ headers: {
781
+ "Content-Type": "text/event-stream; charset=utf-8",
782
+ "Cache-Control": "no-cache, no-transform",
783
+ Connection: "keep-alive",
784
+ },
785
+ });
786
+ }
787
+
589
788
  function observeSseConnection(
590
- stream: "session" | "workspace_control",
789
+ stream: SseStreamKind,
591
790
  after: number,
592
791
  observability: Observability | undefined,
593
792
  ): () => void {
@@ -659,7 +858,7 @@ export type SessionSseDeliveryOptions = SseDeliveryOptions & {
659
858
  };
660
859
 
661
860
  function sseObservationReporter(
662
- stream: "session" | "workspace_control",
861
+ stream: SseStreamKind,
663
862
  options: SseDeliveryOptions,
664
863
  ): (observation: SseDeliveryBoundObservation) => void {
665
864
  return (observation) => {
@@ -692,3 +891,28 @@ function resolveHeartbeatInterval(value: number | undefined): number {
692
891
  }
693
892
  return interval;
694
893
  }
894
+
895
+ function resolveInteractionPollInterval(value: number | undefined): number {
896
+ const interval = value ?? 1_000;
897
+ if (!Number.isSafeInteger(interval) || interval < 100 || interval > 60_000) {
898
+ throw new RangeError("interaction revision poll interval must be between 100 and 60000ms");
899
+ }
900
+ return interval;
901
+ }
902
+
903
+ function formatWorkspaceInteractionRevisionSse(event: WorkspaceInteractionRevisionEvent): string {
904
+ return `id: ${event.sequence}\nevent: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`;
905
+ }
906
+
907
+ async function abortableDelay(delayMs: number, signal: AbortSignal): Promise<void> {
908
+ if (signal.aborted) return;
909
+ await new Promise<void>((resolve) => {
910
+ const timer = setTimeout(done, delayMs);
911
+ function done() {
912
+ clearTimeout(timer);
913
+ signal.removeEventListener("abort", done);
914
+ resolve();
915
+ }
916
+ signal.addEventListener("abort", done, { once: true });
917
+ });
918
+ }
package/src/index.ts CHANGED
@@ -21,6 +21,8 @@ import {
21
21
  } from "@opengeni/db";
22
22
  import { createNatsEventBus, type ResponderConnection } from "@opengeni/events";
23
23
  import { createObservability, logStartupDependencyRetry } from "@opengeni/observability";
24
+ import { createObjectStorage } from "@opengeni/storage";
25
+ import { isArtifactRuntimeConfigured } from "@opengeni/artifact-tool/runtime/development";
24
26
  import { SESSION_WORKFLOW_WAKE_DISPATCHER_SCHEDULE_ID } from "@opengeni/core";
25
27
  import {
26
28
  Connection,
@@ -35,6 +37,17 @@ import { observabilityEventLogger } from "./observability";
35
37
  import { startAuthCalloutResponder } from "./sandbox/auth-callout";
36
38
  import { startHelloIngestion, startMetricsIngestion } from "./sandbox/metrics-ingestion";
37
39
  import { startSlackInteractionPump } from "./integrations/slack-interactions";
40
+ import { startMemorySlackPublicationPump } from "./memory-slack-delivery";
41
+ import { startTemporalScheduleCleanupPump } from "./temporal-schedule-cleanup";
42
+ import {
43
+ EDITABLE_ARTIFACT_LIVE_WEBSOCKET_MAX_MESSAGE_BYTES,
44
+ EditableArtifactWebSocketTransport,
45
+ type EditableArtifactWebSocketConnection,
46
+ } from "./editable-artifact-websocket";
47
+ import {
48
+ createStandaloneEditableArtifactApplication,
49
+ type StandaloneEditableArtifactApplication,
50
+ } from "./editable-artifact-production";
38
51
 
39
52
  /**
40
53
  * A REJECT_DUPLICATE start collides on the deterministic workflowId when the
@@ -156,6 +169,14 @@ export async function createTemporalWorkflowClient(
156
169
  },
157
170
  syncScheduledTask: async ({ task }) => {
158
171
  const schedule = temporal.schedule.getHandle(task.temporalScheduleId);
172
+ if (task.schedule.type === "manual") {
173
+ try {
174
+ await schedule.delete();
175
+ } catch (error) {
176
+ if (!(error instanceof ScheduleNotFoundError)) throw error;
177
+ }
178
+ return;
179
+ }
159
180
  const options = temporalScheduleOptions(task, settings.temporalTaskQueue);
160
181
  try {
161
182
  await schedule.update(() => temporalScheduleUpdateOptions(options));
@@ -167,16 +188,21 @@ export async function createTemporalWorkflowClient(
167
188
  }
168
189
  },
169
190
  deleteScheduledTaskSchedule: async ({ temporalScheduleId }) => {
170
- await temporal.schedule
171
- .getHandle(temporalScheduleId)
172
- .delete()
173
- .catch(() => undefined);
191
+ try {
192
+ await temporal.withDeadline(Date.now() + 5_000, async () => {
193
+ await temporal.schedule.getHandle(temporalScheduleId).delete();
194
+ });
195
+ } catch (error) {
196
+ if (error instanceof ScheduleNotFoundError) return;
197
+ throw error;
198
+ }
174
199
  },
175
200
  triggerScheduledTask: async ({
176
201
  task,
177
202
  agentRunUsageIdempotencyKey,
178
203
  triggerWorkflowId,
179
204
  initiator,
205
+ triggerType = "manual",
180
206
  }) => {
181
207
  // Deterministic workflowId (derived from the trigger token by the
182
208
  // caller) + REJECT_DUPLICATE makes a retried manual trigger idempotent:
@@ -193,7 +219,7 @@ export async function createTemporalWorkflowClient(
193
219
  accountId: task.accountId,
194
220
  workspaceId: task.workspaceId,
195
221
  taskId: task.id,
196
- triggerType: "manual",
222
+ triggerType,
197
223
  agentRunUsageIdempotencyKey,
198
224
  initiator,
199
225
  },
@@ -213,18 +239,25 @@ export async function createTemporalWorkflowClient(
213
239
  if (!targetId) {
214
240
  throw new Error("rig verification requires changeId or versionId");
215
241
  }
216
- await temporal.workflow.start("rigVerificationWorkflow", {
217
- taskQueue: settings.temporalTaskQueue,
218
- workflowId: workflowId ?? `rig-verification-${targetId}-${crypto.randomUUID()}`,
219
- workflowIdReusePolicy: "ALLOW_DUPLICATE",
220
- args: [
221
- {
222
- workspaceId,
223
- ...(changeId ? { changeId } : {}),
224
- ...(versionId ? { versionId } : {}),
225
- },
226
- ],
227
- });
242
+ try {
243
+ await temporal.workflow.start("rigVerificationWorkflow", {
244
+ taskQueue: settings.temporalTaskQueue,
245
+ workflowId: workflowId ?? `rig-verification-${targetId}-${crypto.randomUUID()}`,
246
+ workflowIdReusePolicy: "REJECT_DUPLICATE",
247
+ args: [
248
+ {
249
+ workspaceId,
250
+ ...(changeId ? { changeId } : {}),
251
+ ...(versionId ? { versionId } : {}),
252
+ },
253
+ ],
254
+ });
255
+ } catch (error) {
256
+ // A lost start acknowledgement is indistinguishable from a retry. The
257
+ // caller-owned workflow id makes both cases the same successful start.
258
+ if (isWorkflowAlreadyStarted(error)) return;
259
+ throw error;
260
+ }
228
261
  },
229
262
  check: async () => {
230
263
  await connection.workflowService.getSystemInfo({});
@@ -312,26 +345,77 @@ export async function startApi() {
312
345
  await dbClient.close();
313
346
  throw new Error("OpenGeni API startup dependencies were not initialized");
314
347
  }
348
+ const objectStorage = createObjectStorage(settings);
349
+ let editableArtifactComposition: StandaloneEditableArtifactApplication | undefined;
350
+ if (objectStorage && isArtifactRuntimeConfigured()) {
351
+ try {
352
+ editableArtifactComposition = await createStandaloneEditableArtifactApplication({
353
+ db: dbClient.db,
354
+ bus,
355
+ objectStorage,
356
+ });
357
+ } catch (error) {
358
+ await Promise.allSettled([bus.close(), workflowClient.close(), dbClient.close()]);
359
+ throw error;
360
+ }
361
+ }
315
362
  const { app, routeDeps } = createAppComposition({
316
363
  settings,
317
364
  db: dbClient.db,
318
365
  bus,
319
366
  workflowClient: workflowClient.client,
320
367
  documentIndexer: workflowClient.documentIndexer,
368
+ objectStorage,
369
+ ...(editableArtifactComposition
370
+ ? {
371
+ editableArtifacts: editableArtifactComposition.application,
372
+ editableArtifactExports: editableArtifactComposition.durableExports,
373
+ editableArtifactAgent: editableArtifactComposition.agent,
374
+ editableArtifactOfficeImports: editableArtifactComposition.officeImports,
375
+ }
376
+ : {}),
321
377
  observability,
322
378
  readinessChecks: {
323
379
  db: runtimeDatabaseReadyCheck(dbClient.db, databasePosture),
324
380
  },
325
381
  });
326
- const server = Bun.serve({
382
+ if (!routeDeps.editableArtifacts) {
383
+ observability.warn("editable artifact engine is not composed", {
384
+ subsystem: "editable_artifacts",
385
+ behavior: "http_and_websocket_fail_closed",
386
+ });
387
+ }
388
+ const artifactWebSockets = new EditableArtifactWebSocketTransport(routeDeps.editableArtifacts);
389
+ const server = Bun.serve<EditableArtifactWebSocketConnection>({
327
390
  hostname: settings.apiHost,
328
391
  port: settings.apiPort,
329
392
  idleTimeout: 255,
330
- fetch: app.fetch,
393
+ fetch: (request, bunServer) => {
394
+ if (artifactWebSockets.handles(request)) {
395
+ return artifactWebSockets.upgrade(request, bunServer);
396
+ }
397
+ return app.fetch(request);
398
+ },
399
+ websocket: {
400
+ maxPayloadLength: EDITABLE_ARTIFACT_LIVE_WEBSOCKET_MAX_MESSAGE_BYTES,
401
+ backpressureLimit: 16 * 1024 * 1024,
402
+ closeOnBackpressureLimit: true,
403
+ open: (socket) => artifactWebSockets.websocket.open(socket),
404
+ message: (socket, message) => artifactWebSockets.websocket.message(socket, message),
405
+ close: (socket) => artifactWebSockets.websocket.close(socket),
406
+ },
331
407
  });
332
408
  const stopSlackInteractionPump = settings.slackSigningSecret
333
409
  ? startSlackInteractionPump(routeDeps)
334
410
  : undefined;
411
+ const stopMemorySlackPublicationPump = startMemorySlackPublicationPump(routeDeps);
412
+ const stopTemporalScheduleCleanupPump = startTemporalScheduleCleanupPump({
413
+ db: dbClient.db,
414
+ deleteSchedule: async (temporalScheduleId) => {
415
+ await workflowClient.client.deleteScheduledTaskSchedule({ temporalScheduleId });
416
+ },
417
+ observability,
418
+ });
335
419
  // M10 — start the metrics-ingestion consumer (agent heartbeats → DB last-sample
336
420
  // + downsampled series), gated on the selfhosted flag. A no-op when disabled.
337
421
  let stopMetricsIngestion: (() => void) | undefined;
@@ -391,9 +475,12 @@ export async function startApi() {
391
475
  close: async () => {
392
476
  server.stop(true);
393
477
  stopSlackInteractionPump?.();
478
+ await stopMemorySlackPublicationPump();
394
479
  stopMetricsIngestion?.();
395
480
  stopHelloIngestion?.();
481
+ await stopTemporalScheduleCleanupPump();
396
482
  await Promise.allSettled([
483
+ Promise.resolve(editableArtifactComposition?.close()),
397
484
  authCalloutResponder?.close(),
398
485
  bus.close(),
399
486
  workflowClient.close(),
@@ -422,6 +509,9 @@ export function shouldCreateScheduleAfterUpdateError(error: unknown): boolean {
422
509
  }
423
510
 
424
511
  export function temporalScheduleSpec(schedule: ScheduledTaskScheduleSpec): ScheduleSpec {
512
+ if (schedule.type === "manual") {
513
+ throw new Error("manual scheduled tasks do not have a Temporal Schedule spec");
514
+ }
425
515
  if (schedule.type === "interval") {
426
516
  return {
427
517
  intervals: [temporalIntervalSpec(schedule)],
@@ -508,7 +598,7 @@ function temporalScheduleOptions(task: ScheduledTask, taskQueue: string): Schedu
508
598
  pauseOnFailure: false,
509
599
  },
510
600
  state: {
511
- paused: task.status === "paused",
601
+ paused: scheduledTaskEffectivelyPaused(task),
512
602
  ...(task.schedule.type === "once" ? { remainingActions: 1 } : {}),
513
603
  },
514
604
  memo: {
@@ -520,6 +610,15 @@ function temporalScheduleOptions(task: ScheduledTask, taskQueue: string): Schedu
520
610
  };
521
611
  }
522
612
 
613
+ function scheduledTaskEffectivelyPaused(task: ScheduledTask): boolean {
614
+ if (task.status === "paused") return true;
615
+ if (task.action.kind !== "knowledge_source_sync") return false;
616
+ const control = task.metadata.knowledgeSourceSync;
617
+ if (!control || typeof control !== "object") return false;
618
+ const record = control as Record<string, unknown>;
619
+ return record.sourceEnabled === false || record.connectionPaused === true;
620
+ }
621
+
523
622
  function temporalScheduleUpdateOptions(options: ScheduleOptions): ScheduleUpdateOptions {
524
623
  return {
525
624
  spec: options.spec,