@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.
- package/dist/app.d.ts +3 -2
- package/dist/app.js +5 -3
- package/dist/auth/canonical-human-session-admission.d.ts +27 -0
- package/dist/browser-auth-broker.d.ts +20 -0
- package/dist/browser-controller-authority.d.ts +61 -0
- package/dist/browser-network-route.d.ts +8 -0
- package/dist/browser-state-authority.d.ts +27 -0
- package/dist/{chunk-T4T2PGU4.js → chunk-2PQMSRCB.js} +37891 -17463
- package/dist/chunk-2PQMSRCB.js.map +1 -0
- package/dist/codemode.d.ts +23 -0
- package/dist/controller-data-plane.d.ts +12 -0
- package/dist/editable-artifact-live-hints.d.ts +11 -0
- package/dist/editable-artifact-native-kernel.d.ts +37 -0
- package/dist/editable-artifact-office-import.d.ts +22 -0
- package/dist/editable-artifact-production.d.ts +29 -0
- package/dist/editable-artifact-websocket.d.ts +49 -0
- package/dist/editable-artifact-workspace-files.d.ts +23 -0
- package/dist/github-browser-flow.d.ts +6 -0
- package/dist/http/auth.d.ts +1 -1
- package/dist/http/cors.d.ts +1 -0
- package/dist/http/sse.d.ts +18 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1796 -22
- package/dist/index.js.map +1 -1
- package/dist/integrations/api-integrations.d.ts +24 -0
- package/dist/integrations/atlassian.d.ts +176 -0
- package/dist/integrations/fiken.d.ts +233 -0
- package/dist/integrations/github-skill-source.d.ts +5 -0
- package/dist/integrations/google-drive.d.ts +85 -0
- package/dist/integrations/oauth-client.d.ts +22 -1
- package/dist/integrations/provider-oauth.d.ts +17 -0
- package/dist/integrations/slack-bot.d.ts +175 -1
- package/dist/integrations/slack-interactions.d.ts +24 -6
- package/dist/integrations/social-api.d.ts +2 -1
- package/dist/interaction-holder-heartbeat.d.ts +17 -0
- package/dist/interaction-metrics.d.ts +11 -0
- package/dist/mcp/editable-artifact-query-schema.d.ts +4 -0
- package/dist/mcp/editable-artifacts.d.ts +13 -0
- package/dist/mcp/scheduled-task-view.d.ts +168 -0
- package/dist/mcp/server.d.ts +16 -4
- package/dist/memory-slack-delivery.d.ts +9 -0
- package/dist/model-catalog.d.ts +1 -0
- package/dist/routes/api-integrations.d.ts +8 -0
- package/dist/routes/browser-identities.d.ts +5 -0
- package/dist/routes/browser-sessions.d.ts +6 -0
- package/dist/routes/canonical-human-identities.d.ts +3 -0
- package/dist/routes/channels.d.ts +3 -0
- package/dist/routes/company-profile.d.ts +3 -0
- package/dist/routes/computer-sessions.d.ts +6 -0
- package/dist/routes/editable-artifacts.d.ts +44 -0
- package/dist/routes/integration-facets.d.ts +3 -0
- package/dist/routes/interaction-resources.d.ts +5 -0
- package/dist/routes/memory-slack-publications.d.ts +6 -0
- package/dist/routes/organization-memberships.d.ts +3 -0
- package/dist/routes/plugins.d.ts +8 -0
- package/dist/routes/sessions.d.ts +18 -2
- package/dist/routes/skills.d.ts +6 -0
- package/dist/routes/slack-task-policy.d.ts +3 -0
- package/dist/routes/supergrok.d.ts +3 -0
- package/dist/routes/video-generation.d.ts +3 -0
- package/dist/sandbox/channel-a.d.ts +62 -2
- package/dist/sandbox/machines.d.ts +2 -2
- package/dist/sandbox/metrics-ingestion.d.ts +6 -1
- package/dist/sandbox/viewer.d.ts +7 -3
- package/dist/slack-reaction-files.d.ts +27 -0
- package/dist/temporal-schedule-cleanup.d.ts +26 -0
- package/dist/transcription/providers/xai-subscription.d.ts +8 -0
- package/dist/xai-realtime.d.ts +26 -0
- package/dist/xai-subscription-auth.d.ts +23 -0
- package/package.json +20 -14
- package/src/app.ts +604 -64
- package/src/auth/canonical-human-session-admission.ts +116 -0
- package/src/auth/managed-auth.ts +101 -0
- package/src/browser-auth-broker.ts +155 -0
- package/src/browser-controller-authority.ts +186 -0
- package/src/browser-network-route.ts +34 -0
- package/src/browser-state-authority.ts +236 -0
- package/src/codemode.ts +186 -0
- package/src/controller-data-plane.ts +35 -0
- package/src/editable-artifact-live-hints.ts +64 -0
- package/src/editable-artifact-native-kernel.ts +659 -0
- package/src/editable-artifact-office-import.ts +230 -0
- package/src/editable-artifact-production.ts +419 -0
- package/src/editable-artifact-websocket.ts +311 -0
- package/src/editable-artifact-workspace-files.ts +186 -0
- package/src/github-browser-flow.ts +35 -6
- package/src/http/auth.ts +29 -9
- package/src/http/cors.ts +3 -0
- package/src/http/sse.ts +229 -5
- package/src/index.ts +119 -20
- package/src/integrations/api-integrations.ts +365 -0
- package/src/integrations/atlassian.ts +1621 -0
- package/src/integrations/fiken.ts +1230 -0
- package/src/integrations/github-skill-source.ts +142 -0
- package/src/integrations/google-drive.ts +1005 -64
- package/src/integrations/oauth-client.ts +123 -33
- package/src/integrations/provider-oauth.ts +756 -0
- package/src/integrations/slack-bot.ts +669 -11
- package/src/integrations/slack-interactions.ts +2063 -93
- package/src/integrations/social-api.ts +11 -0
- package/src/interaction-holder-heartbeat.ts +95 -0
- package/src/interaction-metrics.ts +159 -0
- package/src/mcp/documents.ts +146 -6
- package/src/mcp/editable-artifact-query-schema.ts +236 -0
- package/src/mcp/editable-artifacts.ts +448 -0
- package/src/mcp/scheduled-task-view.ts +34 -0
- package/src/mcp/server.ts +1756 -355
- package/src/memory-slack-delivery.ts +209 -0
- package/src/model-catalog.ts +19 -6
- package/src/routes/api-integrations.ts +410 -0
- package/src/routes/browser-identities.ts +160 -0
- package/src/routes/browser-sessions.ts +4482 -0
- package/src/routes/canonical-human-identities.ts +156 -0
- package/src/routes/channels.ts +90 -0
- package/src/routes/company-profile.ts +255 -0
- package/src/routes/computer-sessions.ts +1602 -0
- package/src/routes/connections.ts +421 -99
- package/src/routes/documents.ts +22 -3
- package/src/routes/editable-artifacts.ts +1159 -0
- package/src/routes/files.ts +106 -4
- package/src/routes/github.ts +18 -2
- package/src/routes/install.ts +7 -1
- package/src/routes/integration-facets.ts +258 -0
- package/src/routes/interaction-resources.ts +595 -0
- package/src/routes/memory-slack-publications.ts +216 -0
- package/src/routes/organization-memberships.ts +53 -0
- package/src/routes/packs.ts +437 -7
- package/src/routes/plugins.ts +752 -0
- package/src/routes/rigs.ts +77 -20
- package/src/routes/scheduled-tasks.ts +67 -40
- package/src/routes/sessions.ts +830 -278
- package/src/routes/skills.ts +256 -0
- package/src/routes/slack-task-policy.ts +115 -0
- package/src/routes/supergrok.ts +717 -0
- package/src/routes/transcription-recordings.ts +9 -2
- package/src/routes/transcriptions.ts +2 -1
- package/src/routes/video-generation.ts +162 -0
- package/src/routes/workspaces.ts +68 -28
- package/src/sandbox/channel-a.ts +750 -116
- package/src/sandbox/machines.ts +5 -5
- package/src/sandbox/metrics-ingestion.ts +121 -3
- package/src/sandbox/rematerialize.ts +35 -47
- package/src/sandbox/viewer.ts +39 -12
- package/src/slack-reaction-files.ts +181 -0
- package/src/temporal-schedule-cleanup.ts +135 -0
- package/src/transcription/providers/xai-subscription.ts +110 -0
- package/src/transcription/service.ts +17 -2
- package/src/xai-realtime.ts +216 -0
- package/src/xai-subscription-auth.ts +132 -0
- package/dist/chunk-T4T2PGU4.js.map +0 -1
- package/dist/mcp/toolspace.d.ts +0 -71
- package/src/mcp/toolspace.ts +0 -1190
package/src/http/sse.ts
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
171
|
-
.
|
|
172
|
-
|
|
173
|
-
|
|
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
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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,
|