@opengeni/api-router 0.22.2 → 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.
- package/dist/app.d.ts +2 -2
- package/dist/app.js +1 -1
- package/dist/auth/managed-auth.d.ts +0 -30
- package/dist/browser-controller-authority.d.ts +43 -0
- package/dist/browser-state-authority.d.ts +27 -0
- package/dist/{chunk-HWXJW5C7.js → chunk-JIKNR5YL.js} +27993 -14546
- package/dist/chunk-JIKNR5YL.js.map +1 -0
- package/dist/codemode.d.ts +23 -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/cors.d.ts +1 -0
- package/dist/http/sse.d.ts +2 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1824 -30
- 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/github-skill-source.d.ts +5 -0
- package/dist/integrations/google-drive.d.ts +85 -0
- package/dist/integrations/oauth-client.d.ts +30 -1
- package/dist/integrations/provider-oauth.d.ts +19 -0
- package/dist/integrations/slack-bot.d.ts +4 -0
- package/dist/integrations/slack-interactions.d.ts +14 -2
- package/dist/integrations/social-api.d.ts +2 -1
- package/dist/mcp/editable-artifact-query-schema.d.ts +4 -0
- package/dist/mcp/editable-artifacts.d.ts +13 -0
- package/dist/mcp/receipts.d.ts +28 -0
- package/dist/mcp/scheduled-task-view.d.ts +518 -0
- package/dist/mcp/server.d.ts +14 -3
- package/dist/memory-slack-delivery.d.ts +9 -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/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-features.d.ts +3 -0
- package/dist/routes/memory-slack-publications.d.ts +6 -0
- package/dist/routes/plugins.d.ts +8 -0
- package/dist/routes/sessions.d.ts +17 -2
- package/dist/routes/skills.d.ts +6 -0
- package/dist/routes/video-generation.d.ts +3 -0
- package/dist/sandbox/auth-callout.d.ts +2 -0
- package/dist/sandbox/channel-a.d.ts +59 -2
- package/dist/sandbox/metrics-ingestion.d.ts +6 -1
- package/dist/sandbox/viewer.d.ts +4 -2
- package/dist/temporal-schedule-cleanup.d.ts +26 -0
- package/package.json +19 -14
- package/src/app.ts +277 -41
- package/src/auth/managed-auth.ts +0 -16
- package/src/browser-controller-authority.ts +137 -0
- package/src/browser-state-authority.ts +236 -0
- package/src/codemode.ts +186 -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 +2 -0
- package/src/http/cors.ts +3 -0
- package/src/http/sse.ts +101 -6
- package/src/index.ts +147 -23
- package/src/integrations/api-integrations.ts +350 -0
- package/src/integrations/atlassian.ts +1621 -0
- package/src/integrations/github-skill-source.ts +142 -0
- package/src/integrations/google-drive.ts +1000 -64
- package/src/integrations/oauth-client.ts +159 -89
- package/src/integrations/provider-oauth.ts +777 -0
- package/src/integrations/slack-bot.ts +31 -2
- package/src/integrations/slack-interactions.ts +610 -42
- package/src/integrations/social-api.ts +11 -0
- package/src/mcp/documents.ts +74 -26
- package/src/mcp/editable-artifact-query-schema.ts +236 -0
- package/src/mcp/editable-artifacts.ts +448 -0
- package/src/mcp/receipts.ts +95 -0
- package/src/mcp/scheduled-task-view.ts +642 -0
- package/src/mcp/server.ts +1718 -310
- package/src/memory-slack-delivery.ts +209 -0
- package/src/observability.ts +3 -3
- package/src/routes/api-integrations.ts +407 -0
- package/src/routes/api-keys.ts +7 -1
- package/src/routes/browser-identities.ts +136 -0
- package/src/routes/browser-sessions.ts +2543 -0
- package/src/routes/codex.ts +7 -4
- package/src/routes/company-profile.ts +255 -0
- package/src/routes/computer-sessions.ts +1247 -0
- package/src/routes/connections.ts +358 -102
- package/src/routes/documents.ts +22 -3
- package/src/routes/editable-artifacts.ts +1159 -0
- package/src/routes/enrollments.ts +54 -12
- package/src/routes/environments.ts +60 -11
- package/src/routes/files.ts +277 -65
- package/src/routes/github.ts +18 -2
- package/src/routes/install.ts +38 -2
- package/src/routes/integration-features.ts +258 -0
- package/src/routes/machines.ts +1 -1
- package/src/routes/memory-slack-publications.ts +216 -0
- package/src/routes/packs.ts +437 -7
- package/src/routes/plugins.ts +751 -0
- package/src/routes/rigs.ts +77 -20
- package/src/routes/scheduled-tasks.ts +94 -42
- package/src/routes/sessions.ts +475 -234
- package/src/routes/skills.ts +174 -0
- package/src/routes/transcription-recordings.ts +65 -33
- package/src/routes/video-generation.ts +132 -0
- package/src/routes/workspaces.ts +46 -24
- package/src/sandbox/auth-callout.ts +16 -4
- package/src/sandbox/channel-a.ts +809 -85
- package/src/sandbox/enrollment.ts +13 -3
- package/src/sandbox/machines.ts +1 -1
- package/src/sandbox/metrics-ingestion.ts +121 -3
- package/src/sandbox/rematerialize.ts +35 -47
- package/src/sandbox/viewer.ts +58 -29
- package/src/temporal-schedule-cleanup.ts +135 -0
- package/dist/chunk-HWXJW5C7.js.map +0 -1
- package/dist/mcp/toolspace.d.ts +0 -62
- package/src/mcp/toolspace.ts +0 -1186
package/src/http/sse.ts
CHANGED
|
@@ -12,6 +12,11 @@ const SESSION_REPLAY_PAGE_SIZE = 100;
|
|
|
12
12
|
const WORKSPACE_CONTROL_REPLAY_PAGE_SIZE = 100;
|
|
13
13
|
export const SSE_QUEUED_FRAME_MAX_COUNT = 1;
|
|
14
14
|
export const SSE_WRITE_STALL_TIMEOUT_MS = 30_000;
|
|
15
|
+
export const SSE_HEARTBEAT_INTERVAL_MS = 15_000;
|
|
16
|
+
const activeSseStreams: Record<"session" | "workspace_control", number> = {
|
|
17
|
+
session: 0,
|
|
18
|
+
workspace_control: 0,
|
|
19
|
+
};
|
|
15
20
|
|
|
16
21
|
export type SseDeliveryBoundObservation = {
|
|
17
22
|
reason: "desired_size_non_positive" | "stall_timeout" | "frame_too_large";
|
|
@@ -251,6 +256,7 @@ export async function sseSessionStream(
|
|
|
251
256
|
signal: AbortSignal,
|
|
252
257
|
options: SessionSseDeliveryOptions = {},
|
|
253
258
|
): Promise<Response> {
|
|
259
|
+
const heartbeatIntervalMs = resolveHeartbeatInterval(options.heartbeatIntervalMs);
|
|
254
260
|
if (
|
|
255
261
|
options.reauthorize &&
|
|
256
262
|
options.reauthorizeAfterMs !== undefined &&
|
|
@@ -266,13 +272,20 @@ export async function sseSessionStream(
|
|
|
266
272
|
let unsubscribe: (() => void) | null = null;
|
|
267
273
|
let delivery: LatestWinsDelivery<SessionEvent> | null = null;
|
|
268
274
|
let reauthorizationTimer: ReturnType<typeof setTimeout> | null = null;
|
|
275
|
+
let heartbeatTimer: ReturnType<typeof setTimeout> | null = null;
|
|
269
276
|
let detachAbortListener = () => {};
|
|
277
|
+
let closeMetrics = () => {};
|
|
270
278
|
const stopUpstream = () => {
|
|
279
|
+
closeMetrics();
|
|
271
280
|
detachAbortListener();
|
|
272
281
|
if (reauthorizationTimer) {
|
|
273
282
|
clearTimeout(reauthorizationTimer);
|
|
274
283
|
reauthorizationTimer = null;
|
|
275
284
|
}
|
|
285
|
+
if (heartbeatTimer) {
|
|
286
|
+
clearTimeout(heartbeatTimer);
|
|
287
|
+
heartbeatTimer = null;
|
|
288
|
+
}
|
|
276
289
|
delivery?.stop();
|
|
277
290
|
const release = unsubscribe;
|
|
278
291
|
unsubscribe = null;
|
|
@@ -284,6 +297,7 @@ export async function sseSessionStream(
|
|
|
284
297
|
onObservation: sseObservationReporter("session", options),
|
|
285
298
|
onStop: stopUpstream,
|
|
286
299
|
});
|
|
300
|
+
closeMetrics = observeSseConnection("session", after, options.observability);
|
|
287
301
|
|
|
288
302
|
const fail = (error: unknown) => {
|
|
289
303
|
channel.fail(retryableSseFailure("session event stream delivery failed", error));
|
|
@@ -299,10 +313,24 @@ export async function sseSessionStream(
|
|
|
299
313
|
}, interval);
|
|
300
314
|
};
|
|
301
315
|
scheduleReauthorization();
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
316
|
+
let writeTail = Promise.resolve();
|
|
317
|
+
const writeFrame = (frame: string): Promise<void> => {
|
|
318
|
+
const write = writeTail.then(async () => {
|
|
319
|
+
if (!(await channel.write(frame))) throw new SseStreamStoppedError();
|
|
320
|
+
});
|
|
321
|
+
writeTail = write.catch(() => {});
|
|
322
|
+
return write;
|
|
323
|
+
};
|
|
324
|
+
const scheduleHeartbeat = () => {
|
|
325
|
+
if (channel.stopped()) return;
|
|
326
|
+
heartbeatTimer = setTimeout(() => {
|
|
327
|
+
heartbeatTimer = null;
|
|
328
|
+
void writeFrame(": heartbeat\n\n")
|
|
329
|
+
.then(scheduleHeartbeat)
|
|
330
|
+
.catch((error) => {
|
|
331
|
+
if (!(error instanceof SseStreamStoppedError)) fail(error);
|
|
332
|
+
});
|
|
333
|
+
}, heartbeatIntervalMs);
|
|
306
334
|
};
|
|
307
335
|
const send = async (event: SessionEvent) => {
|
|
308
336
|
if (event.sequence <= lastSent) return;
|
|
@@ -362,6 +390,7 @@ export async function sseSessionStream(
|
|
|
362
390
|
SESSION_REPLAY_PAGE_SIZE,
|
|
363
391
|
);
|
|
364
392
|
await writeFrame(": connected\n\n");
|
|
393
|
+
scheduleHeartbeat();
|
|
365
394
|
bootstrapping = false;
|
|
366
395
|
const buffered = newestBuffered;
|
|
367
396
|
newestBuffered = null;
|
|
@@ -425,14 +454,22 @@ export async function sseWorkspaceControlStream(
|
|
|
425
454
|
signal: AbortSignal,
|
|
426
455
|
options: SseDeliveryOptions = {},
|
|
427
456
|
): Promise<Response> {
|
|
457
|
+
const heartbeatIntervalMs = resolveHeartbeatInterval(options.heartbeatIntervalMs);
|
|
428
458
|
let lastSent = after;
|
|
429
459
|
let bootstrapping = true;
|
|
430
460
|
let newestBuffered: WorkspaceControlEvent | null = null;
|
|
431
461
|
let unsubscribe: (() => void) | null = null;
|
|
432
462
|
let delivery: LatestWinsDelivery<WorkspaceControlEvent> | null = null;
|
|
463
|
+
let heartbeatTimer: ReturnType<typeof setTimeout> | null = null;
|
|
433
464
|
let detachAbortListener = () => {};
|
|
465
|
+
let closeMetrics = () => {};
|
|
434
466
|
const stopUpstream = () => {
|
|
467
|
+
closeMetrics();
|
|
435
468
|
detachAbortListener();
|
|
469
|
+
if (heartbeatTimer) {
|
|
470
|
+
clearTimeout(heartbeatTimer);
|
|
471
|
+
heartbeatTimer = null;
|
|
472
|
+
}
|
|
436
473
|
delivery?.stop();
|
|
437
474
|
const release = unsubscribe;
|
|
438
475
|
unsubscribe = null;
|
|
@@ -444,12 +481,29 @@ export async function sseWorkspaceControlStream(
|
|
|
444
481
|
onObservation: sseObservationReporter("workspace_control", options),
|
|
445
482
|
onStop: stopUpstream,
|
|
446
483
|
});
|
|
484
|
+
closeMetrics = observeSseConnection("workspace_control", after, options.observability);
|
|
447
485
|
|
|
448
486
|
const fail = (error: unknown) => {
|
|
449
487
|
channel.fail(retryableSseFailure("workspace control stream delivery failed", error));
|
|
450
488
|
};
|
|
451
|
-
|
|
452
|
-
|
|
489
|
+
let writeTail = Promise.resolve();
|
|
490
|
+
const writeFrame = (frame: string): Promise<void> => {
|
|
491
|
+
const write = writeTail.then(async () => {
|
|
492
|
+
if (!(await channel.write(frame))) throw new SseStreamStoppedError();
|
|
493
|
+
});
|
|
494
|
+
writeTail = write.catch(() => {});
|
|
495
|
+
return write;
|
|
496
|
+
};
|
|
497
|
+
const scheduleHeartbeat = () => {
|
|
498
|
+
if (channel.stopped()) return;
|
|
499
|
+
heartbeatTimer = setTimeout(() => {
|
|
500
|
+
heartbeatTimer = null;
|
|
501
|
+
void writeFrame(": heartbeat\n\n")
|
|
502
|
+
.then(scheduleHeartbeat)
|
|
503
|
+
.catch((error) => {
|
|
504
|
+
if (!(error instanceof SseStreamStoppedError)) fail(error);
|
|
505
|
+
});
|
|
506
|
+
}, heartbeatIntervalMs);
|
|
453
507
|
};
|
|
454
508
|
const send = async (event: WorkspaceControlEvent) => {
|
|
455
509
|
if (event.sequence <= lastSent) return;
|
|
@@ -505,6 +559,7 @@ export async function sseWorkspaceControlStream(
|
|
|
505
559
|
WORKSPACE_CONTROL_REPLAY_PAGE_SIZE,
|
|
506
560
|
);
|
|
507
561
|
await writeFrame(": connected\n\n");
|
|
562
|
+
scheduleHeartbeat();
|
|
508
563
|
bootstrapping = false;
|
|
509
564
|
const buffered = newestBuffered;
|
|
510
565
|
newestBuffered = null;
|
|
@@ -531,6 +586,37 @@ export async function sseWorkspaceControlStream(
|
|
|
531
586
|
});
|
|
532
587
|
}
|
|
533
588
|
|
|
589
|
+
function observeSseConnection(
|
|
590
|
+
stream: "session" | "workspace_control",
|
|
591
|
+
after: number,
|
|
592
|
+
observability: Observability | undefined,
|
|
593
|
+
): () => void {
|
|
594
|
+
activeSseStreams[stream] += 1;
|
|
595
|
+
observability?.incrementCounter({
|
|
596
|
+
name: "opengeni_sse_connections_total",
|
|
597
|
+
help: "SSE connections opened at the API boundary.",
|
|
598
|
+
labels: { stream, resume: after > 0 ? "resumed" : "fresh" },
|
|
599
|
+
});
|
|
600
|
+
observability?.setGauge?.({
|
|
601
|
+
name: "opengeni_sse_connections_active",
|
|
602
|
+
help: "Currently active SSE connections at the API boundary.",
|
|
603
|
+
labels: { stream },
|
|
604
|
+
value: activeSseStreams[stream],
|
|
605
|
+
});
|
|
606
|
+
let closed = false;
|
|
607
|
+
return () => {
|
|
608
|
+
if (closed) return;
|
|
609
|
+
closed = true;
|
|
610
|
+
activeSseStreams[stream] = Math.max(0, activeSseStreams[stream] - 1);
|
|
611
|
+
observability?.setGauge?.({
|
|
612
|
+
name: "opengeni_sse_connections_active",
|
|
613
|
+
help: "Currently active SSE connections at the API boundary.",
|
|
614
|
+
labels: { stream },
|
|
615
|
+
value: activeSseStreams[stream],
|
|
616
|
+
});
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
|
|
534
620
|
async function replayWorkspaceControlEvents(
|
|
535
621
|
loadPage: (after: number, limit: number) => Promise<WorkspaceControlEvent[]>,
|
|
536
622
|
send: (event: WorkspaceControlEvent) => Promise<void>,
|
|
@@ -561,6 +647,7 @@ class SseStreamStoppedError extends Error {}
|
|
|
561
647
|
export type SseDeliveryOptions = {
|
|
562
648
|
maxQueuedBytes?: number;
|
|
563
649
|
stallTimeoutMs?: number;
|
|
650
|
+
heartbeatIntervalMs?: number;
|
|
564
651
|
observability?: Observability | undefined;
|
|
565
652
|
onObservation?: ((observation: SseDeliveryBoundObservation) => void) | undefined;
|
|
566
653
|
};
|
|
@@ -597,3 +684,11 @@ function sseObservationReporter(
|
|
|
597
684
|
function retryableSseFailure(message: string, error: unknown): TypeError {
|
|
598
685
|
return error instanceof TypeError ? error : new TypeError(message, { cause: error });
|
|
599
686
|
}
|
|
687
|
+
|
|
688
|
+
function resolveHeartbeatInterval(value: number | undefined): number {
|
|
689
|
+
const interval = value ?? SSE_HEARTBEAT_INTERVAL_MS;
|
|
690
|
+
if (!Number.isSafeInteger(interval) || interval < 1_000 || interval > 60_000) {
|
|
691
|
+
throw new RangeError("SSE heartbeat interval must be between 1000 and 60000ms");
|
|
692
|
+
}
|
|
693
|
+
return interval;
|
|
694
|
+
}
|
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;
|
|
@@ -366,11 +450,13 @@ export async function startApi() {
|
|
|
366
450
|
{ db: dbClient.db, settings, callout, observability },
|
|
367
451
|
settings.natsUrl,
|
|
368
452
|
);
|
|
369
|
-
} catch
|
|
453
|
+
} catch {
|
|
370
454
|
// A responder start failure must not crash the API (other planes work); log
|
|
371
455
|
// loudly — selfhosted agents will fail to connect until it is up.
|
|
372
456
|
observability.error("OpenGeni NATS auth-callout responder failed to start", {
|
|
373
|
-
|
|
457
|
+
errorClass: "NatsAuthCalloutOperationError",
|
|
458
|
+
errorCode: "nats_auth_callout_start_failed",
|
|
459
|
+
origin: "api",
|
|
374
460
|
});
|
|
375
461
|
}
|
|
376
462
|
} else {
|
|
@@ -389,9 +475,12 @@ export async function startApi() {
|
|
|
389
475
|
close: async () => {
|
|
390
476
|
server.stop(true);
|
|
391
477
|
stopSlackInteractionPump?.();
|
|
478
|
+
await stopMemorySlackPublicationPump();
|
|
392
479
|
stopMetricsIngestion?.();
|
|
393
480
|
stopHelloIngestion?.();
|
|
481
|
+
await stopTemporalScheduleCleanupPump();
|
|
394
482
|
await Promise.allSettled([
|
|
483
|
+
Promise.resolve(editableArtifactComposition?.close()),
|
|
395
484
|
authCalloutResponder?.close(),
|
|
396
485
|
bus.close(),
|
|
397
486
|
workflowClient.close(),
|
|
@@ -420,9 +509,12 @@ export function shouldCreateScheduleAfterUpdateError(error: unknown): boolean {
|
|
|
420
509
|
}
|
|
421
510
|
|
|
422
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
|
+
}
|
|
423
515
|
if (schedule.type === "interval") {
|
|
424
516
|
return {
|
|
425
|
-
intervals: [
|
|
517
|
+
intervals: [temporalIntervalSpec(schedule)],
|
|
426
518
|
...(schedule.startAt ? { startAt: new Date(schedule.startAt) } : {}),
|
|
427
519
|
...(schedule.endAt ? { endAt: new Date(schedule.endAt) } : {}),
|
|
428
520
|
};
|
|
@@ -456,6 +548,29 @@ export function temporalScheduleSpec(schedule: ScheduledTaskScheduleSpec): Sched
|
|
|
456
548
|
};
|
|
457
549
|
}
|
|
458
550
|
|
|
551
|
+
function temporalIntervalSpec(
|
|
552
|
+
schedule: Extract<ScheduledTaskScheduleSpec, { type: "interval" }>,
|
|
553
|
+
): NonNullable<ScheduleSpec["intervals"]>[number] {
|
|
554
|
+
const every = `${schedule.everySeconds}s` as `${number}s`;
|
|
555
|
+
if (!schedule.startAt) {
|
|
556
|
+
return { every };
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Temporal interval schedules match Epoch + (n * every) + offset. Its
|
|
560
|
+
// top-level startAt only filters matching times before that boundary, so it
|
|
561
|
+
// does not itself anchor the cadence. Derive the phase from startAt to make
|
|
562
|
+
// the stored OpenGeni timestamp the first interval boundary rather than the
|
|
563
|
+
// next epoch-aligned match.
|
|
564
|
+
const everyMilliseconds = BigInt(schedule.everySeconds) * 1_000n;
|
|
565
|
+
const startMilliseconds = BigInt(new Date(schedule.startAt).getTime());
|
|
566
|
+
const offsetMilliseconds =
|
|
567
|
+
((startMilliseconds % everyMilliseconds) + everyMilliseconds) % everyMilliseconds;
|
|
568
|
+
return {
|
|
569
|
+
every,
|
|
570
|
+
...(offsetMilliseconds === 0n ? {} : { offset: `${offsetMilliseconds}ms` as `${number}ms` }),
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
|
|
459
574
|
function temporalMonth(monthIndex: number) {
|
|
460
575
|
return TEMPORAL_MONTHS[monthIndex]!;
|
|
461
576
|
}
|
|
@@ -483,7 +598,7 @@ function temporalScheduleOptions(task: ScheduledTask, taskQueue: string): Schedu
|
|
|
483
598
|
pauseOnFailure: false,
|
|
484
599
|
},
|
|
485
600
|
state: {
|
|
486
|
-
paused: task
|
|
601
|
+
paused: scheduledTaskEffectivelyPaused(task),
|
|
487
602
|
...(task.schedule.type === "once" ? { remainingActions: 1 } : {}),
|
|
488
603
|
},
|
|
489
604
|
memo: {
|
|
@@ -495,6 +610,15 @@ function temporalScheduleOptions(task: ScheduledTask, taskQueue: string): Schedu
|
|
|
495
610
|
};
|
|
496
611
|
}
|
|
497
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
|
+
|
|
498
622
|
function temporalScheduleUpdateOptions(options: ScheduleOptions): ScheduleUpdateOptions {
|
|
499
623
|
return {
|
|
500
624
|
spec: options.spec,
|