@opengeni/db 0.16.2 → 0.17.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/src/index.ts CHANGED
@@ -1190,6 +1190,8 @@ export const allWorkspacePermissions: Permission[] = [
1190
1190
  "goals:manage",
1191
1191
  "enrollments:read",
1192
1192
  "enrollments:manage",
1193
+ "artifacts:read",
1194
+ "artifacts:publish",
1193
1195
  ];
1194
1196
 
1195
1197
  export const allAccountPermissions: Permission[] = [
@@ -5297,6 +5299,881 @@ export async function revokeConnection(
5297
5299
  );
5298
5300
  }
5299
5301
 
5302
+ export type SlackInstallationRoute = {
5303
+ accountId: string;
5304
+ workspaceId: string;
5305
+ connectionId: string;
5306
+ botId: string;
5307
+ botUserId: string;
5308
+ };
5309
+
5310
+ export type SlackBotUserLink = {
5311
+ id: string;
5312
+ accountId: string;
5313
+ workspaceId: string;
5314
+ connectionId: string;
5315
+ slackTeamId: string;
5316
+ slackUserId: string;
5317
+ subjectId: string;
5318
+ linkedBySubjectId: string;
5319
+ createdAt: Date;
5320
+ updatedAt: Date;
5321
+ };
5322
+
5323
+ export type SlackInteractionTriggerKind =
5324
+ | "app_mention"
5325
+ | "dm"
5326
+ | "slash_command"
5327
+ | "message_shortcut"
5328
+ | "thread_reply";
5329
+
5330
+ export type SlackInteractionInboxEntry = {
5331
+ id: string;
5332
+ accountId: string;
5333
+ workspaceId: string;
5334
+ connectionId: string;
5335
+ providerEventId: string;
5336
+ providerMessageId: string;
5337
+ slackTeamId: string;
5338
+ slackUserId: string;
5339
+ slackChannelId: string;
5340
+ slackMessageTs: string;
5341
+ slackThreadTs: string | null;
5342
+ triggerKind: SlackInteractionTriggerKind;
5343
+ text: string;
5344
+ status: "pending" | "processing" | "processed" | "failed";
5345
+ claimHolderId: string | null;
5346
+ claimExpiresAt: Date | null;
5347
+ attemptCount: number;
5348
+ lastErrorCode: string | null;
5349
+ processedAt: Date | null;
5350
+ createdAt: Date;
5351
+ updatedAt: Date;
5352
+ };
5353
+
5354
+ export type SlackInteraction = {
5355
+ id: string;
5356
+ accountId: string;
5357
+ workspaceId: string;
5358
+ connectionId: string;
5359
+ slackTeamId: string;
5360
+ slackChannelId: string;
5361
+ slackThreadTs: string;
5362
+ routeKey: string;
5363
+ triggeringProviderEventId: string;
5364
+ owningSubjectId: string;
5365
+ visibility: "private" | "workspace";
5366
+ sessionReservationId: string;
5367
+ sessionId: string | null;
5368
+ lastDeliveredSessionEventSequence: number;
5369
+ deliveryClaimHolderId: string | null;
5370
+ deliveryClaimExpiresAt: Date | null;
5371
+ ackSlackMessageTs: string | null;
5372
+ progressCount: number;
5373
+ terminalDeliveryState: "open" | "completed" | "failed" | "cancelled" | "blocked";
5374
+ createdAt: Date;
5375
+ updatedAt: Date;
5376
+ };
5377
+
5378
+ export type SlackInteractionProgressDelivery = {
5379
+ id: string;
5380
+ accountId: string;
5381
+ workspaceId: string;
5382
+ interactionId: string;
5383
+ sessionEventSequence: number;
5384
+ slot: number;
5385
+ operationId: string;
5386
+ createdAt: Date;
5387
+ };
5388
+
5389
+ export async function resolveSlackInstallationRoute(
5390
+ db: Database,
5391
+ slackTeamId: string,
5392
+ ): Promise<SlackInstallationRoute | null> {
5393
+ const rows = await db.execute<{
5394
+ account_id: string;
5395
+ workspace_id: string;
5396
+ connection_id: string;
5397
+ bot_id: string;
5398
+ bot_user_id: string;
5399
+ }>(sql`select * from opengeni_private.resolve_slack_installation(${slackTeamId})`);
5400
+ const row = rows[0];
5401
+ return row
5402
+ ? {
5403
+ accountId: row.account_id,
5404
+ workspaceId: row.workspace_id,
5405
+ connectionId: row.connection_id,
5406
+ botId: row.bot_id,
5407
+ botUserId: row.bot_user_id,
5408
+ }
5409
+ : null;
5410
+ }
5411
+
5412
+ export async function saveSlackBotUserLink(
5413
+ db: Database,
5414
+ input: Omit<SlackBotUserLink, "id" | "createdAt" | "updatedAt">,
5415
+ ): Promise<SlackBotUserLink> {
5416
+ return await withWorkspaceRls(db, input.workspaceId, async (scopedDb) => {
5417
+ const [row] = await scopedDb
5418
+ .insert(schema.slackBotUserLinks)
5419
+ .values(input)
5420
+ .onConflictDoUpdate({
5421
+ target: [schema.slackBotUserLinks.connectionId, schema.slackBotUserLinks.slackUserId],
5422
+ set: {
5423
+ accountId: input.accountId,
5424
+ workspaceId: input.workspaceId,
5425
+ slackTeamId: input.slackTeamId,
5426
+ subjectId: input.subjectId,
5427
+ linkedBySubjectId: input.linkedBySubjectId,
5428
+ updatedAt: sql`now()`,
5429
+ },
5430
+ })
5431
+ .returning();
5432
+ if (!row) throw new Error("Slack identity link write returned no row");
5433
+ return mapSlackBotUserLink(row);
5434
+ });
5435
+ }
5436
+
5437
+ export async function getSlackBotUserLink(
5438
+ db: Database,
5439
+ workspaceId: string,
5440
+ connectionId: string,
5441
+ slackUserId: string,
5442
+ ): Promise<SlackBotUserLink | null> {
5443
+ return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
5444
+ const [row] = await scopedDb
5445
+ .select()
5446
+ .from(schema.slackBotUserLinks)
5447
+ .where(
5448
+ and(
5449
+ eq(schema.slackBotUserLinks.workspaceId, workspaceId),
5450
+ eq(schema.slackBotUserLinks.connectionId, connectionId),
5451
+ eq(schema.slackBotUserLinks.slackUserId, slackUserId),
5452
+ ),
5453
+ )
5454
+ .limit(1);
5455
+ return row ? mapSlackBotUserLink(row) : null;
5456
+ });
5457
+ }
5458
+
5459
+ export async function deleteSlackBotUserLink(
5460
+ db: Database,
5461
+ workspaceId: string,
5462
+ connectionId: string,
5463
+ slackUserId: string,
5464
+ ): Promise<boolean> {
5465
+ return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
5466
+ const rows = await scopedDb
5467
+ .delete(schema.slackBotUserLinks)
5468
+ .where(
5469
+ and(
5470
+ eq(schema.slackBotUserLinks.workspaceId, workspaceId),
5471
+ eq(schema.slackBotUserLinks.connectionId, connectionId),
5472
+ eq(schema.slackBotUserLinks.slackUserId, slackUserId),
5473
+ ),
5474
+ )
5475
+ .returning({ id: schema.slackBotUserLinks.id });
5476
+ return rows.length > 0;
5477
+ });
5478
+ }
5479
+
5480
+ export async function enqueueSlackInteractionInbox(
5481
+ db: Database,
5482
+ input: Omit<
5483
+ SlackInteractionInboxEntry,
5484
+ | "id"
5485
+ | "status"
5486
+ | "claimHolderId"
5487
+ | "claimExpiresAt"
5488
+ | "attemptCount"
5489
+ | "lastErrorCode"
5490
+ | "processedAt"
5491
+ | "createdAt"
5492
+ | "updatedAt"
5493
+ >,
5494
+ ): Promise<{ inserted: boolean; entry: SlackInteractionInboxEntry }> {
5495
+ return await withRlsContext(
5496
+ db,
5497
+ { accountId: input.accountId, workspaceId: input.workspaceId },
5498
+ async (scopedDb) => {
5499
+ const [created] = await scopedDb
5500
+ .insert(schema.slackInteractionInbox)
5501
+ .values(input)
5502
+ .onConflictDoNothing()
5503
+ .returning();
5504
+ if (created) return { inserted: true, entry: mapSlackInteractionInbox(created) };
5505
+ const [existing] = await scopedDb
5506
+ .select()
5507
+ .from(schema.slackInteractionInbox)
5508
+ .where(
5509
+ and(
5510
+ eq(schema.slackInteractionInbox.connectionId, input.connectionId),
5511
+ or(
5512
+ eq(schema.slackInteractionInbox.providerEventId, input.providerEventId),
5513
+ eq(schema.slackInteractionInbox.providerMessageId, input.providerMessageId),
5514
+ ),
5515
+ ),
5516
+ )
5517
+ .limit(1);
5518
+ if (!existing) throw new Error("Slack inbox idempotency conflict could not be resolved");
5519
+ return { inserted: false, entry: mapSlackInteractionInbox(existing) };
5520
+ },
5521
+ );
5522
+ }
5523
+
5524
+ export async function claimSlackInteractionInbox(
5525
+ db: Database,
5526
+ claimHolderId: string,
5527
+ claimLeaseMs: number,
5528
+ ): Promise<SlackInteractionInboxEntry | null> {
5529
+ const rows = await db.execute<typeof schema.slackInteractionInbox.$inferSelect>(
5530
+ sql`select * from opengeni_private.claim_slack_interaction_inbox(${claimHolderId}::uuid, ${claimLeaseMs})`,
5531
+ );
5532
+ return rows[0] ? mapSlackInteractionInbox(rows[0]) : null;
5533
+ }
5534
+
5535
+ export async function settleSlackInteractionInbox(
5536
+ db: Database,
5537
+ input: {
5538
+ entry: Pick<SlackInteractionInboxEntry, "id" | "accountId" | "workspaceId">;
5539
+ claimHolderId: string;
5540
+ outcome: "processed" | "failed";
5541
+ errorCode?: string | null;
5542
+ },
5543
+ ): Promise<boolean> {
5544
+ return await withRlsContext(db, input.entry, async (scopedDb) => {
5545
+ const rows = await scopedDb
5546
+ .update(schema.slackInteractionInbox)
5547
+ .set({
5548
+ status: input.outcome,
5549
+ claimHolderId: null,
5550
+ claimExpiresAt: null,
5551
+ processedAt: sql`now()`,
5552
+ lastErrorCode: input.errorCode ?? null,
5553
+ updatedAt: sql`now()`,
5554
+ })
5555
+ .where(
5556
+ and(
5557
+ eq(schema.slackInteractionInbox.id, input.entry.id),
5558
+ eq(schema.slackInteractionInbox.claimHolderId, input.claimHolderId),
5559
+ eq(schema.slackInteractionInbox.status, "processing"),
5560
+ ),
5561
+ )
5562
+ .returning({ id: schema.slackInteractionInbox.id });
5563
+ return rows.length === 1;
5564
+ });
5565
+ }
5566
+
5567
+ export async function releaseSlackInteractionInbox(
5568
+ db: Database,
5569
+ input: {
5570
+ entry: Pick<SlackInteractionInboxEntry, "id" | "accountId" | "workspaceId">;
5571
+ claimHolderId: string;
5572
+ errorCode: string;
5573
+ },
5574
+ ): Promise<boolean> {
5575
+ return await withRlsContext(db, input.entry, async (scopedDb) => {
5576
+ const rows = await scopedDb
5577
+ .update(schema.slackInteractionInbox)
5578
+ .set({
5579
+ status: "pending",
5580
+ claimHolderId: null,
5581
+ claimExpiresAt: null,
5582
+ lastErrorCode: input.errorCode,
5583
+ updatedAt: sql`now()`,
5584
+ })
5585
+ .where(
5586
+ and(
5587
+ eq(schema.slackInteractionInbox.id, input.entry.id),
5588
+ eq(schema.slackInteractionInbox.claimHolderId, input.claimHolderId),
5589
+ eq(schema.slackInteractionInbox.status, "processing"),
5590
+ ),
5591
+ )
5592
+ .returning({ id: schema.slackInteractionInbox.id });
5593
+ return rows.length === 1;
5594
+ });
5595
+ }
5596
+
5597
+ export async function getOrCreateSlackInteraction(
5598
+ db: Database,
5599
+ input: Omit<
5600
+ SlackInteraction,
5601
+ | "id"
5602
+ | "sessionReservationId"
5603
+ | "sessionId"
5604
+ | "lastDeliveredSessionEventSequence"
5605
+ | "deliveryClaimHolderId"
5606
+ | "deliveryClaimExpiresAt"
5607
+ | "ackSlackMessageTs"
5608
+ | "progressCount"
5609
+ | "terminalDeliveryState"
5610
+ | "createdAt"
5611
+ | "updatedAt"
5612
+ >,
5613
+ ): Promise<{ created: boolean; interaction: SlackInteraction }> {
5614
+ return await withRlsContext(db, input, async (scopedDb) => {
5615
+ const [created] = await scopedDb
5616
+ .insert(schema.slackInteractions)
5617
+ .values(input)
5618
+ .onConflictDoNothing()
5619
+ .returning();
5620
+ if (created) return { created: true, interaction: mapSlackInteraction(created) };
5621
+ const [existing] = await scopedDb
5622
+ .select()
5623
+ .from(schema.slackInteractions)
5624
+ .where(
5625
+ and(
5626
+ eq(schema.slackInteractions.connectionId, input.connectionId),
5627
+ eq(schema.slackInteractions.routeKey, input.routeKey),
5628
+ ),
5629
+ )
5630
+ .limit(1);
5631
+ if (!existing) throw new Error("Slack route idempotency conflict could not be resolved");
5632
+ return { created: false, interaction: mapSlackInteraction(existing) };
5633
+ });
5634
+ }
5635
+
5636
+ export async function getSlackInteractionByRoute(
5637
+ db: Database,
5638
+ workspaceId: string,
5639
+ connectionId: string,
5640
+ routeKey: string,
5641
+ ): Promise<SlackInteraction | null> {
5642
+ return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
5643
+ const [row] = await scopedDb
5644
+ .select()
5645
+ .from(schema.slackInteractions)
5646
+ .where(
5647
+ and(
5648
+ eq(schema.slackInteractions.workspaceId, workspaceId),
5649
+ eq(schema.slackInteractions.connectionId, connectionId),
5650
+ eq(schema.slackInteractions.routeKey, routeKey),
5651
+ ),
5652
+ )
5653
+ .limit(1);
5654
+ return row ? mapSlackInteraction(row) : null;
5655
+ });
5656
+ }
5657
+
5658
+ export async function getSlackInteractionSessionAccess(
5659
+ db: Database,
5660
+ workspaceId: string,
5661
+ rootSessionId: string,
5662
+ ): Promise<Pick<SlackInteraction, "owningSubjectId" | "visibility"> | null> {
5663
+ return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
5664
+ const [row] = await scopedDb
5665
+ .select({
5666
+ owningSubjectId: schema.slackInteractions.owningSubjectId,
5667
+ visibility: schema.slackInteractions.visibility,
5668
+ })
5669
+ .from(schema.slackInteractions)
5670
+ .where(
5671
+ and(
5672
+ eq(schema.slackInteractions.workspaceId, workspaceId),
5673
+ eq(schema.slackInteractions.sessionReservationId, rootSessionId),
5674
+ ),
5675
+ )
5676
+ .limit(1);
5677
+ return row ?? null;
5678
+ });
5679
+ }
5680
+
5681
+ export async function getSlackInteractionSessionAccessForSession(
5682
+ db: Database,
5683
+ input: {
5684
+ accountId: string;
5685
+ workspaceId: string;
5686
+ sessionId: string;
5687
+ },
5688
+ ): Promise<
5689
+ (Pick<SlackInteraction, "owningSubjectId" | "visibility"> & { rootSessionId: string }) | null
5690
+ > {
5691
+ return await withRlsContext(db, input, async (scopedDb) => {
5692
+ const rows = await scopedDb.execute<{
5693
+ rootSessionId: string;
5694
+ parentSessionId: string | null;
5695
+ depth: number;
5696
+ cycle: boolean;
5697
+ owningSubjectId: string | null;
5698
+ visibility: SlackInteraction["visibility"] | null;
5699
+ }>(sql`
5700
+ with recursive lineage(id, parent_session_id, depth, path, cycle) as (
5701
+ select
5702
+ ${schema.sessions.id},
5703
+ ${schema.sessions.parentSessionId},
5704
+ 0,
5705
+ array[${schema.sessions.id}],
5706
+ false
5707
+ from ${schema.sessions}
5708
+ where ${schema.sessions.accountId} = ${input.accountId}
5709
+ and ${schema.sessions.workspaceId} = ${input.workspaceId}
5710
+ and ${schema.sessions.id} = ${input.sessionId}
5711
+ union all
5712
+ select
5713
+ parent.id,
5714
+ parent.parent_session_id,
5715
+ lineage.depth + 1,
5716
+ lineage.path || parent.id,
5717
+ parent.id = any(lineage.path)
5718
+ from ${schema.sessions} parent
5719
+ join lineage on lineage.parent_session_id = parent.id
5720
+ where parent.account_id = ${input.accountId}
5721
+ and parent.workspace_id = ${input.workspaceId}
5722
+ and not lineage.cycle
5723
+ and lineage.depth < 64
5724
+ ), root as (
5725
+ select id, parent_session_id, depth, cycle
5726
+ from lineage
5727
+ order by depth desc
5728
+ limit 1
5729
+ )
5730
+ select
5731
+ root.id as "rootSessionId",
5732
+ root.parent_session_id as "parentSessionId",
5733
+ root.depth,
5734
+ root.cycle,
5735
+ interaction.owning_subject_id as "owningSubjectId",
5736
+ interaction.visibility
5737
+ from root
5738
+ left join ${schema.slackInteractions} interaction
5739
+ on interaction.account_id = ${input.accountId}
5740
+ and interaction.workspace_id = ${input.workspaceId}
5741
+ and interaction.session_reservation_id = root.id
5742
+ `);
5743
+ const root = rows[0];
5744
+ if (!root) return null;
5745
+ if (root.cycle || root.parentSessionId !== null || Number(root.depth) >= 64) {
5746
+ throw new Error(`session lineage for ${input.sessionId} has no valid workspace root`);
5747
+ }
5748
+ if (!root.owningSubjectId || !root.visibility) return null;
5749
+ return {
5750
+ rootSessionId: root.rootSessionId,
5751
+ owningSubjectId: root.owningSubjectId,
5752
+ visibility: root.visibility,
5753
+ };
5754
+ });
5755
+ }
5756
+
5757
+ export async function bindSlackInteractionSession(
5758
+ db: Database,
5759
+ input: Pick<SlackInteraction, "id" | "accountId" | "workspaceId" | "owningSubjectId"> & {
5760
+ sessionId: string;
5761
+ },
5762
+ ): Promise<SlackInteraction | null> {
5763
+ return await withRlsContext(db, input, async (scopedDb) => {
5764
+ const [row] = await scopedDb
5765
+ .update(schema.slackInteractions)
5766
+ .set({ sessionId: input.sessionId, updatedAt: sql`now()` })
5767
+ .where(
5768
+ and(
5769
+ eq(schema.slackInteractions.id, input.id),
5770
+ eq(schema.slackInteractions.owningSubjectId, input.owningSubjectId),
5771
+ eq(schema.slackInteractions.sessionReservationId, input.sessionId),
5772
+ isNull(schema.slackInteractions.sessionId),
5773
+ ),
5774
+ )
5775
+ .returning();
5776
+ if (row) return mapSlackInteraction(row);
5777
+ const [existing] = await scopedDb
5778
+ .select()
5779
+ .from(schema.slackInteractions)
5780
+ .where(eq(schema.slackInteractions.id, input.id))
5781
+ .limit(1);
5782
+ return existing?.sessionId === input.sessionId ? mapSlackInteraction(existing) : null;
5783
+ });
5784
+ }
5785
+
5786
+ export type SlackInteractionProgressClaim =
5787
+ | {
5788
+ kind: "claimed";
5789
+ created: boolean;
5790
+ progressCount: number;
5791
+ delivery: SlackInteractionProgressDelivery;
5792
+ }
5793
+ | { kind: "limit_reached"; progressCount: number }
5794
+ | { kind: "not_owned" };
5795
+
5796
+ /**
5797
+ * Reserve one globally bounded progress slot before any Slack provider call.
5798
+ * The interaction row lock serializes replicas and expired delivery claim
5799
+ * successors; the event row preserves one operation UUID across every retry.
5800
+ */
5801
+ export async function claimSlackInteractionProgressDelivery(
5802
+ db: Database,
5803
+ input: {
5804
+ accountId: string;
5805
+ workspaceId: string;
5806
+ interactionId: string;
5807
+ claimHolderId: string;
5808
+ sessionEventSequence: number;
5809
+ maxProgress: number;
5810
+ },
5811
+ ): Promise<SlackInteractionProgressClaim> {
5812
+ if (
5813
+ !Number.isSafeInteger(input.sessionEventSequence) ||
5814
+ input.sessionEventSequence < 1 ||
5815
+ !Number.isSafeInteger(input.maxProgress) ||
5816
+ input.maxProgress < 1 ||
5817
+ input.maxProgress > 3
5818
+ ) {
5819
+ throw new RangeError("invalid Slack progress delivery claim bounds");
5820
+ }
5821
+ return await withRlsContext(db, input, async (scopedDb) => {
5822
+ const [interaction] = await scopedDb
5823
+ .select({
5824
+ progressCount: schema.slackInteractions.progressCount,
5825
+ deliveryClaimHolderId: schema.slackInteractions.deliveryClaimHolderId,
5826
+ terminalDeliveryState: schema.slackInteractions.terminalDeliveryState,
5827
+ })
5828
+ .from(schema.slackInteractions)
5829
+ .where(
5830
+ and(
5831
+ eq(schema.slackInteractions.id, input.interactionId),
5832
+ eq(schema.slackInteractions.accountId, input.accountId),
5833
+ eq(schema.slackInteractions.workspaceId, input.workspaceId),
5834
+ ),
5835
+ )
5836
+ .for("update")
5837
+ .limit(1);
5838
+ if (
5839
+ !interaction ||
5840
+ interaction.deliveryClaimHolderId !== input.claimHolderId ||
5841
+ interaction.terminalDeliveryState !== "open"
5842
+ ) {
5843
+ return { kind: "not_owned" };
5844
+ }
5845
+
5846
+ const [existing] = await scopedDb
5847
+ .select()
5848
+ .from(schema.slackInteractionProgressDeliveries)
5849
+ .where(
5850
+ and(
5851
+ eq(schema.slackInteractionProgressDeliveries.interactionId, input.interactionId),
5852
+ eq(
5853
+ schema.slackInteractionProgressDeliveries.sessionEventSequence,
5854
+ input.sessionEventSequence,
5855
+ ),
5856
+ ),
5857
+ )
5858
+ .limit(1);
5859
+ if (existing) {
5860
+ return {
5861
+ kind: "claimed",
5862
+ created: false,
5863
+ progressCount: interaction.progressCount,
5864
+ delivery: mapSlackInteractionProgressDelivery(existing),
5865
+ };
5866
+ }
5867
+ if (interaction.progressCount >= input.maxProgress) {
5868
+ return { kind: "limit_reached", progressCount: interaction.progressCount };
5869
+ }
5870
+
5871
+ const slot = interaction.progressCount + 1;
5872
+ const [delivery] = await scopedDb
5873
+ .insert(schema.slackInteractionProgressDeliveries)
5874
+ .values({
5875
+ accountId: input.accountId,
5876
+ workspaceId: input.workspaceId,
5877
+ interactionId: input.interactionId,
5878
+ sessionEventSequence: input.sessionEventSequence,
5879
+ slot,
5880
+ operationId: crypto.randomUUID(),
5881
+ })
5882
+ .returning();
5883
+ if (!delivery) throw new Error("Slack progress delivery claim returned no row");
5884
+ const [updated] = await scopedDb
5885
+ .update(schema.slackInteractions)
5886
+ .set({ progressCount: slot, updatedAt: sql`now()` })
5887
+ .where(
5888
+ and(
5889
+ eq(schema.slackInteractions.id, input.interactionId),
5890
+ eq(schema.slackInteractions.deliveryClaimHolderId, input.claimHolderId),
5891
+ eq(schema.slackInteractions.progressCount, interaction.progressCount),
5892
+ ),
5893
+ )
5894
+ .returning({ progressCount: schema.slackInteractions.progressCount });
5895
+ if (!updated) throw new Error("Slack progress delivery lost its durable interaction claim");
5896
+ return {
5897
+ kind: "claimed",
5898
+ created: true,
5899
+ progressCount: updated.progressCount,
5900
+ delivery: mapSlackInteractionProgressDelivery(delivery),
5901
+ };
5902
+ });
5903
+ }
5904
+
5905
+ export async function rekeySlackInteractionRoute(
5906
+ db: Database,
5907
+ input: Pick<SlackInteraction, "id" | "accountId" | "workspaceId"> & {
5908
+ routeKey: string;
5909
+ slackThreadTs: string;
5910
+ ackSlackMessageTs: string;
5911
+ },
5912
+ ): Promise<SlackInteraction | null> {
5913
+ return await withRlsContext(db, input, async (scopedDb) => {
5914
+ const [row] = await scopedDb
5915
+ .update(schema.slackInteractions)
5916
+ .set({
5917
+ routeKey: input.routeKey,
5918
+ slackThreadTs: input.slackThreadTs,
5919
+ ackSlackMessageTs: input.ackSlackMessageTs,
5920
+ updatedAt: sql`now()`,
5921
+ })
5922
+ .where(eq(schema.slackInteractions.id, input.id))
5923
+ .returning();
5924
+ return row ? mapSlackInteraction(row) : null;
5925
+ });
5926
+ }
5927
+
5928
+ export async function claimSlackInteractionDelivery(
5929
+ db: Database,
5930
+ claimHolderId: string,
5931
+ claimLeaseMs: number,
5932
+ ): Promise<SlackInteraction | null> {
5933
+ const rows = await db.execute<typeof schema.slackInteractions.$inferSelect>(
5934
+ sql`select * from opengeni_private.claim_slack_interaction_delivery(${claimHolderId}::uuid, ${claimLeaseMs})`,
5935
+ );
5936
+ return rows[0] ? mapSlackInteraction(rows[0]) : null;
5937
+ }
5938
+
5939
+ export async function reopenSlackInteractionDelivery(
5940
+ db: Database,
5941
+ input: Pick<SlackInteraction, "id" | "accountId" | "workspaceId">,
5942
+ ): Promise<boolean> {
5943
+ return await withRlsContext(db, input, async (scopedDb) => {
5944
+ const rows = await scopedDb
5945
+ .update(schema.slackInteractions)
5946
+ .set({ terminalDeliveryState: "open", updatedAt: sql`now()` })
5947
+ .where(eq(schema.slackInteractions.id, input.id))
5948
+ .returning({ id: schema.slackInteractions.id });
5949
+ return rows.length === 1;
5950
+ });
5951
+ }
5952
+
5953
+ export async function advanceSlackInteractionDelivery(
5954
+ db: Database,
5955
+ input: Pick<SlackInteraction, "id" | "accountId" | "workspaceId"> & {
5956
+ claimHolderId: string;
5957
+ sequence: number;
5958
+ ackSlackMessageTs?: string | null;
5959
+ },
5960
+ ): Promise<boolean> {
5961
+ return await withRlsContext(db, input, async (scopedDb) => {
5962
+ const rows = await scopedDb
5963
+ .update(schema.slackInteractions)
5964
+ .set({
5965
+ lastDeliveredSessionEventSequence: input.sequence,
5966
+ ...(input.ackSlackMessageTs !== undefined
5967
+ ? { ackSlackMessageTs: input.ackSlackMessageTs }
5968
+ : {}),
5969
+ updatedAt: sql`now()`,
5970
+ })
5971
+ .where(
5972
+ and(
5973
+ eq(schema.slackInteractions.id, input.id),
5974
+ eq(schema.slackInteractions.deliveryClaimHolderId, input.claimHolderId),
5975
+ lte(schema.slackInteractions.lastDeliveredSessionEventSequence, input.sequence),
5976
+ ),
5977
+ )
5978
+ .returning({ id: schema.slackInteractions.id });
5979
+ return rows.length === 1;
5980
+ });
5981
+ }
5982
+
5983
+ export async function releaseSlackInteractionDelivery(
5984
+ db: Database,
5985
+ input: Pick<SlackInteraction, "id" | "accountId" | "workspaceId"> & {
5986
+ claimHolderId: string;
5987
+ },
5988
+ ): Promise<boolean> {
5989
+ return await withRlsContext(db, input, async (scopedDb) => {
5990
+ const rows = await scopedDb
5991
+ .update(schema.slackInteractions)
5992
+ .set({
5993
+ deliveryClaimHolderId: null,
5994
+ deliveryClaimExpiresAt: null,
5995
+ updatedAt: sql`now()`,
5996
+ })
5997
+ .where(
5998
+ and(
5999
+ eq(schema.slackInteractions.id, input.id),
6000
+ eq(schema.slackInteractions.deliveryClaimHolderId, input.claimHolderId),
6001
+ ),
6002
+ )
6003
+ .returning({ id: schema.slackInteractions.id });
6004
+ return rows.length === 1;
6005
+ });
6006
+ }
6007
+
6008
+ export async function closeSlackInteractionDelivery(
6009
+ db: Database,
6010
+ input: Pick<SlackInteraction, "id" | "accountId" | "workspaceId"> & {
6011
+ claimHolderId: string;
6012
+ sequence: number;
6013
+ state: Exclude<SlackInteraction["terminalDeliveryState"], "open">;
6014
+ },
6015
+ ): Promise<boolean> {
6016
+ return await withRlsContext(db, input, async (scopedDb) => {
6017
+ const rows = await scopedDb
6018
+ .update(schema.slackInteractions)
6019
+ .set({
6020
+ lastDeliveredSessionEventSequence: input.sequence,
6021
+ terminalDeliveryState: input.state,
6022
+ deliveryClaimHolderId: null,
6023
+ deliveryClaimExpiresAt: null,
6024
+ updatedAt: sql`now()`,
6025
+ })
6026
+ .where(
6027
+ and(
6028
+ eq(schema.slackInteractions.id, input.id),
6029
+ eq(schema.slackInteractions.deliveryClaimHolderId, input.claimHolderId),
6030
+ ),
6031
+ )
6032
+ .returning({ id: schema.slackInteractions.id });
6033
+ return rows.length === 1;
6034
+ });
6035
+ }
6036
+
6037
+ function mapSlackBotUserLink(row: typeof schema.slackBotUserLinks.$inferSelect): SlackBotUserLink {
6038
+ return row;
6039
+ }
6040
+
6041
+ function mapSlackInteractionInbox(
6042
+ row: typeof schema.slackInteractionInbox.$inferSelect | Record<string, unknown>,
6043
+ ): SlackInteractionInboxEntry {
6044
+ return {
6045
+ id: slackRowString(row, "id", "id"),
6046
+ accountId: slackRowString(row, "accountId", "account_id"),
6047
+ workspaceId: slackRowString(row, "workspaceId", "workspace_id"),
6048
+ connectionId: slackRowString(row, "connectionId", "connection_id"),
6049
+ providerEventId: slackRowString(row, "providerEventId", "provider_event_id"),
6050
+ providerMessageId: slackRowString(row, "providerMessageId", "provider_message_id"),
6051
+ slackTeamId: slackRowString(row, "slackTeamId", "slack_team_id"),
6052
+ slackUserId: slackRowString(row, "slackUserId", "slack_user_id"),
6053
+ slackChannelId: slackRowString(row, "slackChannelId", "slack_channel_id"),
6054
+ slackMessageTs: slackRowString(row, "slackMessageTs", "slack_message_ts"),
6055
+ slackThreadTs: slackRowNullableString(row, "slackThreadTs", "slack_thread_ts"),
6056
+ triggerKind: slackRowString(row, "triggerKind", "trigger_kind") as SlackInteractionTriggerKind,
6057
+ text: slackRowString(row, "text", "text"),
6058
+ status: slackRowString(row, "status", "status") as SlackInteractionInboxEntry["status"],
6059
+ claimHolderId: slackRowNullableString(row, "claimHolderId", "claim_holder_id"),
6060
+ claimExpiresAt: slackRowNullableDate(row, "claimExpiresAt", "claim_expires_at"),
6061
+ attemptCount: slackRowNumber(row, "attemptCount", "attempt_count"),
6062
+ lastErrorCode: slackRowNullableString(row, "lastErrorCode", "last_error_code"),
6063
+ processedAt: slackRowNullableDate(row, "processedAt", "processed_at"),
6064
+ createdAt: slackRowDate(row, "createdAt", "created_at"),
6065
+ updatedAt: slackRowDate(row, "updatedAt", "updated_at"),
6066
+ };
6067
+ }
6068
+
6069
+ function mapSlackInteraction(
6070
+ row: typeof schema.slackInteractions.$inferSelect | Record<string, unknown>,
6071
+ ): SlackInteraction {
6072
+ return {
6073
+ id: slackRowString(row, "id", "id"),
6074
+ accountId: slackRowString(row, "accountId", "account_id"),
6075
+ workspaceId: slackRowString(row, "workspaceId", "workspace_id"),
6076
+ connectionId: slackRowString(row, "connectionId", "connection_id"),
6077
+ slackTeamId: slackRowString(row, "slackTeamId", "slack_team_id"),
6078
+ slackChannelId: slackRowString(row, "slackChannelId", "slack_channel_id"),
6079
+ slackThreadTs: slackRowString(row, "slackThreadTs", "slack_thread_ts"),
6080
+ routeKey: slackRowString(row, "routeKey", "route_key"),
6081
+ triggeringProviderEventId: slackRowString(
6082
+ row,
6083
+ "triggeringProviderEventId",
6084
+ "triggering_provider_event_id",
6085
+ ),
6086
+ owningSubjectId: slackRowString(row, "owningSubjectId", "owning_subject_id"),
6087
+ visibility: slackRowString(row, "visibility", "visibility") as SlackInteraction["visibility"],
6088
+ sessionReservationId: slackRowString(row, "sessionReservationId", "session_reservation_id"),
6089
+ sessionId: slackRowNullableString(row, "sessionId", "session_id"),
6090
+ lastDeliveredSessionEventSequence: slackRowNumber(
6091
+ row,
6092
+ "lastDeliveredSessionEventSequence",
6093
+ "last_delivered_session_event_sequence",
6094
+ ),
6095
+ deliveryClaimHolderId: slackRowNullableString(
6096
+ row,
6097
+ "deliveryClaimHolderId",
6098
+ "delivery_claim_holder_id",
6099
+ ),
6100
+ deliveryClaimExpiresAt: slackRowNullableDate(
6101
+ row,
6102
+ "deliveryClaimExpiresAt",
6103
+ "delivery_claim_expires_at",
6104
+ ),
6105
+ ackSlackMessageTs: slackRowNullableString(row, "ackSlackMessageTs", "ack_slack_message_ts"),
6106
+ progressCount: slackRowNumber(row, "progressCount", "progress_count"),
6107
+ terminalDeliveryState: slackRowString(
6108
+ row,
6109
+ "terminalDeliveryState",
6110
+ "terminal_delivery_state",
6111
+ ) as SlackInteraction["terminalDeliveryState"],
6112
+ createdAt: slackRowDate(row, "createdAt", "created_at"),
6113
+ updatedAt: slackRowDate(row, "updatedAt", "updated_at"),
6114
+ };
6115
+ }
6116
+
6117
+ function mapSlackInteractionProgressDelivery(
6118
+ row: typeof schema.slackInteractionProgressDeliveries.$inferSelect | Record<string, unknown>,
6119
+ ): SlackInteractionProgressDelivery {
6120
+ return {
6121
+ id: slackRowString(row, "id", "id"),
6122
+ accountId: slackRowString(row, "accountId", "account_id"),
6123
+ workspaceId: slackRowString(row, "workspaceId", "workspace_id"),
6124
+ interactionId: slackRowString(row, "interactionId", "interaction_id"),
6125
+ sessionEventSequence: slackRowNumber(row, "sessionEventSequence", "session_event_sequence"),
6126
+ slot: slackRowNumber(row, "slot", "slot"),
6127
+ operationId: slackRowString(row, "operationId", "operation_id"),
6128
+ createdAt: slackRowDate(row, "createdAt", "created_at"),
6129
+ };
6130
+ }
6131
+
6132
+ function slackRowValue(row: Record<string, unknown>, camelKey: string, snakeKey: string): unknown {
6133
+ return row[camelKey] ?? row[snakeKey];
6134
+ }
6135
+
6136
+ function slackRowString(row: Record<string, unknown>, camelKey: string, snakeKey: string): string {
6137
+ const value = slackRowValue(row, camelKey, snakeKey);
6138
+ if (typeof value !== "string") throw new Error(`Slack row omitted ${snakeKey}`);
6139
+ return value;
6140
+ }
6141
+
6142
+ function slackRowNullableString(
6143
+ row: Record<string, unknown>,
6144
+ camelKey: string,
6145
+ snakeKey: string,
6146
+ ): string | null {
6147
+ const value = slackRowValue(row, camelKey, snakeKey);
6148
+ if (value === null || value === undefined) return null;
6149
+ if (typeof value !== "string") throw new Error(`Slack row malformed ${snakeKey}`);
6150
+ return value;
6151
+ }
6152
+
6153
+ function slackRowNumber(row: Record<string, unknown>, camelKey: string, snakeKey: string): number {
6154
+ const value = slackRowValue(row, camelKey, snakeKey);
6155
+ const parsed = typeof value === "number" ? value : Number(value);
6156
+ if (!Number.isSafeInteger(parsed)) throw new Error(`Slack row malformed ${snakeKey}`);
6157
+ return parsed;
6158
+ }
6159
+
6160
+ function slackRowDate(row: Record<string, unknown>, camelKey: string, snakeKey: string): Date {
6161
+ const value = slackRowValue(row, camelKey, snakeKey);
6162
+ const parsed = value instanceof Date ? value : new Date(String(value));
6163
+ if (Number.isNaN(parsed.getTime())) throw new Error(`Slack row malformed ${snakeKey}`);
6164
+ return parsed;
6165
+ }
6166
+
6167
+ function slackRowNullableDate(
6168
+ row: Record<string, unknown>,
6169
+ camelKey: string,
6170
+ snakeKey: string,
6171
+ ): Date | null {
6172
+ const value = slackRowValue(row, camelKey, snakeKey);
6173
+ if (value === null || value === undefined) return null;
6174
+ return slackRowDate(row, camelKey, snakeKey);
6175
+ }
6176
+
5300
6177
  export class SlackBotLifecycleSuccessAuditError extends Error {
5301
6178
  constructor() {
5302
6179
  super("OpenGeni Slack bot lifecycle success audit failed");
@@ -5889,7 +6766,10 @@ export async function claimSlackBotDeleteOperation(
5889
6766
  })
5890
6767
  .returning();
5891
6768
  if (created) {
5892
- return { kind: "claimed", operation: mapSlackBotDeleteOperation(created) } as const;
6769
+ return {
6770
+ kind: "claimed",
6771
+ operation: mapSlackBotDeleteOperation(created),
6772
+ } as const;
5893
6773
  }
5894
6774
 
5895
6775
  const [existing] = await tx
@@ -6030,7 +6910,11 @@ export async function releaseSlackBotDeleteOperationClaim(
6030
6910
  }
6031
6911
 
6032
6912
  export type CompleteSlackBotDeleteOperationResult =
6033
- | { kind: "completed"; operation: SlackBotDeleteOperation; newlyCompleted: boolean }
6913
+ | {
6914
+ kind: "completed";
6915
+ operation: SlackBotDeleteOperation;
6916
+ newlyCompleted: boolean;
6917
+ }
6034
6918
  | { kind: "not_found" | "not_owned" };
6035
6919
 
6036
6920
  export async function completeSlackBotDeleteOperation(
@@ -15680,9 +16564,21 @@ export async function sessionTreeStatsForSessions(
15680
16564
  }
15681
16565
 
15682
16566
  function sessionFilters(
15683
- options: Pick<ListSessionsForSubjectOptions, "authorizationScope" | "parentSessionId" | "search">,
16567
+ options: Pick<
16568
+ ListSessionsForSubjectOptions,
16569
+ "authorizationScope" | "parentSessionId" | "search" | "subjectId"
16570
+ >,
15684
16571
  ): SQL[] {
15685
- const filters: SQL[] = [];
16572
+ const filters: SQL[] = [
16573
+ sql`not exists (
16574
+ select 1
16575
+ from ${schema.slackInteractions} private_slack_interaction
16576
+ where private_slack_interaction.workspace_id = ${schema.sessions.workspaceId}
16577
+ and private_slack_interaction.session_reservation_id = ${schema.sessions.rootSessionId}
16578
+ and private_slack_interaction.visibility = 'private'
16579
+ and private_slack_interaction.owning_subject_id <> ${options.subjectId}
16580
+ )`,
16581
+ ];
15686
16582
  if (options.authorizationScope) {
15687
16583
  filters.push(sessionAuthorizationScopeFilter(options.authorizationScope));
15688
16584
  }
@@ -16295,7 +17191,20 @@ export async function getSessionForSubject(
16295
17191
  eq(schema.sessionPins.sessionId, schema.sessions.id),
16296
17192
  ),
16297
17193
  )
16298
- .where(and(eq(schema.sessions.workspaceId, workspaceId), eq(schema.sessions.id, sessionId)))
17194
+ .where(
17195
+ and(
17196
+ eq(schema.sessions.workspaceId, workspaceId),
17197
+ eq(schema.sessions.id, sessionId),
17198
+ sql`not exists (
17199
+ select 1
17200
+ from ${schema.slackInteractions} private_slack_interaction
17201
+ where private_slack_interaction.workspace_id = ${schema.sessions.workspaceId}
17202
+ and private_slack_interaction.session_reservation_id = ${schema.sessions.rootSessionId}
17203
+ and private_slack_interaction.visibility = 'private'
17204
+ and private_slack_interaction.owning_subject_id <> ${subjectId}
17205
+ )`,
17206
+ ),
17207
+ )
16299
17208
  .limit(1);
16300
17209
  if (!row) return null;
16301
17210
  const mcpServers = await sessionMcpServerMetadataForSessions(scopedDb, workspaceId, [
@@ -19436,7 +20345,10 @@ export async function recordSkippedContextCompaction(
19436
20345
  });
19437
20346
  if (!fence.allowed) return { recorded: false as const, reason: fence.reason };
19438
20347
  if (requirePendingRequest && !fence.session.compactRequested) {
19439
- return { recorded: false as const, reason: "request_not_pending" as const };
20348
+ return {
20349
+ recorded: false as const,
20350
+ reason: "request_not_pending" as const,
20351
+ };
19440
20352
  }
19441
20353
  const inserted = await tx
19442
20354
  .insert(schema.sessionEvents)
@@ -27566,7 +28478,11 @@ export type WorkspaceArchiveCaptureClaim = {
27566
28478
  };
27567
28479
 
27568
28480
  export type ClaimWorkspaceArchiveCaptureResult =
27569
- | { status: "claimed"; claim: WorkspaceArchiveCaptureClaim; lease: LeaseSnapshot }
28481
+ | {
28482
+ status: "claimed";
28483
+ claim: WorkspaceArchiveCaptureClaim;
28484
+ lease: LeaseSnapshot;
28485
+ }
27570
28486
  | {
27571
28487
  status:
27572
28488
  | "lease_fenced"
@@ -41457,3 +42373,4 @@ function shortHash(value: string): string {
41457
42373
  // evaluates under the index↔resolver module cycle.
41458
42374
  export * from "./codex-token-resolver";
41459
42375
  export * from "./connection-token-resolver";
42376
+ export * from "./workspace-artifacts";