@opengeni/core 0.4.7 → 0.4.9

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.
@@ -20,7 +20,6 @@ import {
20
20
  import {
21
21
  createSession,
22
22
  createSessionWithIdempotencyKey,
23
- enqueueSessionMessageAtomically,
24
23
  encryptVariableSetValue,
25
24
  getAnySessionInGroup,
26
25
  getEnrollment,
@@ -31,20 +30,26 @@ import {
31
30
  getSandbox,
32
31
  getSession,
33
32
  getSessionByCreateIdempotencyKey,
33
+ getSessionEvent,
34
+ getWorkspaceControlEvent,
34
35
  getSessionLineage,
35
36
  getSessionTurn,
36
37
  getWorkspaceModelPolicy,
37
38
  initializeSessionStartAtomically,
38
39
  requireSession,
40
+ submitHumanPromptInTransaction,
39
41
  updateSessionTitle as updateSessionTitleRow,
42
+ withWorkspaceSubjectRls,
40
43
  type CreateSessionMcpServerInput,
41
44
  type Database,
42
45
  type UpdateSessionMcpServerCredentialsInput,
43
- SessionQueueConflictError,
46
+ QueueCommandConflictError,
47
+ SessionControlConflictError,
44
48
  } from "@opengeni/db";
45
49
  import {
46
50
  appendAndPublishEvents,
47
51
  publishDurableSessionEvents,
52
+ publishDurableWorkspaceControlEvent,
48
53
  type EventBus,
49
54
  } from "@opengeni/events";
50
55
  import { HTTPException } from "hono/http-exception";
@@ -596,7 +601,7 @@ export function reasoningEffortForSession(
596
601
  export async function postUserMessageTurn(input: {
597
602
  db: Database;
598
603
  bus: EventBus;
599
- workflowClient: Pick<SessionWorkflowClient, "wakeSessionWorkflow" | "signalSessionControl">;
604
+ workflowClient: Pick<SessionWorkflowClient, "wakeSessionWorkflow">;
600
605
  settings: Settings;
601
606
  accountId: string;
602
607
  workspaceId: string;
@@ -608,11 +613,11 @@ export async function postUserMessageTurn(input: {
608
613
  reasoningEffort?: Settings["openaiReasoningEffort"] | null;
609
614
  clientEventId?: string;
610
615
  mcpCredentialUpdates?: UpdateSessionMcpServerCredentialsInput[];
611
- delivery?: "queue" | "steer";
616
+ delivery?: "send" | "steer";
612
617
  origin?: "human" | "operator";
613
618
  actor?: string;
614
- expectedControlGeneration?: number;
615
- expectedWorkspaceInferenceGeneration?: number;
619
+ controlEtag?: string | null;
620
+ expectedDraftRevision?: number | null;
616
621
  }): Promise<{ accepted: SessionEvent; turn: SessionTurn }> {
617
622
  const { db, bus, workflowClient, settings, accountId, workspaceId, sessionId } = input;
618
623
  const requestedModel = input.model ?? null;
@@ -621,34 +626,37 @@ export async function postUserMessageTurn(input: {
621
626
  // model inherits the session's model downstream (always a configured id).
622
627
  assertConfiguredModel(settings, requestedModel);
623
628
  await assertWorkspaceModelPolicyAllows(db, settings, workspaceId, requestedModel);
629
+ const operationKey = input.clientEventId ?? crypto.randomUUID();
624
630
  let result;
625
631
  try {
626
- result = await enqueueSessionMessageAtomically(db, {
627
- accountId,
628
- workspaceId,
629
- sessionId,
630
- actor: input.actor ?? accountId,
631
- origin: input.origin ?? "human",
632
- text: input.text,
633
- resources: input.resources,
634
- tools: input.tools,
635
- model: requestedModel,
636
- reasoningEffort: requestedReasoningEffort,
637
- clientEventId: input.clientEventId ?? null,
638
- mcpCredentialUpdates: input.mcpCredentialUpdates ?? [],
639
- delivery: input.delivery ?? "queue",
640
- ...(input.expectedControlGeneration !== undefined
641
- ? { expectedControlGeneration: input.expectedControlGeneration }
642
- : {}),
643
- ...(input.expectedWorkspaceInferenceGeneration !== undefined
644
- ? {
645
- expectedWorkspaceInferenceGeneration: input.expectedWorkspaceInferenceGeneration,
646
- }
647
- : {}),
648
- reasoningEffortFallback: settings.openaiReasoningEffort,
649
- });
632
+ result = await withWorkspaceSubjectRls(db, workspaceId, input.actor ?? accountId, (scoped) =>
633
+ scoped.transaction((tx) =>
634
+ submitHumanPromptInTransaction(tx as unknown as Database, {
635
+ accountId,
636
+ workspaceId,
637
+ sessionId,
638
+ subjectId: input.actor ?? accountId,
639
+ actor: { type: "human", subjectId: input.actor ?? accountId },
640
+ operationKey,
641
+ delivery: input.delivery ?? "send",
642
+ controlEtag: input.controlEtag ?? null,
643
+ expectedDraftRevision: input.expectedDraftRevision ?? null,
644
+ text: input.text,
645
+ resources: input.resources,
646
+ tools: input.tools,
647
+ model: requestedModel,
648
+ reasoningEffort: requestedReasoningEffort,
649
+ reasoningEffortFallback: settings.openaiReasoningEffort,
650
+ source: input.origin === "operator" ? "api" : "user",
651
+ mcpCredentialUpdates: input.mcpCredentialUpdates ?? [],
652
+ }),
653
+ ),
654
+ );
650
655
  } catch (error) {
651
- if (error instanceof SessionQueueConflictError) {
656
+ if (
657
+ error instanceof QueueCommandConflictError ||
658
+ error instanceof SessionControlConflictError
659
+ ) {
652
660
  throw new HTTPException(409, { message: error.message });
653
661
  }
654
662
  if (error instanceof Error && error.message.includes("cancelled")) {
@@ -659,35 +667,51 @@ export async function postUserMessageTurn(input: {
659
667
  }
660
668
  throw error;
661
669
  }
662
- await bus.publish(workspaceId, sessionId, result.events);
663
- if (result.shouldSignalControl && result.controlEvent) {
664
- if (result.workflowWakeRevision === null) {
665
- throw new Error("Steer control has no workflow wake revision");
666
- }
667
- await workflowClient.signalSessionControl({
668
- accountId,
670
+ const events = await Promise.all(
671
+ result.eventIds.map((eventId) => getSessionEvent(db, workspaceId, eventId)),
672
+ );
673
+ if (events.some((event) => event === null)) {
674
+ throw new Error("Committed prompt events could not be reloaded");
675
+ }
676
+ const turn = await getSessionTurn(db, workspaceId, result.turnId);
677
+ if (!turn) throw new Error("Committed prompt turn could not be reloaded");
678
+ const accepted = events.find((event) => event?.id === result.acceptedEventId);
679
+ if (!accepted) throw new Error("Committed user.message event could not be reloaded");
680
+ await publishDurableSessionEvents(
681
+ bus,
682
+ workspaceId,
683
+ sessionId,
684
+ events.filter((event): event is SessionEvent => event !== null),
685
+ );
686
+ if (result.workspaceControlEventId) {
687
+ const controlEvent = await getWorkspaceControlEvent(
688
+ db,
669
689
  workspaceId,
670
- sessionId,
671
- eventId: result.controlEvent.id,
672
- workflowId: result.temporalWorkflowId,
673
- workflowWakeRevision: result.workflowWakeRevision,
674
- });
675
- } else if (result.shouldWake) {
676
- if (result.workflowWakeRevision === null) {
677
- throw new Error("Runnable prompt has no workflow wake revision");
690
+ result.workspaceControlEventId,
691
+ );
692
+ if (!controlEvent) {
693
+ throw new Error(
694
+ `Committed workspace control event disappeared: ${result.workspaceControlEventId}`,
695
+ );
678
696
  }
697
+ await publishDurableWorkspaceControlEvent(bus, workspaceId, controlEvent);
698
+ }
699
+ try {
679
700
  await workflowClient.wakeSessionWorkflow({
680
701
  accountId,
681
702
  workspaceId,
682
703
  sessionId,
683
- workflowId: result.temporalWorkflowId,
684
- wakeRevision: result.workflowWakeRevision,
704
+ workflowId: turn.temporalWorkflowId,
705
+ wakeRevision: result.wakeRevision,
706
+ ...(result.interruptionCount > 0 ? { interruptionRequested: true } : {}),
685
707
  });
708
+ } catch (error) {
709
+ console.warn(
710
+ `[sessions] workflow wake failed for committed prompt ${workspaceId}/${sessionId}; durable outbox will retry`,
711
+ error,
712
+ );
686
713
  }
687
- return {
688
- accepted: result.accepted,
689
- turn: result.turn,
690
- };
714
+ return { accepted, turn };
691
715
  }
692
716
 
693
717
  /**
@@ -1132,10 +1156,10 @@ export async function acceptSessionUserMessage(
1132
1156
  reasoningEffort?: ReasoningEffort | null;
1133
1157
  clientEventId?: string;
1134
1158
  mcpCredentialUpdates?: SessionMcpCredentialUpdateInput[];
1135
- delivery?: "queue" | "steer";
1159
+ delivery?: "send" | "steer";
1136
1160
  origin?: "human" | "operator";
1137
- expectedControlGeneration?: number;
1138
- expectedWorkspaceInferenceGeneration?: number;
1161
+ controlEtag?: string | null;
1162
+ expectedDraftRevision?: number | null;
1139
1163
  },
1140
1164
  ): Promise<{ accepted: SessionEvent; turn: SessionTurn }> {
1141
1165
  const { settings, db, bus, workflowClient, objectStorage } = deps;
@@ -1192,16 +1216,12 @@ export async function acceptSessionUserMessage(
1192
1216
  model: input.model ?? null,
1193
1217
  reasoningEffort: input.reasoningEffort ?? null,
1194
1218
  mcpCredentialUpdates,
1195
- delivery: input.delivery ?? "queue",
1219
+ delivery: input.delivery ?? "send",
1196
1220
  origin: input.origin ?? "human",
1197
1221
  actor: grant.subjectId,
1198
- ...(input.expectedControlGeneration !== undefined
1199
- ? { expectedControlGeneration: input.expectedControlGeneration }
1200
- : {}),
1201
- ...(input.expectedWorkspaceInferenceGeneration !== undefined
1202
- ? {
1203
- expectedWorkspaceInferenceGeneration: input.expectedWorkspaceInferenceGeneration,
1204
- }
1222
+ ...(input.controlEtag !== undefined ? { controlEtag: input.controlEtag } : {}),
1223
+ ...(input.expectedDraftRevision !== undefined
1224
+ ? { expectedDraftRevision: input.expectedDraftRevision }
1205
1225
  : {}),
1206
1226
  ...(input.clientEventId ? { clientEventId: input.clientEventId } : {}),
1207
1227
  });
package/src/index.ts CHANGED
@@ -27,6 +27,7 @@
27
27
  // The central dependency type surface (AppDependencies, ApiRouteDeps,
28
28
  // SessionWorkflowClient, DocumentIndexClient, ObjectStorageDependency).
29
29
  export * from "./dependencies";
30
+ export * from "./workflow-wake-contract";
30
31
 
31
32
  // Boundary type slots referenced by dependencies.ts. The IMPLEMENTATIONS that
32
33
  // construct these (the real sandbox client / Better Auth instance) stay in
@@ -58,3 +59,4 @@ export * from "./domain/resources";
58
59
  export * from "./domain/scheduled-tasks";
59
60
  export * from "./domain/sessions";
60
61
  export * from "./domain/workspace-members";
62
+ export * from "./application/session-commands";
@@ -0,0 +1,6 @@
1
+ /** One global Temporal Schedule owns bounded delivery of committed session wakes. */
2
+ export const SESSION_WORKFLOW_WAKE_DISPATCHER_SCHEDULE_ID =
3
+ "opengeni-session-workflow-wake-dispatcher";
4
+ export const SESSION_WORKFLOW_WAKE_DISPATCHER_WORKFLOW_TYPE =
5
+ "sessionWorkflowWakeDispatcherWorkflow";
6
+ export const SESSION_WORKFLOW_WAKE_DISPATCHER_PERIOD_MS = 10_000;