@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
package/dist/index.js CHANGED
@@ -1,7 +1,10 @@
1
1
  import {
2
+ SlackBotProviderError,
2
3
  createAppComposition,
3
- startSlackInteractionPump
4
- } from "./chunk-T4T2PGU4.js";
4
+ createOpenGeniSlackBotInteractionClient,
5
+ startSlackInteractionPump,
6
+ startTemporalScheduleCleanupPump
7
+ } from "./chunk-JIKNR5YL.js";
5
8
 
6
9
  // src/index.ts
7
10
  import {
@@ -21,6 +24,8 @@ import {
21
24
  } from "@opengeni/db";
22
25
  import { createNatsEventBus } from "@opengeni/events";
23
26
  import { createObservability, logStartupDependencyRetry } from "@opengeni/observability";
27
+ import { createObjectStorage } from "@opengeni/storage";
28
+ import { isArtifactRuntimeConfigured } from "@opengeni/artifact-tool/runtime/development";
24
29
  import { SESSION_WORKFLOW_WAKE_DISPATCHER_SCHEDULE_ID } from "@opengeni/core";
25
30
  import {
26
31
  Connection,
@@ -164,19 +169,26 @@ async function startAuthCalloutResponder(deps, natsUrl) {
164
169
  // src/sandbox/metrics-ingestion.ts
165
170
  import {
166
171
  clearEnrollmentWentOffline,
172
+ disconnectAttachedBrowserDevices,
167
173
  getEnrollment as getEnrollment2,
168
174
  ingestMachineMetricsSample,
175
+ reconcileAttachedBrowserInventory,
169
176
  sessionsWithActiveOpOnEnrollment,
170
177
  setEnrollmentDisplayState,
171
178
  setEnrollmentOpStreamState,
172
179
  setEnrollmentWentOffline,
173
180
  touchEnrollmentLastSeen
174
181
  } from "@opengeni/db";
182
+ import {
183
+ AttachedBrowserInventorySnapshot as AttachedBrowserInventoryContract
184
+ } from "@opengeni/contracts";
175
185
  import { appendAndPublishEvents } from "@opengeni/events";
176
186
  import {
177
187
  AgentEvent,
188
+ Arch,
178
189
  GoingOfflineReason,
179
190
  Hello,
191
+ Os,
180
192
  goingOfflineReasonToJSON
181
193
  } from "@opengeni/agent-proto";
182
194
  var AGENT_EVENTS_SUBJECT = "agent.*.*.events";
@@ -215,6 +227,73 @@ function wireSampleToDbSample(wire) {
215
227
  sampledAt: wire.sampledAtMs && Number(wire.sampledAtMs) > 0 ? new Date(Number(wire.sampledAtMs)) : /* @__PURE__ */ new Date()
216
228
  };
217
229
  }
230
+ function wireAttachedBrowserInventoryToContract(wire) {
231
+ return AttachedBrowserInventoryContract.parse({
232
+ bridgeGeneration: wire.bridgeGeneration,
233
+ revision: safeWireInteger(wire.revision, "browser inventory revision"),
234
+ devices: wire.devices.map((device) => ({
235
+ id: device.id,
236
+ name: device.name,
237
+ profileLabel: device.profileLabel.trim() || null,
238
+ browserName: device.browserName,
239
+ browserVersion: device.browserVersion,
240
+ extensionVersion: device.extensionVersion,
241
+ platform: wireBrowserPlatform(device.platform),
242
+ architecture: wireBrowserArchitecture(device.arch),
243
+ connectionGeneration: device.connectionGeneration,
244
+ inventoryRevision: safeWireInteger(
245
+ device.inventoryRevision,
246
+ "attached browser inventory revision"
247
+ ),
248
+ tabCount: safeWireInteger(device.tabCount, "attached browser tab count"),
249
+ capabilities: device.capabilities
250
+ }))
251
+ });
252
+ }
253
+ function safeWireInteger(value, label) {
254
+ let parsed;
255
+ try {
256
+ parsed = BigInt(value);
257
+ } catch {
258
+ throw new Error(`${label} is not an integer`);
259
+ }
260
+ if (parsed < 0n || parsed > BigInt(Number.MAX_SAFE_INTEGER)) {
261
+ throw new Error(`${label} is outside the safe integer range`);
262
+ }
263
+ return Number(parsed);
264
+ }
265
+ function wireBrowserPlatform(platform) {
266
+ switch (platform) {
267
+ case Os.OS_LINUX:
268
+ return "linux";
269
+ case Os.OS_MACOS:
270
+ return "macos";
271
+ case Os.OS_WINDOWS:
272
+ return "windows";
273
+ default:
274
+ throw new Error("attached browser platform is unspecified or unsupported");
275
+ }
276
+ }
277
+ function wireBrowserArchitecture(architecture) {
278
+ switch (architecture) {
279
+ case Arch.ARCH_X86_64:
280
+ return "x64";
281
+ case Arch.ARCH_AARCH64:
282
+ return "arm64";
283
+ default:
284
+ throw new Error("attached browser architecture is unspecified or unsupported");
285
+ }
286
+ }
287
+ async function ingestAttachedBrowserInventory(db, input) {
288
+ const enrollment = await getEnrollment2(db, input.workspaceId, input.agentId);
289
+ if (!enrollment) return;
290
+ await reconcileAttachedBrowserInventory(db, {
291
+ accountId: enrollment.accountId,
292
+ workspaceId: input.workspaceId,
293
+ enrollmentId: input.agentId,
294
+ snapshot: wireAttachedBrowserInventoryToContract(input.inventory)
295
+ });
296
+ }
218
297
  async function ingestHeartbeat(db, input) {
219
298
  const enrollment = await getEnrollment2(db, input.workspaceId, input.agentId);
220
299
  if (!enrollment) {
@@ -285,6 +364,18 @@ async function handleAgentEventPayload(db, observability, payload, subject, bus)
285
364
  enrollmentId: ids.agentId,
286
365
  reason
287
366
  });
367
+ try {
368
+ await disconnectAttachedBrowserDevices(db, {
369
+ accountId: enrollment.accountId,
370
+ workspaceId: ids.workspaceId,
371
+ enrollmentId: ids.agentId
372
+ });
373
+ } catch (error) {
374
+ observability?.warn?.("Failed to disconnect attached browsers with their machine", {
375
+ subject,
376
+ error: error instanceof Error ? error.message : String(error)
377
+ });
378
+ }
288
379
  if (bus) {
289
380
  const isSelfUpdate = event.event.goingOffline.reason === GoingOfflineReason.GOING_OFFLINE_REASON_UPDATE;
290
381
  await fanOutMachineLinkEvents(
@@ -320,10 +411,23 @@ async function handleAgentEventPayload(db, observability, payload, subject, bus)
320
411
  if (event.event?.$case !== "heartbeat") {
321
412
  return;
322
413
  }
323
- const metrics = event.event.heartbeat.metrics;
324
- if (!metrics) {
325
- return;
414
+ const heartbeat = event.event.heartbeat;
415
+ if (heartbeat.attachedBrowserInventory) {
416
+ try {
417
+ await ingestAttachedBrowserInventory(db, {
418
+ workspaceId: ids.workspaceId,
419
+ agentId: ids.agentId,
420
+ inventory: heartbeat.attachedBrowserInventory
421
+ });
422
+ } catch (error) {
423
+ observability?.warn?.("Failed to ingest an attached-browser inventory", {
424
+ subject,
425
+ error: error instanceof Error ? error.message : String(error)
426
+ });
427
+ }
326
428
  }
429
+ const metrics = heartbeat.metrics;
430
+ if (!metrics) return;
327
431
  try {
328
432
  await ingestHeartbeat(db, {
329
433
  workspaceId: ids.workspaceId,
@@ -458,6 +562,1592 @@ function startHelloIngestion(deps) {
458
562
  );
459
563
  }
460
564
 
565
+ // src/memory-slack-delivery.ts
566
+ import {
567
+ claimMemorySlackPublication,
568
+ completeMemorySlackPublication,
569
+ currentMemorySlackConfigurationMatches,
570
+ deferMemorySlackPublication,
571
+ failMemorySlackPublication
572
+ } from "@opengeni/db";
573
+ import { sanitizeSlackPublicationText } from "@opengeni/core";
574
+ var DELIVERY_LEASE_MS = 3e4;
575
+ var MAX_DELIVERY_ATTEMPTS = 8;
576
+ var MAX_DELIVERY_RETRY_MS = 5 * 6e4;
577
+ var MAX_SLACK_MESSAGE_CHARS = 3500;
578
+ async function drainMemorySlackPublicationsOnce(deps) {
579
+ const claimHolderId = crypto.randomUUID();
580
+ const publication = await claimMemorySlackPublication(deps.db, claimHolderId, DELIVERY_LEASE_MS);
581
+ if (!publication) return false;
582
+ try {
583
+ if (!await currentMemorySlackConfigurationMatches(deps.db, publication)) {
584
+ await failMemorySlackPublication(deps.db, {
585
+ publication,
586
+ claimHolderId,
587
+ errorCode: "configuration_changed",
588
+ cancelled: true
589
+ });
590
+ return true;
591
+ }
592
+ const client = await createOpenGeniSlackBotInteractionClient(deps, {
593
+ accountId: publication.accountId,
594
+ workspaceId: publication.workspaceId,
595
+ connectionId: publication.connectionId,
596
+ subjectId: publication.initiatorSubjectId,
597
+ sessionId: publication.sessionId
598
+ });
599
+ const result = await client.postMessage({
600
+ operationId: publication.operationId,
601
+ channelId: publication.slackChannelId,
602
+ text: formatMemorySlackPublicationMessage(deps, publication),
603
+ requireActiveNonSharedChannel: true
604
+ });
605
+ const completed = await completeMemorySlackPublication(deps.db, {
606
+ publication,
607
+ claimHolderId,
608
+ slackChannelId: result.channelId,
609
+ slackMessageTimestamp: result.timestamp
610
+ });
611
+ if (!completed) throw new Error("Memory Slack publication lost its durable claim");
612
+ } catch (error) {
613
+ const errorCode = deliveryErrorCode(error);
614
+ if (memorySlackDestinationDrift(error)) {
615
+ await failMemorySlackPublication(deps.db, {
616
+ publication,
617
+ claimHolderId,
618
+ errorCode,
619
+ cancelled: true
620
+ }).catch(() => void 0);
621
+ } else if (publication.attemptCount >= MAX_DELIVERY_ATTEMPTS || permanentDeliveryError(error)) {
622
+ await failMemorySlackPublication(deps.db, {
623
+ publication,
624
+ claimHolderId,
625
+ errorCode
626
+ }).catch(() => void 0);
627
+ } else {
628
+ await deferMemorySlackPublication(deps.db, {
629
+ publication,
630
+ claimHolderId,
631
+ errorCode,
632
+ retryAt: new Date(Date.now() + deliveryRetryMs(error, publication.attemptCount))
633
+ }).catch(() => void 0);
634
+ }
635
+ }
636
+ return true;
637
+ }
638
+ function startMemorySlackPublicationPump(deps, options = {}) {
639
+ let stopped = false;
640
+ let running;
641
+ const intervalMs = Math.max(250, options.intervalMs ?? 1e3);
642
+ const maxPerTick = Math.max(1, Math.min(50, options.maxPerTick ?? 10));
643
+ const tick = () => {
644
+ if (stopped || running) return;
645
+ running = (async () => {
646
+ for (let index = 0; index < maxPerTick; index += 1) {
647
+ if (!await drainMemorySlackPublicationsOnce(deps)) break;
648
+ }
649
+ })().catch((error) => {
650
+ deps.observability?.error("Memory Slack publication claim failed", {
651
+ errorClass: error instanceof Error ? error.name : "MemorySlackPublicationPumpError",
652
+ errorCode: deliveryErrorCode(error),
653
+ origin: "api"
654
+ });
655
+ }).finally(() => {
656
+ running = void 0;
657
+ });
658
+ };
659
+ const timer = setInterval(tick, intervalMs);
660
+ queueMicrotask(tick);
661
+ return async () => {
662
+ stopped = true;
663
+ clearInterval(timer);
664
+ await running;
665
+ };
666
+ }
667
+ function formatMemorySlackPublicationMessage(deps, publication) {
668
+ const projection = publication.projection;
669
+ const summary = boundedProjectionString(projection["summary"], 512) || "Important change";
670
+ const owner = boundedProjectionString(projection["ownerLabel"], 96);
671
+ const occurredAt = boundedProjectionString(projection["occurredAt"], 64);
672
+ const changeKind = boundedProjectionString(projection["changeKind"], 32);
673
+ const destination = boundedProjectionString(projection["destination"], 64);
674
+ const outcome = boundedProjectionString(projection["outcome"], 64);
675
+ const heading = publication.sourceType === "workspace_memory" ? `Workspace Memory ${changeKind || "change"}` : `Governed learning ${outcome || "change"}`;
676
+ const details = [
677
+ `Importance: ${publication.importance}`,
678
+ ...destination ? [`Surface: ${destination}`] : [],
679
+ ...owner ? [`Owner: ${owner}`] : [],
680
+ ...occurredAt ? [`Recorded: ${occurredAt}`] : []
681
+ ];
682
+ const path = publication.sourceType === "workspace_memory" ? `/workspaces/${publication.workspaceId}/memory?memoryId=${encodeURIComponent(publication.sourceId)}` : `/workspaces/${publication.workspaceId}/workspace-state`;
683
+ const base = deps.settings.webBaseUrl ?? deps.settings.publicBaseUrl;
684
+ const link = base ? new URL(path, base).toString() : null;
685
+ const text = `*${heading}*
686
+ ${summary}
687
+
688
+ ${details.join(" \xB7 ")}${link ? `
689
+ ${link}` : ""}`;
690
+ return text.length <= MAX_SLACK_MESSAGE_CHARS ? text : `${text.slice(0, MAX_SLACK_MESSAGE_CHARS - 20)}
691
+ \u2026 message truncated`;
692
+ }
693
+ var PERMANENT_DELIVERY_CODES = /* @__PURE__ */ new Set([
694
+ "account_inactive",
695
+ "channel_not_found",
696
+ "invalid_auth",
697
+ "is_archived",
698
+ "not_authed",
699
+ "not_in_channel",
700
+ "token_expired",
701
+ "token_revoked"
702
+ ]);
703
+ var MEMORY_SLACK_DESTINATION_DRIFT_CODES = /* @__PURE__ */ new Set([
704
+ "channel_not_found",
705
+ "is_archived",
706
+ "not_in_channel",
707
+ "slack_connect_unsupported"
708
+ ]);
709
+ function memorySlackDestinationDrift(error) {
710
+ return error instanceof SlackBotProviderError && MEMORY_SLACK_DESTINATION_DRIFT_CODES.has(error.code);
711
+ }
712
+ function permanentDeliveryError(error) {
713
+ if (!(error instanceof SlackBotProviderError)) return false;
714
+ if (PERMANENT_DELIVERY_CODES.has(error.code)) return true;
715
+ const status = /^http_(\d{3})$/.exec(error.code)?.[1];
716
+ return status ? Number(status) >= 400 && Number(status) < 500 && status !== "408" && status !== "429" : false;
717
+ }
718
+ function deliveryRetryMs(error, attemptCount) {
719
+ if (error instanceof SlackBotProviderError && error.retryAfterMs) {
720
+ return Math.min(Math.max(error.retryAfterMs, 1e3), MAX_DELIVERY_RETRY_MS);
721
+ }
722
+ return Math.min(1e3 * 2 ** Math.max(0, attemptCount - 1), MAX_DELIVERY_RETRY_MS);
723
+ }
724
+ function deliveryErrorCode(error) {
725
+ if (error instanceof SlackBotProviderError) return error.code.slice(0, 128);
726
+ const raw = error instanceof Error ? error.name : "memory_slack_delivery_error";
727
+ return raw.toLowerCase().replace(/[^a-z0-9_-]/g, "_").slice(0, 128) || "error";
728
+ }
729
+ function boundedProjectionString(value, maxChars) {
730
+ return typeof value === "string" ? escapeSlackText(sanitizeSlackPublicationText(value).slice(0, maxChars)) : "";
731
+ }
732
+ function escapeSlackText(value) {
733
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
734
+ }
735
+
736
+ // src/editable-artifact-websocket.ts
737
+ import {
738
+ EditableArtifactDomainError,
739
+ EditableArtifactLiveError,
740
+ decodeEditableArtifactLiveClientWireFrame,
741
+ encodeEditableArtifactLiveServerWireFrame
742
+ } from "@opengeni/core";
743
+ var EDITABLE_ARTIFACT_LIVE_WEBSOCKET_PATH = "/v1/editable-artifacts/live";
744
+ var EDITABLE_ARTIFACT_LIVE_WEBSOCKET_PROTOCOL = "opengeni-artifact-v1";
745
+ var EDITABLE_ARTIFACT_LIVE_WEBSOCKET_MAX_MESSAGE_BYTES = 8 * 1024 * 1024 + 64 * 1024;
746
+ var MAX_QUEUED_MESSAGES = 64;
747
+ var MAX_QUEUED_BYTES = 12 * 1024 * 1024;
748
+ var EditableArtifactWebSocketTransport = class {
749
+ constructor(application) {
750
+ this.application = application;
751
+ this.websocket = Object.freeze({
752
+ open: (socket) => socket.data.attach(socket),
753
+ message: (socket, message) => socket.data.receive(message),
754
+ close: (socket) => socket.data.transportClosed()
755
+ });
756
+ }
757
+ websocket;
758
+ handles(request) {
759
+ return new URL(request.url).pathname === EDITABLE_ARTIFACT_LIVE_WEBSOCKET_PATH;
760
+ }
761
+ upgrade(request, server) {
762
+ const url = new URL(request.url);
763
+ if (url.pathname !== EDITABLE_ARTIFACT_LIVE_WEBSOCKET_PATH) return void 0;
764
+ if (request.method !== "GET") {
765
+ return new Response("Method Not Allowed", {
766
+ status: 405,
767
+ headers: { allow: "GET" }
768
+ });
769
+ }
770
+ if (!this.application) return new Response("Service Unavailable", { status: 503 });
771
+ if (!offeredProtocol(request.headers.get("sec-websocket-protocol"))) {
772
+ return new Response("WebSocket protocol required", { status: 426 });
773
+ }
774
+ const connection = new EditableArtifactWebSocketConnection(this.application);
775
+ const upgraded = server.upgrade(request, {
776
+ data: connection,
777
+ headers: {
778
+ "sec-websocket-protocol": EDITABLE_ARTIFACT_LIVE_WEBSOCKET_PROTOCOL
779
+ }
780
+ });
781
+ return upgraded ? void 0 : new Response("Bad Request", { status: 400 });
782
+ }
783
+ };
784
+ var EditableArtifactWebSocketConnection = class {
785
+ constructor(application) {
786
+ this.application = application;
787
+ }
788
+ socket = null;
789
+ session = null;
790
+ work = Promise.resolve();
791
+ queuedMessages = 0;
792
+ queuedBytes = 0;
793
+ terminal = false;
794
+ attach(socket) {
795
+ if (this.socket || this.terminal) {
796
+ socket.close(4400, "invalid connection state");
797
+ return;
798
+ }
799
+ this.socket = socket;
800
+ }
801
+ receive(message) {
802
+ if (this.terminal) return;
803
+ if (typeof message === "string") {
804
+ void this.fail("invalid_frame");
805
+ return;
806
+ }
807
+ const byteLength = message.byteLength;
808
+ if (byteLength === 0 || byteLength > EDITABLE_ARTIFACT_LIVE_WEBSOCKET_MAX_MESSAGE_BYTES || this.queuedMessages + 1 > MAX_QUEUED_MESSAGES || this.queuedBytes + byteLength > MAX_QUEUED_BYTES) {
809
+ void this.fail(
810
+ byteLength > EDITABLE_ARTIFACT_LIVE_WEBSOCKET_MAX_MESSAGE_BYTES ? "oversized_frame" : "slow_client"
811
+ );
812
+ return;
813
+ }
814
+ const bytes = message instanceof Uint8Array ? Uint8Array.from(message) : new Uint8Array(message).slice();
815
+ this.queuedMessages += 1;
816
+ this.queuedBytes += bytes.byteLength;
817
+ const run = this.work.then(async () => {
818
+ try {
819
+ await this.process(bytes);
820
+ } finally {
821
+ this.queuedMessages -= 1;
822
+ this.queuedBytes -= bytes.byteLength;
823
+ }
824
+ });
825
+ this.work = run.catch(async (error) => {
826
+ await this.handleFailure(error);
827
+ });
828
+ }
829
+ async send(frame) {
830
+ if (this.terminal || !this.socket) {
831
+ throw new EditableArtifactLiveError("closed", "Editable artifact socket is closed");
832
+ }
833
+ const bytes = encodeEditableArtifactLiveServerWireFrame(frame);
834
+ if (bytes.byteLength > EDITABLE_ARTIFACT_LIVE_WEBSOCKET_MAX_MESSAGE_BYTES) {
835
+ throw new EditableArtifactLiveError(
836
+ "oversized_frame",
837
+ "Editable artifact server frame exceeds socket limit",
838
+ { requiresSnapshot: true }
839
+ );
840
+ }
841
+ const accepted = this.socket.send(bytes, false);
842
+ if (!Number.isSafeInteger(accepted) || accepted <= 0) {
843
+ throw new EditableArtifactLiveError("closed", "Editable artifact socket rejected frame");
844
+ }
845
+ }
846
+ bufferedBytes() {
847
+ const buffered = this.socket?.getBufferedAmount?.() ?? this.socket?.bufferedAmount ?? 0;
848
+ return Number.isSafeInteger(buffered) && buffered >= 0 ? buffered : Number.MAX_SAFE_INTEGER;
849
+ }
850
+ close(close) {
851
+ if (this.terminal) return;
852
+ this.terminal = true;
853
+ this.socket?.close(closeCode(close.reason), close.reason);
854
+ }
855
+ transportClosed() {
856
+ if (this.terminal) return;
857
+ this.terminal = true;
858
+ void this.session?.close("transport_error");
859
+ }
860
+ async process(bytes) {
861
+ if (this.terminal) return;
862
+ const frame = decodeEditableArtifactLiveClientWireFrame(bytes);
863
+ if (!this.session) {
864
+ if (frame.type !== "open") {
865
+ throw new EditableArtifactLiveError("invalid_frame", "First socket frame must be open");
866
+ }
867
+ const session = await this.application.openLive({
868
+ token: frame.token,
869
+ artifactId: frame.artifactId,
870
+ protocolVersion: frame.protocolVersion,
871
+ resume: frame.resume,
872
+ sink: this
873
+ });
874
+ if (this.terminal) {
875
+ await session.close("transport_error");
876
+ return;
877
+ }
878
+ this.session = session;
879
+ void session.closed.then(() => {
880
+ if (!this.terminal)
881
+ this.close({
882
+ reason: "closed",
883
+ retryable: false,
884
+ requiresSnapshot: false
885
+ });
886
+ });
887
+ return;
888
+ }
889
+ if (frame.type === "open") {
890
+ throw new EditableArtifactLiveError("invalid_frame", "Socket is already open");
891
+ }
892
+ if (frame.artifactId !== this.session.artifactId || frame.streamEpoch !== this.session.streamEpoch) {
893
+ throw new EditableArtifactLiveError("stale_epoch", "Socket frame belongs to another stream");
894
+ }
895
+ if (frame.type === "applied") {
896
+ await this.session.acknowledge(frame);
897
+ return;
898
+ }
899
+ let receipt;
900
+ try {
901
+ receipt = await this.session.submitIntent(frame);
902
+ } catch (error) {
903
+ await this.send({
904
+ type: "mutationRejected",
905
+ protocolVersion: frame.protocolVersion,
906
+ artifactId: frame.artifactId,
907
+ streamEpoch: frame.streamEpoch,
908
+ requestHash: frame.requestHash,
909
+ ...mutationFailure(error)
910
+ });
911
+ return;
912
+ }
913
+ await this.send({
914
+ type: "transaction",
915
+ protocolVersion: frame.protocolVersion,
916
+ artifactId: frame.artifactId,
917
+ streamEpoch: frame.streamEpoch,
918
+ transaction: receipt.transaction
919
+ });
920
+ await this.send({
921
+ type: "mutationAccepted",
922
+ protocolVersion: frame.protocolVersion,
923
+ artifactId: frame.artifactId,
924
+ streamEpoch: frame.streamEpoch,
925
+ requestHash: receipt.requestHash,
926
+ clientTransactionId: receipt.clientTransactionId,
927
+ transactionId: receipt.transaction.transactionId,
928
+ startSequence: receipt.transaction.startSequence,
929
+ endSequence: receipt.transaction.endSequence,
930
+ stateHash: receipt.transaction.stateHash
931
+ });
932
+ }
933
+ async handleFailure(error) {
934
+ if (this.terminal) return;
935
+ if (error instanceof EditableArtifactLiveError) {
936
+ await this.fail(error.code === "invalid_ticket" ? "transport_error" : error.code);
937
+ return;
938
+ }
939
+ await this.fail("transport_error");
940
+ }
941
+ async fail(reason) {
942
+ if (this.terminal) return;
943
+ this.terminal = true;
944
+ await this.session?.close(reason).catch(() => void 0);
945
+ this.socket?.close(closeCode(reason ?? "transport_error"), reason ?? "transport_error");
946
+ }
947
+ };
948
+ function mutationFailure(error) {
949
+ if (error instanceof EditableArtifactDomainError) {
950
+ if (error.code === "forbidden") return { code: "forbidden", retryable: false };
951
+ if (["invalid_request", "request_hash_mismatch", "invalid_undo_target"].includes(error.code)) {
952
+ return { code: "invalid_request", retryable: false };
953
+ }
954
+ if (error.code === "kernel_contract_violation") {
955
+ return { code: "unsupported", retryable: false };
956
+ }
957
+ if (["retryable_conflict", "outbox_lease_conflict"].includes(error.code)) {
958
+ return { code: "conflict", retryable: true };
959
+ }
960
+ return { code: "conflict", retryable: false };
961
+ }
962
+ if (error instanceof EditableArtifactLiveError && error.code === "permission_changed") {
963
+ return { code: "forbidden", retryable: false };
964
+ }
965
+ return { code: "unavailable", retryable: true };
966
+ }
967
+ function offeredProtocol(value) {
968
+ return value?.split(",").map((part) => part.trim()).includes(EDITABLE_ARTIFACT_LIVE_WEBSOCKET_PROTOCOL) ?? false;
969
+ }
970
+ function closeCode(reason) {
971
+ if (reason === "permission_changed") return 4403;
972
+ if (reason === "ticket_expired" || reason === "ticket_replayed") return 4401;
973
+ if (reason === "protocol_mismatch") return 4406;
974
+ if (reason === "slow_client" || reason === "oversized_frame") return 4409;
975
+ if (reason === "closed") return 1e3;
976
+ return 4400;
977
+ }
978
+
979
+ // src/editable-artifact-production.ts
980
+ import { createHash as createHash4, randomUUID } from "crypto";
981
+ import {
982
+ editableArtifactMaterializerCapabilitiesForRuntime
983
+ } from "@opengeni/artifact-tool/runtime/materializer";
984
+ import {
985
+ EditableArtifactDurableExportService,
986
+ EditableArtifactAgentApplication,
987
+ EDITABLE_ARTIFACT_EXPORT_MAX_DOWNLOAD_BYTES,
988
+ EditableArtifactCompactionPipeline,
989
+ EditableArtifactGenesisPipeline,
990
+ EditableArtifactService,
991
+ MAX_EDITABLE_ARTIFACT_SNAPSHOT_BYTES as MAX_EDITABLE_ARTIFACT_SNAPSHOT_BYTES3,
992
+ ProductionEditableArtifactSnapshotVerifier,
993
+ SystemEditableArtifactClock,
994
+ editableArtifactLiveReadPortFromPostgres,
995
+ editableArtifactLiveTicketStorePortFromPostgres,
996
+ editableArtifactStorePortFromPostgres,
997
+ editableArtifactDurableExportStorePortFromPostgres,
998
+ ogatxEditableArtifactMutationIntentCodec,
999
+ editableArtifactId
1000
+ } from "@opengeni/core/editable-artifacts";
1001
+ import {
1002
+ EDITABLE_ARTIFACT_LIVE_PROTOCOL_VERSION,
1003
+ EditableArtifactApplication,
1004
+ EditableArtifactCompatibilityError,
1005
+ EditableArtifactLiveServer,
1006
+ SystemEditableArtifactLiveClock,
1007
+ SystemEditableArtifactLiveScheduler,
1008
+ WebCryptoEditableArtifactLiveTokens
1009
+ } from "@opengeni/core/editable-artifact-live";
1010
+ import {
1011
+ PostgresEditableArtifactLiveReadStore,
1012
+ PostgresEditableArtifactLiveTicketStore,
1013
+ PostgresEditableArtifactStore,
1014
+ listEditableArtifactIdsForSession,
1015
+ touchEditableArtifactSessionLink,
1016
+ dbSql,
1017
+ withRlsContext
1018
+ } from "@opengeni/db";
1019
+ import { PostgresEditableArtifactDurableExportStore } from "@opengeni/db/editable-artifact-durable-export";
1020
+ import {
1021
+ MAX_BOUNDED_OBJECT_CHUNK_BYTES as MAX_BOUNDED_OBJECT_CHUNK_BYTES2,
1022
+ createObjectStorageBoundedPorts
1023
+ } from "@opengeni/storage";
1024
+
1025
+ // src/editable-artifact-live-hints.ts
1026
+ import {
1027
+ decodeEditableArtifactBrokerHint,
1028
+ editableArtifactLiveHintSubject
1029
+ } from "@opengeni/core";
1030
+ var EventBusEditableArtifactLiveHints = class {
1031
+ constructor(bus) {
1032
+ this.bus = bus;
1033
+ }
1034
+ async subscribe(input) {
1035
+ const connection = this.bus.getOpStreamConnection?.();
1036
+ if (!connection) throw new Error("Event bus has no artifact hint subscription transport");
1037
+ const subject = editableArtifactLiveHintSubject(input.scope, input.artifactId);
1038
+ const subscription = connection.subscribe(subject);
1039
+ let active = true;
1040
+ try {
1041
+ await requireFlushBarrier(connection);
1042
+ } catch (error) {
1043
+ subscription.unsubscribe();
1044
+ throw error;
1045
+ }
1046
+ void (async () => {
1047
+ try {
1048
+ for await (const message of subscription) {
1049
+ if (!active) return;
1050
+ try {
1051
+ const hint = decodeEditableArtifactBrokerHint(message.data, {
1052
+ scope: input.scope,
1053
+ artifactId: input.artifactId
1054
+ });
1055
+ input.onHint({
1056
+ artifactId: input.artifactId,
1057
+ headSequence: hint.headSequence
1058
+ });
1059
+ } catch {
1060
+ }
1061
+ }
1062
+ } catch {
1063
+ if (active) input.onReconnect();
1064
+ }
1065
+ })();
1066
+ return () => {
1067
+ if (!active) return;
1068
+ active = false;
1069
+ subscription.unsubscribe();
1070
+ };
1071
+ }
1072
+ };
1073
+ async function requireFlushBarrier(connection) {
1074
+ if (!connection.flush) {
1075
+ throw new Error("Artifact hint subscription transport lacks a flush barrier");
1076
+ }
1077
+ await connection.flush();
1078
+ }
1079
+
1080
+ // src/editable-artifact-native-kernel.ts
1081
+ import { createHash } from "crypto";
1082
+ import { pathToFileURL } from "url";
1083
+ import {
1084
+ createNativeArtifactSession,
1085
+ openNativeArtifactSession
1086
+ } from "@opengeni/artifact-tool/native";
1087
+ import {
1088
+ doctorConfiguredArtifactRuntime
1089
+ } from "@opengeni/artifact-tool/runtime/development";
1090
+ import {
1091
+ decodeEditableArtifactCausalFrontier,
1092
+ encodeEditableArtifactCausalFrontier
1093
+ } from "@opengeni/contracts/editable-artifact-causal-frontier";
1094
+ import { decodeEditableArtifactSerializedCommit } from "@opengeni/contracts/editable-artifact-serialized-commit";
1095
+ import { decodeEditableArtifactMutationIntent } from "@opengeni/contracts/editable-artifacts";
1096
+ import {
1097
+ MAX_EDITABLE_ARTIFACT_SNAPSHOT_BYTES,
1098
+ causalFrontiersEqual,
1099
+ editableArtifactCausalFrontier,
1100
+ editableArtifactContentHash,
1101
+ editableArtifactReplicaId,
1102
+ editableArtifactStateHash
1103
+ } from "@opengeni/core";
1104
+ var MODEL_SCHEMA_VERSION = 1;
1105
+ var OPERATION_PROTOCOL_VERSION = 1;
1106
+ var CRDT_STATE_VERSION = 1;
1107
+ var SNAPSHOT_CHUNK_BYTES = 1024 * 1024;
1108
+ async function loadVerifiedNativeArtifactRuntimeBinding() {
1109
+ const location = await doctorConfiguredArtifactRuntime();
1110
+ const module = await import(
1111
+ /* @vite-ignore */
1112
+ pathToFileURL(location.skillFacadeEntrypoint).href
1113
+ );
1114
+ const candidate = module.getConfiguredArtifactRuntime?.();
1115
+ assertRuntime(candidate, location);
1116
+ return Object.freeze({ runtime: candidate, location, facade: module });
1117
+ }
1118
+ var NativeEditableArtifactKernelAdapter = class {
1119
+ constructor(runtime, objects) {
1120
+ this.runtime = runtime;
1121
+ this.objects = objects;
1122
+ if (runtime.kind !== "native" || runtime.target === "wasm-web") {
1123
+ throw new Error("Editable artifact authority requires a native kernel");
1124
+ }
1125
+ }
1126
+ async applyTransaction(request) {
1127
+ const session = await this.openCurrentState(request.state);
1128
+ try {
1129
+ if (request.modality === "spreadsheet") {
1130
+ if (session.modality !== "spreadsheet") throw new Error("Native modality mismatch");
1131
+ const committedTransactionBytes = session.authorTransaction(
1132
+ request.intentBytes,
1133
+ encodeEditableArtifactCausalFrontier(request.resolvedCausalBase)
1134
+ );
1135
+ return Object.freeze({
1136
+ modality: "spreadsheet",
1137
+ committedTransactionBytes,
1138
+ kernelVersion: this.runtime.buildIdentity,
1139
+ modelSchemaVersion: MODEL_SCHEMA_VERSION
1140
+ });
1141
+ }
1142
+ if (session.modality !== request.modality) {
1143
+ throw new Error("Native modality mismatch");
1144
+ }
1145
+ const nativeReceiptBytes = session.applyCommands(request.intent.commandBytes);
1146
+ return Object.freeze({
1147
+ modality: request.modality,
1148
+ nativeReceiptBytes,
1149
+ resultingStateHash: editableArtifactStateHash(session.stateHash()),
1150
+ kernelVersion: this.runtime.buildIdentity,
1151
+ modelSchemaVersion: MODEL_SCHEMA_VERSION
1152
+ });
1153
+ } finally {
1154
+ session.dispose();
1155
+ }
1156
+ }
1157
+ async query(input) {
1158
+ const session = await this.openCurrentState(input.state);
1159
+ try {
1160
+ return Uint8Array.from(session.query(input.queryBytes));
1161
+ } finally {
1162
+ session.dispose();
1163
+ }
1164
+ }
1165
+ async openCurrentState(state) {
1166
+ const snapshot = state.snapshot;
1167
+ if (!snapshot) throw new Error("Editable artifact has no verified replay snapshot");
1168
+ const snapshotBytes = await readVerifiedSnapshot(this.objects, snapshot);
1169
+ const session = openNativeArtifactSession(this.runtime, {
1170
+ modality: state.modality,
1171
+ snapshot: snapshotBytes
1172
+ });
1173
+ try {
1174
+ verifySnapshotBoundary(session, state);
1175
+ replayKernelTail(session, state);
1176
+ verifyDurableHead(session, state);
1177
+ return session;
1178
+ } catch (error) {
1179
+ session.dispose();
1180
+ throw error;
1181
+ }
1182
+ }
1183
+ async createCanonicalEmptySnapshot(input) {
1184
+ throwIfAborted(input.signal);
1185
+ const session = createNativeArtifactSession(this.runtime, {
1186
+ modality: input.modality,
1187
+ replicaNamespace: namespaceForArtifact(input.artifactId)
1188
+ });
1189
+ try {
1190
+ const snapshot = session.snapshot();
1191
+ throwIfAborted(input.signal);
1192
+ const revision = safeRevision(session.revision());
1193
+ if (revision !== 0) throw new Error("Native genesis revision is not zero");
1194
+ const common = {
1195
+ scope: input.scope,
1196
+ artifactId: input.artifactId,
1197
+ modality: input.modality,
1198
+ canonicalChunks: fixedChunks(snapshot, input.signal),
1199
+ canonicalByteSize: snapshot.byteLength,
1200
+ canonicalContentHash: editableArtifactContentHash(
1201
+ `sha256:${createHash("sha256").update(snapshot).digest("hex")}`
1202
+ ),
1203
+ stateHash: editableArtifactStateHash(session.stateHash()),
1204
+ coveredHeadSequence: 0,
1205
+ modelSchemaVersion: MODEL_SCHEMA_VERSION,
1206
+ kernelVersion: this.runtime.buildIdentity
1207
+ };
1208
+ if (session.modality === "spreadsheet") {
1209
+ return Object.freeze({
1210
+ ...common,
1211
+ modality: "spreadsheet",
1212
+ coveredCausalFrontier: domainFrontier(session.frontier()),
1213
+ operationProtocolVersion: OPERATION_PROTOCOL_VERSION,
1214
+ crdtStateVersion: CRDT_STATE_VERSION
1215
+ });
1216
+ }
1217
+ return Object.freeze({
1218
+ ...common,
1219
+ modality: session.modality,
1220
+ nativeRevision: 0
1221
+ });
1222
+ } finally {
1223
+ session.dispose();
1224
+ }
1225
+ }
1226
+ async createCanonicalSnapshot(input) {
1227
+ throwIfAborted(input.signal);
1228
+ const snapshot = input.state.snapshot;
1229
+ if (!snapshot) throw new Error("Editable artifact has no verified replay snapshot");
1230
+ const snapshotBytes = await readVerifiedSnapshot(this.objects, snapshot, input.signal);
1231
+ const session = openNativeArtifactSession(this.runtime, {
1232
+ modality: input.state.modality,
1233
+ snapshot: snapshotBytes
1234
+ });
1235
+ try {
1236
+ verifySnapshotBoundary(session, input.state);
1237
+ replayKernelTail(session, input.state);
1238
+ verifyDurableHead(session, input.state);
1239
+ throwIfAborted(input.signal);
1240
+ const canonical = session.snapshot();
1241
+ const common = {
1242
+ scope: input.state.artifact.scope,
1243
+ artifactId: input.state.artifact.id,
1244
+ modality: input.state.modality,
1245
+ canonicalChunks: fixedChunks(canonical, input.signal),
1246
+ canonicalByteSize: canonical.byteLength,
1247
+ canonicalContentHash: editableArtifactContentHash(
1248
+ `sha256:${createHash("sha256").update(canonical).digest("hex")}`
1249
+ ),
1250
+ stateHash: editableArtifactStateHash(session.stateHash()),
1251
+ coveredHeadSequence: input.state.artifact.headSequence,
1252
+ modelSchemaVersion: MODEL_SCHEMA_VERSION,
1253
+ kernelVersion: this.runtime.buildIdentity
1254
+ };
1255
+ if (session.modality === "spreadsheet") {
1256
+ if (input.state.modality !== "spreadsheet") throw new Error("Native modality mismatch");
1257
+ return Object.freeze({
1258
+ ...common,
1259
+ modality: session.modality,
1260
+ coveredCausalFrontier: domainFrontier(session.frontier()),
1261
+ operationProtocolVersion: OPERATION_PROTOCOL_VERSION,
1262
+ crdtStateVersion: CRDT_STATE_VERSION
1263
+ });
1264
+ }
1265
+ if (session.modality !== input.state.modality) throw new Error("Native modality mismatch");
1266
+ return Object.freeze({
1267
+ ...common,
1268
+ modality: session.modality,
1269
+ nativeRevision: safeRevision(session.revision())
1270
+ });
1271
+ } finally {
1272
+ session.dispose();
1273
+ }
1274
+ }
1275
+ async verifyAndReconstruct(input) {
1276
+ throwIfAborted(input.signal);
1277
+ const bytes = await collectExact(
1278
+ input.snapshotChunks,
1279
+ input.expectedSnapshotByteSize,
1280
+ input.signal
1281
+ );
1282
+ const session = openNativeArtifactSession(this.runtime, {
1283
+ modality: input.modality,
1284
+ snapshot: bytes
1285
+ });
1286
+ try {
1287
+ const canonical = session.snapshot();
1288
+ if (!equalBytes(canonical, bytes)) throw new Error("Native snapshot is not canonical");
1289
+ const expected = input.expectedSnapshot;
1290
+ if (expected.modality !== session.modality) throw new Error("Snapshot modality mismatch");
1291
+ const coveredHeadSequence = expected.coveredHeadSequence;
1292
+ const stateHash = editableArtifactStateHash(session.stateHash());
1293
+ if (stateHash !== expected.stateHash) throw new Error("Snapshot state hash mismatch");
1294
+ const canonicalContentHash = editableArtifactContentHash(
1295
+ `sha256:${createHash("sha256").update(canonical).digest("hex")}`
1296
+ );
1297
+ if (session.modality === "spreadsheet") {
1298
+ const coveredCausalFrontier = domainFrontier(session.frontier());
1299
+ if (expected.modality !== "spreadsheet" || !causalFrontiersEqual(coveredCausalFrontier, expected.coveredCausalFrontier)) {
1300
+ throw new Error("Snapshot causal coverage mismatch");
1301
+ }
1302
+ if (input.replayPlan && input.replayPlan.modality !== "spreadsheet") {
1303
+ throw new Error("Replay modality mismatch");
1304
+ }
1305
+ const replayedTarget2 = input.replayPlan ? await replaySpreadsheetPlan(session, input.replayPlan, input.signal) : void 0;
1306
+ return Object.freeze({
1307
+ modality: "spreadsheet",
1308
+ canonicalReencodeVerified: true,
1309
+ scope: input.scope,
1310
+ artifactId: input.artifactId,
1311
+ canonicalByteSize: canonical.byteLength,
1312
+ canonicalContentHash,
1313
+ coveredHeadSequence,
1314
+ stateHash,
1315
+ modelSchemaVersion: MODEL_SCHEMA_VERSION,
1316
+ kernelVersion: this.runtime.buildIdentity,
1317
+ fullCrdtStateVerified: true,
1318
+ coveredCausalFrontier,
1319
+ operationProtocolVersion: OPERATION_PROTOCOL_VERSION,
1320
+ crdtStateVersion: CRDT_STATE_VERSION,
1321
+ ...replayedTarget2 ? { replayedTarget: replayedTarget2 } : {}
1322
+ });
1323
+ }
1324
+ if (expected.modality === "spreadsheet") throw new Error("Snapshot modality mismatch");
1325
+ const nativeRevision = safeRevision(session.revision());
1326
+ if (nativeRevision !== expected.nativeRevision) {
1327
+ throw new Error("Snapshot native revision mismatch");
1328
+ }
1329
+ if (input.replayPlan && input.replayPlan.modality === "spreadsheet") {
1330
+ throw new Error("Replay modality mismatch");
1331
+ }
1332
+ const replayedTarget = input.replayPlan ? await replaySerializedPlan(session, input.replayPlan, input.signal) : void 0;
1333
+ return Object.freeze({
1334
+ modality: session.modality,
1335
+ canonicalReencodeVerified: true,
1336
+ scope: input.scope,
1337
+ artifactId: input.artifactId,
1338
+ canonicalByteSize: canonical.byteLength,
1339
+ canonicalContentHash,
1340
+ coveredHeadSequence,
1341
+ stateHash,
1342
+ modelSchemaVersion: MODEL_SCHEMA_VERSION,
1343
+ kernelVersion: this.runtime.buildIdentity,
1344
+ nativeRevision,
1345
+ ...replayedTarget ? { replayedTarget } : {}
1346
+ });
1347
+ } finally {
1348
+ session.dispose();
1349
+ }
1350
+ }
1351
+ };
1352
+ function assertRuntime(candidate, location) {
1353
+ if (typeof candidate !== "object" || candidate === null) {
1354
+ throw new Error("Verified artifact facade returned no runtime");
1355
+ }
1356
+ const runtime = candidate;
1357
+ if (runtime.kind !== "native" || runtime.target === "wasm-web" || runtime.target !== location.target || runtime.buildIdentity !== location.kernel.buildIdentity || typeof runtime.createCollaborationSession !== "function" || typeof runtime.openCollaborationSession !== "function" || typeof runtime.createDocumentSession !== "function" || typeof runtime.openDocumentSession !== "function" || typeof runtime.createPresentationSession !== "function" || typeof runtime.openPresentationSession !== "function") {
1358
+ throw new Error("Verified artifact facade returned an incompatible runtime");
1359
+ }
1360
+ }
1361
+ function verifySnapshotBoundary(session, state) {
1362
+ const snapshot = state.snapshot;
1363
+ if (!snapshot || session.modality !== state.modality)
1364
+ throw new Error("Snapshot modality mismatch");
1365
+ if (session.stateHash() !== snapshot.stateHash) {
1366
+ throw new Error("Native snapshot boundary does not match durable metadata");
1367
+ }
1368
+ if (state.modality === "spreadsheet") {
1369
+ if (session.modality !== "spreadsheet" || snapshot.modality !== "spreadsheet" || !causalFrontiersEqual(domainFrontier(session.frontier()), snapshot.coveredCausalFrontier)) {
1370
+ throw new Error("Native snapshot frontier does not match durable metadata");
1371
+ }
1372
+ } else if (session.modality === "spreadsheet" || snapshot.modality === "spreadsheet" || safeRevision(session.revision()) !== snapshot.nativeRevision || state.baseNativeRevision !== snapshot.nativeRevision) {
1373
+ throw new Error("Native snapshot revision does not match durable metadata");
1374
+ }
1375
+ }
1376
+ function replayKernelTail(session, state) {
1377
+ if (state.modality === "spreadsheet") {
1378
+ if (session.modality !== "spreadsheet") throw new Error("Native modality mismatch");
1379
+ for (const transaction of state.committedTransactionTail) {
1380
+ const priorNativeRevision = safeRevision(session.revision());
1381
+ if (session.stateHash() !== transaction.priorStateHash) {
1382
+ throw new Error("Spreadsheet replay prior state mismatch");
1383
+ }
1384
+ session.applyCommitted(transaction.committedTransactionBytes);
1385
+ if (safeRevision(session.revision()) !== priorNativeRevision + 1 || session.stateHash() !== transaction.stateHash || !causalFrontiersEqual(
1386
+ domainFrontier(session.frontier()),
1387
+ transaction.resultingCausalFrontier
1388
+ )) {
1389
+ throw new Error("Spreadsheet replay result mismatch");
1390
+ }
1391
+ }
1392
+ return;
1393
+ }
1394
+ if (session.modality !== state.modality) {
1395
+ throw new Error("Native modality mismatch");
1396
+ }
1397
+ for (const transaction of state.committedTransactionTail) {
1398
+ if (session.stateHash() !== transaction.priorStateHash || safeRevision(session.revision()) !== transaction.priorNativeRevision) {
1399
+ throw new Error("Serialized replay prior state mismatch");
1400
+ }
1401
+ const commit = decodeEditableArtifactSerializedCommit(
1402
+ transaction.committedTransactionBytes,
1403
+ state.modality
1404
+ );
1405
+ const intent = decodeEditableArtifactMutationIntent(commit.intentBytes);
1406
+ const receipt = session.applyCommands(intent.commandBytes);
1407
+ if (!equalBytes(receipt, commit.nativeReceiptBytes) || !equalBytes(receipt, transaction.nativeReceiptBytes) || safeRevision(session.revision()) !== transaction.nativeRevision || session.stateHash() !== transaction.stateHash) {
1408
+ throw new Error("Serialized replay result mismatch");
1409
+ }
1410
+ }
1411
+ }
1412
+ function verifyDurableHead(session, state) {
1413
+ const snapshot = state.snapshot;
1414
+ if (!snapshot) throw new Error("Editable artifact has no replay snapshot");
1415
+ let durableSequence = snapshot.coveredHeadSequence;
1416
+ for (const transaction of state.committedTransactionTail) {
1417
+ if (transaction.sequenceStart !== durableSequence + 1) {
1418
+ throw new Error("Durable transaction tail contains a sequence gap");
1419
+ }
1420
+ durableSequence = transaction.sequenceEnd;
1421
+ }
1422
+ if (session.modality !== state.modality || durableSequence !== state.artifact.headSequence || session.stateHash() !== state.artifact.stateHash) {
1423
+ throw new Error("Reconstructed native state does not match the durable head");
1424
+ }
1425
+ if (state.modality === "spreadsheet" && (session.modality !== "spreadsheet" || !causalFrontiersEqual(domainFrontier(session.frontier()), state.artifact.causalFrontier))) {
1426
+ throw new Error("Reconstructed native frontier does not match the durable head");
1427
+ }
1428
+ if (state.modality !== "spreadsheet") {
1429
+ const expectedNativeRevision = state.committedTransactionTail.at(-1)?.nativeRevision ?? state.baseNativeRevision;
1430
+ if (safeRevision(session.revision()) !== expectedNativeRevision) {
1431
+ throw new Error("Reconstructed native revision does not match the serialized tail");
1432
+ }
1433
+ }
1434
+ }
1435
+ async function replaySpreadsheetPlan(session, plan, signal) {
1436
+ let sequence = plan.baseHeadSequence;
1437
+ for await (const segment of plan.segments) {
1438
+ throwIfAborted(signal);
1439
+ if (segment.sequenceStart !== sequence + 1) throw new Error("Replay sequence gap");
1440
+ if (segment.modality !== "spreadsheet") throw new Error("Replay segment modality mismatch");
1441
+ session.applyCommitted(segment.bytes);
1442
+ sequence = segment.sequenceEnd;
1443
+ }
1444
+ if (sequence !== plan.targetHeadSequence || session.stateHash() !== plan.targetStateHash) {
1445
+ throw new Error("Replay target mismatch");
1446
+ }
1447
+ const causalFrontier = domainFrontier(session.frontier());
1448
+ if (!causalFrontiersEqual(causalFrontier, plan.targetCausalFrontier)) {
1449
+ throw new Error("Replay frontier mismatch");
1450
+ }
1451
+ return Object.freeze({
1452
+ headSequence: sequence,
1453
+ causalFrontier,
1454
+ stateHash: editableArtifactStateHash(session.stateHash())
1455
+ });
1456
+ }
1457
+ async function replaySerializedPlan(session, plan, signal) {
1458
+ if (session.modality !== plan.modality) throw new Error("Replay modality mismatch");
1459
+ let sequence = plan.baseHeadSequence;
1460
+ for await (const segment of plan.segments) {
1461
+ throwIfAborted(signal);
1462
+ if (segment.sequenceStart !== sequence + 1 || segment.modality !== session.modality) {
1463
+ throw new Error("Replay sequence or modality mismatch");
1464
+ }
1465
+ const commit = decodeEditableArtifactSerializedCommit(segment.bytes, session.modality);
1466
+ const intent = decodeEditableArtifactMutationIntent(commit.intentBytes);
1467
+ const receipt = session.applyCommands(intent.commandBytes);
1468
+ if (!equalBytes(receipt, commit.nativeReceiptBytes)) throw new Error("Replay receipt mismatch");
1469
+ sequence = segment.sequenceEnd;
1470
+ }
1471
+ if (sequence !== plan.targetHeadSequence || session.stateHash() !== plan.targetStateHash) {
1472
+ throw new Error("Replay target mismatch");
1473
+ }
1474
+ if (safeRevision(session.revision()) !== plan.targetNativeRevision) {
1475
+ throw new Error("Replay native revision mismatch");
1476
+ }
1477
+ return Object.freeze({
1478
+ headSequence: sequence,
1479
+ nativeRevision: plan.targetNativeRevision,
1480
+ stateHash: editableArtifactStateHash(session.stateHash())
1481
+ });
1482
+ }
1483
+ function domainFrontier(bytes) {
1484
+ return editableArtifactCausalFrontier(
1485
+ decodeEditableArtifactCausalFrontier(bytes).map((entry) => ({
1486
+ replicaId: editableArtifactReplicaId(entry.replicaId),
1487
+ counter: entry.counter
1488
+ }))
1489
+ );
1490
+ }
1491
+ async function readVerifiedSnapshot(objects, snapshot, signal) {
1492
+ const object = await objects.open({
1493
+ opaqueReference: snapshot.blobReference,
1494
+ maxBytes: MAX_EDITABLE_ARTIFACT_SNAPSHOT_BYTES,
1495
+ expectedByteSize: snapshot.byteSize,
1496
+ ...signal ? { signal } : {}
1497
+ });
1498
+ try {
1499
+ if (object.contentType !== void 0 && object.contentType !== snapshot.mimeType) {
1500
+ throw new Error("Snapshot content type mismatch");
1501
+ }
1502
+ const bytes = await collectExact(
1503
+ object.chunks(signal ? { signal } : {}),
1504
+ snapshot.byteSize,
1505
+ signal
1506
+ );
1507
+ const contentHash = `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
1508
+ if (contentHash !== snapshot.contentHash) throw new Error("Snapshot content hash mismatch");
1509
+ await object.assertUnchanged(signal);
1510
+ return bytes;
1511
+ } finally {
1512
+ await object.close();
1513
+ }
1514
+ }
1515
+ async function collectExact(chunks, expectedBytes, signal) {
1516
+ if (!Number.isSafeInteger(expectedBytes) || expectedBytes <= 0) {
1517
+ throw new Error("Snapshot byte size is invalid");
1518
+ }
1519
+ const result = new Uint8Array(expectedBytes);
1520
+ let offset = 0;
1521
+ for await (const chunk of chunks) {
1522
+ throwIfAborted(signal);
1523
+ if (!(chunk instanceof Uint8Array) || chunk.byteLength === 0) {
1524
+ throw new Error("Snapshot stream is truncated");
1525
+ }
1526
+ if (offset + chunk.byteLength > result.byteLength) {
1527
+ throw new Error("Snapshot stream exceeds its declared size");
1528
+ }
1529
+ result.set(chunk, offset);
1530
+ offset += chunk.byteLength;
1531
+ }
1532
+ throwIfAborted(signal);
1533
+ if (offset !== result.byteLength) throw new Error("Snapshot stream is truncated");
1534
+ return result;
1535
+ }
1536
+ async function* fixedChunks(bytes, signal) {
1537
+ for (let offset = 0; offset < bytes.byteLength; offset += SNAPSHOT_CHUNK_BYTES) {
1538
+ throwIfAborted(signal);
1539
+ yield bytes.subarray(offset, Math.min(bytes.byteLength, offset + SNAPSHOT_CHUNK_BYTES));
1540
+ }
1541
+ }
1542
+ function namespaceForArtifact(artifactId) {
1543
+ const digest = createHash("sha256").update(artifactId).digest();
1544
+ let value = 0n;
1545
+ for (let index = 0; index < 8; index += 1) value = value << 8n | BigInt(digest[index]);
1546
+ return value === 0n ? 1n : value;
1547
+ }
1548
+ function safeRevision(value) {
1549
+ if (value < 0n || value > BigInt(Number.MAX_SAFE_INTEGER)) {
1550
+ throw new Error("Native revision exceeds the safe integer range");
1551
+ }
1552
+ return Number(value);
1553
+ }
1554
+ function equalBytes(left, right) {
1555
+ if (left.byteLength !== right.byteLength) return false;
1556
+ let difference = 0;
1557
+ for (let index = 0; index < left.byteLength; index += 1) {
1558
+ difference |= left[index] ^ right[index];
1559
+ }
1560
+ return difference === 0;
1561
+ }
1562
+ function throwIfAborted(signal) {
1563
+ if (signal?.aborted) throw new Error("Editable artifact native operation was cancelled");
1564
+ }
1565
+
1566
+ // src/editable-artifact-office-import.ts
1567
+ import { createHash as createHash2 } from "crypto";
1568
+ import {
1569
+ ArtifactOfficeSourceUnsupportedError,
1570
+ prepareArtifactOfficeImport
1571
+ } from "@opengeni/artifact-tool/office-import";
1572
+ import {
1573
+ MAX_EDITABLE_ARTIFACT_SNAPSHOT_BYTES as MAX_EDITABLE_ARTIFACT_SNAPSHOT_BYTES2,
1574
+ EDITABLE_ARTIFACT_ORIGINAL_IMPORT_MAX_BYTES,
1575
+ editableArtifactCausalFrontier as editableArtifactCausalFrontier2,
1576
+ editableArtifactContentHash as editableArtifactContentHash2,
1577
+ editableArtifactReplicaId as editableArtifactReplicaId2,
1578
+ editableArtifactStateHash as editableArtifactStateHash2,
1579
+ EditableArtifactOfficeImportError
1580
+ } from "@opengeni/core/editable-artifacts";
1581
+ import { getFile } from "@opengeni/db";
1582
+ import {
1583
+ MAX_BOUNDED_OBJECT_CHUNK_BYTES
1584
+ } from "@opengeni/storage";
1585
+ var SNAPSHOT_MIME = "application/vnd.opengeni.editable-artifact-snapshot";
1586
+ var EditableArtifactOfficeImportAdapter = class {
1587
+ constructor(dependencies) {
1588
+ this.dependencies = dependencies;
1589
+ this.readFile = dependencies.readFile ?? getFile;
1590
+ }
1591
+ readFile;
1592
+ async prepare(input) {
1593
+ throwIfAborted2(input.signal);
1594
+ const file = await this.readFile(this.dependencies.db, input.scope.workspaceId, input.fileId);
1595
+ if (!file || file.status !== "ready") {
1596
+ throw new EditableArtifactOfficeImportError("invalid_source");
1597
+ }
1598
+ const expectedMimeType = officeMimeType(input.modality);
1599
+ if (file.contentType !== expectedMimeType || !officeFilenameMatches(file.filename, input.modality) || !Number.isSafeInteger(file.sizeBytes) || file.sizeBytes < 1 || file.sizeBytes > EDITABLE_ARTIFACT_ORIGINAL_IMPORT_MAX_BYTES || typeof file.sha256 !== "string" || !/^[0-9a-f]{64}$/u.test(file.sha256)) {
1600
+ throw new EditableArtifactOfficeImportError("invalid_source");
1601
+ }
1602
+ const sourceBytes = await readVerifiedWorkspaceFile(
1603
+ this.dependencies.objectStorage,
1604
+ file,
1605
+ input.signal
1606
+ );
1607
+ const prepared = await prepareOfficeImport({
1608
+ runtime: this.dependencies.runtime,
1609
+ modality: input.modality,
1610
+ filename: file.filename,
1611
+ mimeType: expectedMimeType,
1612
+ bytes: sourceBytes,
1613
+ prepare: this.dependencies.prepareOffice ?? prepareArtifactOfficeImport
1614
+ });
1615
+ const retainedSource = await this.dependencies.sourceObjects.write({
1616
+ chunks: byteChunks(sourceBytes),
1617
+ contentType: expectedMimeType,
1618
+ maxBytes: EDITABLE_ARTIFACT_ORIGINAL_IMPORT_MAX_BYTES,
1619
+ expectedByteSize: prepared.source.byteSize,
1620
+ expectedContentHash: prepared.source.contentHash,
1621
+ ...input.signal ? { signal: input.signal } : {}
1622
+ });
1623
+ const retainedSnapshot = await this.dependencies.snapshotObjects.write({
1624
+ chunks: byteChunks(prepared.snapshot.bytes),
1625
+ contentType: SNAPSHOT_MIME,
1626
+ maxBytes: MAX_EDITABLE_ARTIFACT_SNAPSHOT_BYTES2,
1627
+ expectedByteSize: prepared.snapshot.byteSize,
1628
+ expectedContentHash: prepared.snapshot.contentHash,
1629
+ ...input.signal ? { signal: input.signal } : {}
1630
+ });
1631
+ const common = {
1632
+ modality: prepared.snapshot.modality,
1633
+ blobReference: retainedSnapshot.opaqueReference,
1634
+ byteSize: prepared.snapshot.byteSize,
1635
+ contentHash: editableArtifactContentHash2(prepared.snapshot.contentHash),
1636
+ mimeType: SNAPSHOT_MIME,
1637
+ coveredHeadSequence: 0,
1638
+ stateHash: editableArtifactStateHash2(prepared.snapshot.stateHash),
1639
+ modelSchemaVersion: prepared.snapshot.modelSchemaVersion,
1640
+ kernelVersion: prepared.snapshot.kernelVersion
1641
+ };
1642
+ return Object.freeze({
1643
+ originalImport: Object.freeze({
1644
+ fileId: file.id,
1645
+ blobReference: retainedSource.opaqueReference,
1646
+ byteSize: prepared.source.byteSize,
1647
+ contentHash: editableArtifactContentHash2(prepared.source.contentHash),
1648
+ mimeType: expectedMimeType
1649
+ }),
1650
+ snapshot: prepared.snapshot.modality === "spreadsheet" ? Object.freeze({
1651
+ ...common,
1652
+ modality: "spreadsheet",
1653
+ coveredCausalFrontier: editableArtifactCausalFrontier2(
1654
+ prepared.snapshot.coveredCausalFrontier.map((entry) => ({
1655
+ replicaId: editableArtifactReplicaId2(entry.replicaId),
1656
+ counter: entry.counter
1657
+ }))
1658
+ ),
1659
+ operationProtocolVersion: prepared.snapshot.operationProtocolVersion,
1660
+ crdtStateVersion: prepared.snapshot.crdtStateVersion
1661
+ }) : Object.freeze({
1662
+ ...common,
1663
+ modality: prepared.snapshot.modality,
1664
+ nativeRevision: prepared.snapshot.nativeRevision
1665
+ })
1666
+ });
1667
+ }
1668
+ };
1669
+ async function prepareOfficeImport(input) {
1670
+ try {
1671
+ return await input.prepare({
1672
+ facade: input.runtime.facade,
1673
+ modality: input.modality,
1674
+ filename: input.filename,
1675
+ mimeType: input.mimeType,
1676
+ bytes: input.bytes,
1677
+ expectedRuntimeTarget: input.runtime.location.target,
1678
+ expectedKernelVersion: input.runtime.runtime.buildIdentity
1679
+ });
1680
+ } catch (error) {
1681
+ if (error instanceof ArtifactOfficeSourceUnsupportedError) {
1682
+ throw new EditableArtifactOfficeImportError("unsupported_content");
1683
+ }
1684
+ throw error;
1685
+ }
1686
+ }
1687
+ function officeMimeType(modality) {
1688
+ if (modality === "spreadsheet") {
1689
+ return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
1690
+ }
1691
+ if (modality === "document") {
1692
+ return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
1693
+ }
1694
+ return "application/vnd.openxmlformats-officedocument.presentationml.presentation";
1695
+ }
1696
+ function officeFilenameMatches(filename, modality) {
1697
+ const extension = modality === "spreadsheet" ? ".xlsx" : modality === "document" ? ".docx" : ".pptx";
1698
+ return filename.toLowerCase().endsWith(extension);
1699
+ }
1700
+ async function readVerifiedWorkspaceFile(storage, file, signal) {
1701
+ throwIfAborted2(signal);
1702
+ const before = await storage.headFile(file);
1703
+ if (before.ContentLength !== file.sizeBytes || before.ContentType !== file.contentType || before.Metadata?.sha256 !== file.sha256 || typeof before.VersionToken !== "string" || before.VersionToken.length < 1) {
1704
+ throw new EditableArtifactOfficeImportError("source_changed");
1705
+ }
1706
+ const bytes = new Uint8Array(file.sizeBytes);
1707
+ const digest = createHash2("sha256");
1708
+ for (let start = 0; start < file.sizeBytes; start += MAX_BOUNDED_OBJECT_CHUNK_BYTES) {
1709
+ throwIfAborted2(signal);
1710
+ const end = Math.min(file.sizeBytes, start + MAX_BOUNDED_OBJECT_CHUNK_BYTES) - 1;
1711
+ const chunk = await storage.getFileRange(file, { start, end });
1712
+ if (!chunk || chunk.byteLength !== end - start + 1) {
1713
+ throw new EditableArtifactOfficeImportError("source_changed");
1714
+ }
1715
+ bytes.set(chunk, start);
1716
+ digest.update(chunk);
1717
+ }
1718
+ const after = await storage.headFile(file);
1719
+ const sha256 = digest.digest("hex");
1720
+ if (after.ContentLength !== before.ContentLength || after.ContentType !== before.ContentType || after.Metadata?.sha256 !== before.Metadata?.sha256 || after.VersionToken !== before.VersionToken || sha256 !== file.sha256) {
1721
+ throw new EditableArtifactOfficeImportError("source_changed");
1722
+ }
1723
+ return bytes;
1724
+ }
1725
+ async function* byteChunks(bytes) {
1726
+ for (let offset = 0; offset < bytes.byteLength; offset += MAX_BOUNDED_OBJECT_CHUNK_BYTES) {
1727
+ yield bytes.subarray(offset, offset + MAX_BOUNDED_OBJECT_CHUNK_BYTES);
1728
+ }
1729
+ }
1730
+ function throwIfAborted2(signal) {
1731
+ if (signal?.aborted) throw signal.reason ?? new Error("Artifact import aborted");
1732
+ }
1733
+
1734
+ // src/editable-artifact-workspace-files.ts
1735
+ import { createHash as createHash3 } from "crypto";
1736
+ import { completeFileUpload, prepareGeneratedWorkspaceFile } from "@opengeni/db";
1737
+ var UPLOAD_INTENT_TTL_MS = 60 * 6e4;
1738
+ var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
1739
+ var EditableArtifactWorkspaceFileAdapter = class {
1740
+ constructor(dependencies) {
1741
+ this.dependencies = dependencies;
1742
+ this.now = dependencies.now ?? (() => /* @__PURE__ */ new Date());
1743
+ this.prepareFile = dependencies.prepareFile ?? prepareGeneratedWorkspaceFile;
1744
+ this.completeFile = dependencies.completeFile ?? completeFileUpload;
1745
+ }
1746
+ now;
1747
+ prepareFile;
1748
+ completeFile;
1749
+ async ensureMaterializationFile(input) {
1750
+ throwIfAborted3(input.signal);
1751
+ const storage = this.dependencies.objectStorage;
1752
+ if (!storage.headObject || !storage.putObjectStreamIfAbsent) {
1753
+ throw new Error("Object storage lacks immutable streaming upload support");
1754
+ }
1755
+ const download = await this.dependencies.durableExports.openMaterializationDownload({
1756
+ scope: input.scope,
1757
+ actor: input.actor,
1758
+ artifactId: input.artifact.id,
1759
+ jobId: input.jobId,
1760
+ ...input.signal ? { signal: input.signal } : {}
1761
+ });
1762
+ try {
1763
+ const sha256 = plainSha256(download.contentHash);
1764
+ const filename = exportFilename(input.filename, download.format);
1765
+ const fileId = deterministicUuid(
1766
+ `editable-artifact-export:file:${input.scope.workspaceId}:${input.artifact.id}:${input.versionId}:${input.jobId}`
1767
+ );
1768
+ const uploadId = deterministicUuid(
1769
+ `editable-artifact-export:upload:${input.scope.workspaceId}:${input.artifact.id}:${input.versionId}:${input.jobId}`
1770
+ );
1771
+ const safeFilename = `artifact-${input.artifact.id}-${input.jobId}.${download.format}`;
1772
+ const objectKey = `workspaces/${input.scope.workspaceId}/files/${fileId}/artifact-exports/${safeFilename}`;
1773
+ const prepared = await this.prepareFile(this.dependencies.db, {
1774
+ accountId: input.scope.accountId,
1775
+ workspaceId: input.scope.workspaceId,
1776
+ fileId,
1777
+ uploadId,
1778
+ filename,
1779
+ safeFilename,
1780
+ contentType: download.mimeType,
1781
+ sizeBytes: download.byteSize,
1782
+ sha256,
1783
+ bucket: storage.bucket,
1784
+ objectKey,
1785
+ expiresAt: new Date(this.now().getTime() + UPLOAD_INTENT_TTL_MS)
1786
+ });
1787
+ let file = prepared.file;
1788
+ if (file.status !== "ready") {
1789
+ const existing = await storage.headObject(objectKey);
1790
+ if (existing) {
1791
+ assertStoredExport(existing, download.byteSize, download.mimeType, sha256);
1792
+ } else {
1793
+ await storage.putObjectStreamIfAbsent({
1794
+ key: objectKey,
1795
+ contentType: download.mimeType,
1796
+ chunks: download.chunks(input.signal ? { signal: input.signal } : {}),
1797
+ byteSize: download.byteSize,
1798
+ sha256,
1799
+ ...input.signal ? { signal: input.signal } : {}
1800
+ });
1801
+ const stored = await storage.headObject(objectKey);
1802
+ assertStoredExport(stored, download.byteSize, download.mimeType, sha256);
1803
+ }
1804
+ await download.assertUnchanged(input.signal);
1805
+ throwIfAborted3(input.signal);
1806
+ file = await this.completeFile(this.dependencies.db, input.scope.workspaceId, uploadId);
1807
+ } else {
1808
+ assertStoredExport(
1809
+ await storage.headObject(objectKey),
1810
+ download.byteSize,
1811
+ download.mimeType,
1812
+ sha256
1813
+ );
1814
+ await download.assertUnchanged(input.signal);
1815
+ }
1816
+ if (file.id !== fileId || file.status !== "ready" || file.filename !== filename || file.contentType !== download.mimeType || file.sizeBytes !== download.byteSize || file.sha256 !== sha256) {
1817
+ throw new Error("Generated workspace file differs from its immutable export");
1818
+ }
1819
+ return Object.freeze({
1820
+ fileId: file.id,
1821
+ filename: file.filename,
1822
+ contentType: file.contentType,
1823
+ sizeBytes: file.sizeBytes,
1824
+ sha256,
1825
+ artifactId: input.artifact.id,
1826
+ versionId: input.versionId,
1827
+ materializationJobId: input.jobId,
1828
+ sourceHeadSequence: input.sourceHeadSequence,
1829
+ sourceStateHash: input.sourceStateHash
1830
+ });
1831
+ } finally {
1832
+ await download.close();
1833
+ }
1834
+ }
1835
+ };
1836
+ function assertStoredExport(head, byteSize, contentType, sha256) {
1837
+ if (!head || head.ContentLength !== byteSize || head.ContentType !== contentType || head.Metadata?.sha256 !== sha256 || typeof head.VersionToken !== "string" || head.VersionToken.length < 1) {
1838
+ throw new Error("Workspace export object differs from its durable materialization");
1839
+ }
1840
+ }
1841
+ function exportFilename(value, format) {
1842
+ const expectedExtension = `.${format}`;
1843
+ const normalized = value.normalize("NFKC").replace(/[\\/:*?"<>|\u0000-\u001f\u007f]+/gu, "-").replace(/\s+/gu, " ").trim().slice(0, 200);
1844
+ const stem = normalized.toLowerCase().endsWith(expectedExtension) ? normalized.slice(0, -expectedExtension.length) : normalized;
1845
+ return `${stem.replace(/[. ]+$/u, "").slice(0, 190) || "artifact"}${expectedExtension}`;
1846
+ }
1847
+ function plainSha256(value) {
1848
+ const match = /^sha256:([0-9a-f]{64})$/u.exec(value);
1849
+ if (!match) throw new Error("Materialized artifact content hash is invalid");
1850
+ return match[1];
1851
+ }
1852
+ function deterministicUuid(seed) {
1853
+ const bytes = createHash3("sha256").update(seed, "utf8").digest().subarray(0, 16);
1854
+ bytes[6] = (bytes[6] ?? 0) & 15 | 80;
1855
+ bytes[8] = (bytes[8] ?? 0) & 63 | 128;
1856
+ const hex = bytes.toString("hex");
1857
+ const value = `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
1858
+ if (!UUID_PATTERN.test(value)) throw new Error("Generated workspace file identity is invalid");
1859
+ return value;
1860
+ }
1861
+ function throwIfAborted3(signal) {
1862
+ if (signal?.aborted) throw signal.reason ?? new Error("Artifact export aborted");
1863
+ }
1864
+
1865
+ // src/editable-artifact-production.ts
1866
+ var INVALIDATION_POLL_MS = 1e3;
1867
+ var TICKET_CLEANUP_MS = 3e4;
1868
+ async function createStandaloneEditableArtifactApplication(input) {
1869
+ const boundedObjects = createObjectStorageBoundedPorts(input.objectStorage);
1870
+ const materializationObjects = createObjectStorageBoundedPorts(input.objectStorage, {
1871
+ keyPrefix: "editable-artifacts/materializations/v1/sha256/"
1872
+ });
1873
+ const importSourceObjects = createObjectStorageBoundedPorts(input.objectStorage, {
1874
+ keyPrefix: "editable-artifacts/imports/v1/sha256/"
1875
+ });
1876
+ const runtimeBinding = await loadVerifiedNativeArtifactRuntimeBinding();
1877
+ const runtime = runtimeBinding.runtime;
1878
+ const kernel = new NativeEditableArtifactKernelAdapter(runtime, boundedObjects.read);
1879
+ const persistedStore = new PostgresEditableArtifactStore(input.db);
1880
+ const store = editableArtifactStorePortFromPostgres(persistedStore);
1881
+ const authorization = new PostgresEditableArtifactAuthorization(input.db);
1882
+ const ids = new RandomEditableArtifactStableIds();
1883
+ const clock = new SystemEditableArtifactClock();
1884
+ const snapshotVerifier = new ProductionEditableArtifactSnapshotVerifier({
1885
+ objects: boundedObjects.read,
1886
+ kernel
1887
+ });
1888
+ const genesis = new EditableArtifactGenesisPipeline({
1889
+ kernel,
1890
+ objects: boundedObjects.write,
1891
+ now: () => clock.now()
1892
+ });
1893
+ const compaction = new EditableArtifactCompactionPipeline({
1894
+ kernel,
1895
+ objects: boundedObjects.write,
1896
+ now: () => clock.now()
1897
+ });
1898
+ const domain = new EditableArtifactService({
1899
+ authorization,
1900
+ kernel,
1901
+ store,
1902
+ intentCodec: ogatxEditableArtifactMutationIntentCodec,
1903
+ clock,
1904
+ ids,
1905
+ snapshotVerifier,
1906
+ genesis,
1907
+ compaction
1908
+ });
1909
+ const materializerCapabilities = editableArtifactMaterializerCapabilitiesForRuntime(
1910
+ runtime,
1911
+ runtimeBinding.location.artifactTool.packageVersion
1912
+ );
1913
+ const durableExports = new EditableArtifactDurableExportService({
1914
+ authorization,
1915
+ exactSnapshots: Object.freeze({
1916
+ ensure: async (request) => await domain.compactCurrentHead(request)
1917
+ }),
1918
+ store: editableArtifactDurableExportStorePortFromPostgres(
1919
+ new PostgresEditableArtifactDurableExportStore(input.db)
1920
+ ),
1921
+ ids: new RandomEditableArtifactDurableExportIds(),
1922
+ profiles: materializationProfiles(materializerCapabilities),
1923
+ materializationObjects: materializationObjects.read
1924
+ });
1925
+ const officeImports = new EditableArtifactOfficeImportAdapter({
1926
+ db: input.db,
1927
+ objectStorage: input.objectStorage,
1928
+ runtime: runtimeBinding,
1929
+ sourceObjects: importSourceObjects.write,
1930
+ snapshotObjects: boundedObjects.write
1931
+ });
1932
+ const agent = new EditableArtifactAgentApplication({
1933
+ domain,
1934
+ exports: durableExports,
1935
+ associations: Object.freeze({
1936
+ listArtifactIds: async ({ scope, sessionId, limit }) => (await listEditableArtifactIdsForSession(input.db, scope, sessionId, limit)).map(
1937
+ editableArtifactId
1938
+ ),
1939
+ touch: async ({ scope, sessionId, artifactId }) => await touchEditableArtifactSessionLink(input.db, scope, sessionId, artifactId)
1940
+ }),
1941
+ inspector: kernel,
1942
+ officeImports,
1943
+ workspaceFiles: new EditableArtifactWorkspaceFileAdapter({
1944
+ db: input.db,
1945
+ objectStorage: input.objectStorage,
1946
+ durableExports
1947
+ })
1948
+ });
1949
+ const ticketStore = new PostgresEditableArtifactLiveTicketStore(input.db);
1950
+ const liveRead = new PostgresEditableArtifactLiveReadStore(input.db, {
1951
+ snapshotBytes: snapshotBytesPort(boundedObjects.read)
1952
+ });
1953
+ const invalidations = new PollingEditableArtifactAuthorizationInvalidations(authorization);
1954
+ const live = new EditableArtifactLiveServer({
1955
+ authorization,
1956
+ domain,
1957
+ tickets: editableArtifactLiveTicketStorePortFromPostgres(ticketStore),
1958
+ tokens: new WebCryptoEditableArtifactLiveTokens(),
1959
+ clock: new SystemEditableArtifactLiveClock(),
1960
+ scheduler: new SystemEditableArtifactLiveScheduler(),
1961
+ read: editableArtifactLiveReadPortFromPostgres(liveRead),
1962
+ hints: new EventBusEditableArtifactLiveHints(input.bus),
1963
+ invalidations
1964
+ });
1965
+ const compatibility = Object.freeze({
1966
+ assertCompatible(request) {
1967
+ if (request.protocolVersion !== EDITABLE_ARTIFACT_LIVE_PROTOCOL_VERSION || request.modelSchemaVersion !== 1 || request.kernelVersion !== runtime.buildIdentity) {
1968
+ throw new EditableArtifactCompatibilityError();
1969
+ }
1970
+ }
1971
+ });
1972
+ const application = new EditableArtifactApplication({
1973
+ domain,
1974
+ live,
1975
+ compatibility
1976
+ });
1977
+ const cleanup = setInterval(() => {
1978
+ void ticketStore.cleanupExpired().catch(() => void 0);
1979
+ }, TICKET_CLEANUP_MS);
1980
+ cleanup.unref?.();
1981
+ return Object.freeze({
1982
+ application,
1983
+ durableExports,
1984
+ agent,
1985
+ officeImports,
1986
+ kernelVersion: runtime.buildIdentity,
1987
+ close() {
1988
+ clearInterval(cleanup);
1989
+ invalidations.close();
1990
+ }
1991
+ });
1992
+ }
1993
+ var PostgresEditableArtifactAuthorization = class {
1994
+ constructor(db) {
1995
+ this.db = db;
1996
+ }
1997
+ async authorize(request) {
1998
+ const actor = request.actor;
1999
+ const rows = await withRlsContext(
2000
+ this.db,
2001
+ request.scope,
2002
+ async (tx) => rawRows(
2003
+ await tx.execute(dbSql`select *
2004
+ from opengeni_private.authorize_editable_artifact_actor(
2005
+ ${request.scope.accountId}::uuid,
2006
+ ${request.scope.workspaceId}::uuid,
2007
+ ${request.artifactId},
2008
+ ${actor.kind},
2009
+ ${actor.subjectId},
2010
+ ${actor.kind === "agent" ? actor.sessionId : null},
2011
+ ${actor.kind === "agent" ? actor.turnId : null},
2012
+ ${actor.kind === "agent" ? actor.attemptId : null},
2013
+ ${actor.kind === "agent" ? actor.generation : null},
2014
+ ${actor.kind === "service" ? actor.service : null},
2015
+ ${request.permission},
2016
+ current_schema()
2017
+ )`)
2018
+ )
2019
+ );
2020
+ if (rows.length !== 1) throw new Error("Editable artifact authorization returned no decision");
2021
+ const revision = Number(rows[0].authorization_revision);
2022
+ if (!Number.isSafeInteger(revision) || revision < 1) {
2023
+ throw new Error("Editable artifact authorization returned an invalid revision");
2024
+ }
2025
+ return Object.freeze({ allowed: rows[0].allowed === true, revision });
2026
+ }
2027
+ };
2028
+ var PollingEditableArtifactAuthorizationInvalidations = class {
2029
+ constructor(authorization) {
2030
+ this.authorization = authorization;
2031
+ }
2032
+ stops = /* @__PURE__ */ new Set();
2033
+ async subscribe(input) {
2034
+ let active = true;
2035
+ let polling = false;
2036
+ let baseline = await this.read(input);
2037
+ const timer = setInterval(() => {
2038
+ if (!active || polling) return;
2039
+ polling = true;
2040
+ void this.read(input).then((next) => {
2041
+ if (!active) return;
2042
+ if (next !== baseline) {
2043
+ baseline = next;
2044
+ input.onInvalidated();
2045
+ }
2046
+ }).catch(() => {
2047
+ if (active) input.onInvalidated();
2048
+ }).finally(() => {
2049
+ polling = false;
2050
+ });
2051
+ }, INVALIDATION_POLL_MS);
2052
+ timer.unref?.();
2053
+ const stop = () => {
2054
+ if (!active) return;
2055
+ active = false;
2056
+ clearInterval(timer);
2057
+ this.stops.delete(stop);
2058
+ };
2059
+ this.stops.add(stop);
2060
+ return stop;
2061
+ }
2062
+ close() {
2063
+ for (const stop of [...this.stops]) stop();
2064
+ }
2065
+ async read(input) {
2066
+ const decision = await this.authorization.authorize({
2067
+ scope: input.scope,
2068
+ artifactId: input.artifactId,
2069
+ actor: input.actor,
2070
+ permission: "read"
2071
+ });
2072
+ return `${decision.allowed ? 1 : 0}:${decision.revision}`;
2073
+ }
2074
+ };
2075
+ var RandomEditableArtifactStableIds = class {
2076
+ next(_kind) {
2077
+ return randomUUID().replaceAll("-", "");
2078
+ }
2079
+ };
2080
+ var RandomEditableArtifactDurableExportIds = class {
2081
+ next(_kind) {
2082
+ return randomUUID().replaceAll("-", "");
2083
+ }
2084
+ };
2085
+ function materializationProfiles(capabilities) {
2086
+ if (capabilities.protocol !== "OGAMC001" || capabilities.runtimeKind !== "native" || capabilities.maxOutputBytes !== EDITABLE_ARTIFACT_EXPORT_MAX_DOWNLOAD_BYTES || capabilities.codecVersions["opengeni.xlsx"] === void 0 || !capabilities.supportedModelSchemaVersions.includes(1) || !capabilities.supportedOperationProtocolVersions.includes(1) || !capabilities.supportedSnapshotProtocolVersions.includes(1)) {
2087
+ throw new Error("Verified artifact materializer profile is incompatible");
2088
+ }
2089
+ const codecVersion = capabilities.codecVersions["opengeni.xlsx"];
2090
+ return Object.freeze({
2091
+ async resolve(input) {
2092
+ if (input.modality !== "spreadsheet" || input.format !== "xlsx" || Object.keys(input.options).length !== 0) {
2093
+ return null;
2094
+ }
2095
+ return Object.freeze({
2096
+ modality: "spreadsheet",
2097
+ format: "xlsx",
2098
+ codecId: "opengeni.xlsx",
2099
+ codecVersion,
2100
+ kernelVersion: capabilities.kernelVersion,
2101
+ fontRegistryHash: capabilities.fontRegistryHash,
2102
+ policyHash: capabilities.policyHash,
2103
+ normalizedOptions: "{}"
2104
+ });
2105
+ }
2106
+ });
2107
+ }
2108
+ function snapshotBytesPort(objects) {
2109
+ return Object.freeze({
2110
+ async readSnapshotBytes(snapshot) {
2111
+ const object = await objects.open({
2112
+ opaqueReference: snapshot.blobReference,
2113
+ maxBytes: MAX_EDITABLE_ARTIFACT_SNAPSHOT_BYTES3,
2114
+ expectedByteSize: snapshot.byteSize
2115
+ });
2116
+ try {
2117
+ if (object.contentType !== void 0 && object.contentType !== snapshot.mimeType) {
2118
+ throw new Error("Editable artifact snapshot content type mismatch");
2119
+ }
2120
+ const bytes = new Uint8Array(snapshot.byteSize);
2121
+ const digest = createHash4("sha256");
2122
+ let offset = 0;
2123
+ for await (const chunk of object.chunks({
2124
+ chunkBytes: MAX_BOUNDED_OBJECT_CHUNK_BYTES2
2125
+ })) {
2126
+ if (offset + chunk.byteLength > bytes.byteLength) {
2127
+ throw new Error("Editable artifact snapshot exceeds its declared size");
2128
+ }
2129
+ bytes.set(chunk, offset);
2130
+ digest.update(chunk);
2131
+ offset += chunk.byteLength;
2132
+ }
2133
+ if (offset !== bytes.byteLength || `sha256:${digest.digest("hex")}` !== snapshot.contentHash) {
2134
+ throw new Error("Editable artifact snapshot digest or size mismatch");
2135
+ }
2136
+ await object.assertUnchanged();
2137
+ return bytes;
2138
+ } finally {
2139
+ await object.close();
2140
+ }
2141
+ }
2142
+ });
2143
+ }
2144
+ function rawRows(result) {
2145
+ if (Array.isArray(result)) return result;
2146
+ const rows = result?.rows;
2147
+ if (Array.isArray(rows)) return rows;
2148
+ throw new Error("Unsupported database execute result shape");
2149
+ }
2150
+
461
2151
  // src/index.ts
462
2152
  function isWorkflowAlreadyStarted(error) {
463
2153
  return error instanceof WorkflowExecutionAlreadyStartedError;
@@ -562,6 +2252,14 @@ async function createTemporalWorkflowClient(settings, db) {
562
2252
  },
563
2253
  syncScheduledTask: async ({ task }) => {
564
2254
  const schedule = temporal.schedule.getHandle(task.temporalScheduleId);
2255
+ if (task.schedule.type === "manual") {
2256
+ try {
2257
+ await schedule.delete();
2258
+ } catch (error) {
2259
+ if (!(error instanceof ScheduleNotFoundError)) throw error;
2260
+ }
2261
+ return;
2262
+ }
565
2263
  const options = temporalScheduleOptions(task, settings.temporalTaskQueue);
566
2264
  try {
567
2265
  await schedule.update(() => temporalScheduleUpdateOptions(options));
@@ -573,13 +2271,21 @@ async function createTemporalWorkflowClient(settings, db) {
573
2271
  }
574
2272
  },
575
2273
  deleteScheduledTaskSchedule: async ({ temporalScheduleId }) => {
576
- await temporal.schedule.getHandle(temporalScheduleId).delete().catch(() => void 0);
2274
+ try {
2275
+ await temporal.withDeadline(Date.now() + 5e3, async () => {
2276
+ await temporal.schedule.getHandle(temporalScheduleId).delete();
2277
+ });
2278
+ } catch (error) {
2279
+ if (error instanceof ScheduleNotFoundError) return;
2280
+ throw error;
2281
+ }
577
2282
  },
578
2283
  triggerScheduledTask: async ({
579
2284
  task,
580
2285
  agentRunUsageIdempotencyKey,
581
2286
  triggerWorkflowId,
582
- initiator
2287
+ initiator,
2288
+ triggerType = "manual"
583
2289
  }) => {
584
2290
  const workflowId = triggerWorkflowId;
585
2291
  try {
@@ -592,7 +2298,7 @@ async function createTemporalWorkflowClient(settings, db) {
592
2298
  accountId: task.accountId,
593
2299
  workspaceId: task.workspaceId,
594
2300
  taskId: task.id,
595
- triggerType: "manual",
2301
+ triggerType,
596
2302
  agentRunUsageIdempotencyKey,
597
2303
  initiator
598
2304
  }
@@ -610,18 +2316,23 @@ async function createTemporalWorkflowClient(settings, db) {
610
2316
  if (!targetId) {
611
2317
  throw new Error("rig verification requires changeId or versionId");
612
2318
  }
613
- await temporal.workflow.start("rigVerificationWorkflow", {
614
- taskQueue: settings.temporalTaskQueue,
615
- workflowId: workflowId ?? `rig-verification-${targetId}-${crypto.randomUUID()}`,
616
- workflowIdReusePolicy: "ALLOW_DUPLICATE",
617
- args: [
618
- {
619
- workspaceId,
620
- ...changeId ? { changeId } : {},
621
- ...versionId ? { versionId } : {}
622
- }
623
- ]
624
- });
2319
+ try {
2320
+ await temporal.workflow.start("rigVerificationWorkflow", {
2321
+ taskQueue: settings.temporalTaskQueue,
2322
+ workflowId: workflowId ?? `rig-verification-${targetId}-${crypto.randomUUID()}`,
2323
+ workflowIdReusePolicy: "REJECT_DUPLICATE",
2324
+ args: [
2325
+ {
2326
+ workspaceId,
2327
+ ...changeId ? { changeId } : {},
2328
+ ...versionId ? { versionId } : {}
2329
+ }
2330
+ ]
2331
+ });
2332
+ } catch (error) {
2333
+ if (isWorkflowAlreadyStarted(error)) return;
2334
+ throw error;
2335
+ }
625
2336
  },
626
2337
  check: async () => {
627
2338
  await connection.workflowService.getSystemInfo({});
@@ -698,24 +2409,73 @@ async function startApi() {
698
2409
  await dbClient.close();
699
2410
  throw new Error("OpenGeni API startup dependencies were not initialized");
700
2411
  }
2412
+ const objectStorage = createObjectStorage(settings);
2413
+ let editableArtifactComposition;
2414
+ if (objectStorage && isArtifactRuntimeConfigured()) {
2415
+ try {
2416
+ editableArtifactComposition = await createStandaloneEditableArtifactApplication({
2417
+ db: dbClient.db,
2418
+ bus,
2419
+ objectStorage
2420
+ });
2421
+ } catch (error) {
2422
+ await Promise.allSettled([bus.close(), workflowClient.close(), dbClient.close()]);
2423
+ throw error;
2424
+ }
2425
+ }
701
2426
  const { app, routeDeps } = createAppComposition({
702
2427
  settings,
703
2428
  db: dbClient.db,
704
2429
  bus,
705
2430
  workflowClient: workflowClient.client,
706
2431
  documentIndexer: workflowClient.documentIndexer,
2432
+ objectStorage,
2433
+ ...editableArtifactComposition ? {
2434
+ editableArtifacts: editableArtifactComposition.application,
2435
+ editableArtifactExports: editableArtifactComposition.durableExports,
2436
+ editableArtifactAgent: editableArtifactComposition.agent,
2437
+ editableArtifactOfficeImports: editableArtifactComposition.officeImports
2438
+ } : {},
707
2439
  observability,
708
2440
  readinessChecks: {
709
2441
  db: runtimeDatabaseReadyCheck(dbClient.db, databasePosture)
710
2442
  }
711
2443
  });
2444
+ if (!routeDeps.editableArtifacts) {
2445
+ observability.warn("editable artifact engine is not composed", {
2446
+ subsystem: "editable_artifacts",
2447
+ behavior: "http_and_websocket_fail_closed"
2448
+ });
2449
+ }
2450
+ const artifactWebSockets = new EditableArtifactWebSocketTransport(routeDeps.editableArtifacts);
712
2451
  const server = Bun.serve({
713
2452
  hostname: settings.apiHost,
714
2453
  port: settings.apiPort,
715
2454
  idleTimeout: 255,
716
- fetch: app.fetch
2455
+ fetch: (request, bunServer) => {
2456
+ if (artifactWebSockets.handles(request)) {
2457
+ return artifactWebSockets.upgrade(request, bunServer);
2458
+ }
2459
+ return app.fetch(request);
2460
+ },
2461
+ websocket: {
2462
+ maxPayloadLength: EDITABLE_ARTIFACT_LIVE_WEBSOCKET_MAX_MESSAGE_BYTES,
2463
+ backpressureLimit: 16 * 1024 * 1024,
2464
+ closeOnBackpressureLimit: true,
2465
+ open: (socket) => artifactWebSockets.websocket.open(socket),
2466
+ message: (socket, message) => artifactWebSockets.websocket.message(socket, message),
2467
+ close: (socket) => artifactWebSockets.websocket.close(socket)
2468
+ }
717
2469
  });
718
2470
  const stopSlackInteractionPump = settings.slackSigningSecret ? startSlackInteractionPump(routeDeps) : void 0;
2471
+ const stopMemorySlackPublicationPump = startMemorySlackPublicationPump(routeDeps);
2472
+ const stopTemporalScheduleCleanupPump = startTemporalScheduleCleanupPump({
2473
+ db: dbClient.db,
2474
+ deleteSchedule: async (temporalScheduleId) => {
2475
+ await workflowClient.client.deleteScheduledTaskSchedule({ temporalScheduleId });
2476
+ },
2477
+ observability
2478
+ });
719
2479
  let stopMetricsIngestion;
720
2480
  let stopHelloIngestion;
721
2481
  let authCalloutResponder;
@@ -761,9 +2521,12 @@ async function startApi() {
761
2521
  close: async () => {
762
2522
  server.stop(true);
763
2523
  stopSlackInteractionPump?.();
2524
+ await stopMemorySlackPublicationPump();
764
2525
  stopMetricsIngestion?.();
765
2526
  stopHelloIngestion?.();
2527
+ await stopTemporalScheduleCleanupPump();
766
2528
  await Promise.allSettled([
2529
+ Promise.resolve(editableArtifactComposition?.close()),
767
2530
  authCalloutResponder?.close(),
768
2531
  bus.close(),
769
2532
  workflowClient.close(),
@@ -788,6 +2551,9 @@ function shouldCreateScheduleAfterUpdateError(error) {
788
2551
  return error instanceof ScheduleNotFoundError;
789
2552
  }
790
2553
  function temporalScheduleSpec(schedule) {
2554
+ if (schedule.type === "manual") {
2555
+ throw new Error("manual scheduled tasks do not have a Temporal Schedule spec");
2556
+ }
791
2557
  if (schedule.type === "interval") {
792
2558
  return {
793
2559
  intervals: [temporalIntervalSpec(schedule)],
@@ -862,7 +2628,7 @@ function temporalScheduleOptions(task, taskQueue) {
862
2628
  pauseOnFailure: false
863
2629
  },
864
2630
  state: {
865
- paused: task.status === "paused",
2631
+ paused: scheduledTaskEffectivelyPaused(task),
866
2632
  ...task.schedule.type === "once" ? { remainingActions: 1 } : {}
867
2633
  },
868
2634
  memo: {
@@ -873,6 +2639,14 @@ function temporalScheduleOptions(task, taskQueue) {
873
2639
  }
874
2640
  };
875
2641
  }
2642
+ function scheduledTaskEffectivelyPaused(task) {
2643
+ if (task.status === "paused") return true;
2644
+ if (task.action.kind !== "knowledge_source_sync") return false;
2645
+ const control = task.metadata.knowledgeSourceSync;
2646
+ if (!control || typeof control !== "object") return false;
2647
+ const record = control;
2648
+ return record.sourceEnabled === false || record.connectionPaused === true;
2649
+ }
876
2650
  function temporalScheduleUpdateOptions(options) {
877
2651
  return {
878
2652
  spec: options.spec,