@opengeni/api-router 2.1.0-canary.0 → 2.3.2-canary.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/app.js +1 -1
  2. package/dist/auth/managed-auth.d.ts +5 -2
  3. package/dist/auth/managed-email.d.ts +29 -0
  4. package/dist/auth/organization-user-setup.d.ts +57 -0
  5. package/dist/{chunk-QKDFBBUE.js → chunk-IBV7Z6F4.js} +3872 -1845
  6. package/dist/chunk-IBV7Z6F4.js.map +1 -0
  7. package/dist/index.js +1 -1
  8. package/dist/integrations/slack-app-home.d.ts +1 -1
  9. package/dist/integrations/slack-bot.d.ts +8 -0
  10. package/dist/integrations/slack-interactions.d.ts +35 -2
  11. package/dist/mcp/server.d.ts +1 -1
  12. package/dist/mcp/session-view.d.ts +1 -0
  13. package/dist/routes/automations.d.ts +13 -0
  14. package/dist/routes/insights.d.ts +2 -1
  15. package/dist/routes/managed-onboarding.d.ts +29 -0
  16. package/dist/routes/pr-review-github.d.ts +3 -0
  17. package/package.json +18 -18
  18. package/src/app.ts +64 -5
  19. package/src/auth/managed-auth.ts +29 -34
  20. package/src/auth/managed-email.ts +174 -0
  21. package/src/auth/organization-user-setup.ts +217 -0
  22. package/src/http/auth.ts +15 -0
  23. package/src/http/sse.ts +62 -13
  24. package/src/integrations/slack-app-home.ts +2 -2
  25. package/src/integrations/slack-bot.ts +5 -0
  26. package/src/integrations/slack-interactions.ts +653 -71
  27. package/src/integrations/slack-routing.ts +25 -12
  28. package/src/mcp/company-brain-governed-writes.ts +4 -4
  29. package/src/mcp/company-profile-agent-admin.ts +11 -18
  30. package/src/mcp/remember.ts +4 -4
  31. package/src/mcp/server.ts +50 -4
  32. package/src/mcp/session-view.ts +8 -2
  33. package/src/routes/automations.ts +3 -3
  34. package/src/routes/documents.ts +136 -3
  35. package/src/routes/insights.ts +61 -19
  36. package/src/routes/managed-onboarding.ts +317 -0
  37. package/src/routes/organization-memberships.ts +212 -155
  38. package/src/routes/pr-review-github.ts +844 -0
  39. package/src/routes/pr-review.ts +20 -0
  40. package/src/routes/rigs.ts +37 -4
  41. package/src/routes/sessions.ts +101 -0
  42. package/src/sandbox/channel-a.ts +34 -7
  43. package/src/sandbox/viewer.ts +47 -11
  44. package/dist/chunk-QKDFBBUE.js.map +0 -1
@@ -0,0 +1,174 @@
1
+ import type { Settings } from "@opengeni/config";
2
+ import type {
3
+ ManagedEmailDeliveryResult,
4
+ ManagedEmailMessage,
5
+ ManagedEmailTransport,
6
+ } from "@opengeni/core";
7
+ import { createHmac } from "node:crypto";
8
+ import { Resend } from "resend";
9
+
10
+ const CAPTURE_MAX_MESSAGES = 250;
11
+ const CAPTURE_TTL_MS = 15 * 60 * 1000;
12
+
13
+ export type CapturedManagedEmail = ManagedEmailMessage & {
14
+ capturedAt: string;
15
+ };
16
+
17
+ /**
18
+ * Process-local test/development transport. Captures are count/TTL bounded and
19
+ * one-time readable; there is deliberately no route, disk, database, or log
20
+ * projection for message bodies or setup bearers.
21
+ */
22
+ export class InMemoryManagedEmailTransport implements ManagedEmailTransport {
23
+ private readonly messages: CapturedManagedEmail[] = [];
24
+ readonly sender: string;
25
+ readonly idempotency: ManagedEmailTransport["idempotency"];
26
+
27
+ constructor(
28
+ private readonly options: {
29
+ maxMessages?: number;
30
+ ttlMs?: number;
31
+ now?: () => number;
32
+ sender?: string;
33
+ idempotency?: ManagedEmailTransport["idempotency"];
34
+ } = {},
35
+ ) {
36
+ this.sender = options.sender ?? "OpenGeni <auth@mail.opengeni.ai>";
37
+ this.idempotency = options.idempotency ?? {
38
+ scope: "opengeni-in-memory-v1",
39
+ retentionSeconds: 86_400,
40
+ };
41
+ }
42
+
43
+ async send(message: ManagedEmailMessage): Promise<ManagedEmailDeliveryResult> {
44
+ this.prune();
45
+ const maxMessages = this.options.maxMessages ?? CAPTURE_MAX_MESSAGES;
46
+ if (this.messages.length >= maxMessages) this.messages.splice(0, 1);
47
+ this.messages.push({
48
+ ...message,
49
+ capturedAt: new Date(this.now()).toISOString(),
50
+ });
51
+ return { status: "sent", providerMessageId: null };
52
+ }
53
+
54
+ take(predicate: (message: CapturedManagedEmail) => boolean): CapturedManagedEmail | null {
55
+ this.prune();
56
+ const index = this.messages.findIndex(predicate);
57
+ if (index < 0) return null;
58
+ return this.messages.splice(index, 1)[0] ?? null;
59
+ }
60
+
61
+ size(): number {
62
+ this.prune();
63
+ return this.messages.length;
64
+ }
65
+
66
+ private prune(): void {
67
+ const cutoff = this.now() - (this.options.ttlMs ?? CAPTURE_TTL_MS);
68
+ while (this.messages[0] && Date.parse(this.messages[0].capturedAt) <= cutoff) {
69
+ this.messages.shift();
70
+ }
71
+ }
72
+
73
+ private now(): number {
74
+ return this.options.now?.() ?? Date.now();
75
+ }
76
+ }
77
+
78
+ class UnconfiguredManagedEmailTransport implements ManagedEmailTransport {
79
+ readonly idempotency = {
80
+ scope: "opengeni-unconfigured-v1",
81
+ retentionSeconds: 0,
82
+ } as const;
83
+
84
+ constructor(readonly sender: string) {}
85
+
86
+ async send(): Promise<ManagedEmailDeliveryResult> {
87
+ return { status: "failed", errorClass: "provider_not_configured" };
88
+ }
89
+ }
90
+
91
+ class ResendManagedEmailTransport implements ManagedEmailTransport {
92
+ private readonly client: Resend;
93
+ readonly idempotency: ManagedEmailTransport["idempotency"];
94
+
95
+ constructor(
96
+ apiKey: string,
97
+ readonly sender: string,
98
+ scopeSecret: string,
99
+ ) {
100
+ this.client = new Resend(apiKey);
101
+ this.idempotency = {
102
+ // The keyed digest binds this delivery to the Resend account without
103
+ // persisting an API-key derivative that can be tested offline.
104
+ scope: `resend-v1-24h:${createHmac("sha256", scopeSecret).update(apiKey).digest("hex")}`,
105
+ retentionSeconds: 86_400,
106
+ };
107
+ }
108
+
109
+ async send(message: ManagedEmailMessage): Promise<ManagedEmailDeliveryResult> {
110
+ if (message.from !== this.sender) {
111
+ return { status: "failed", errorClass: "sender_changed" };
112
+ }
113
+ try {
114
+ const result = await this.client.emails.send(
115
+ {
116
+ from: message.from,
117
+ to: message.to,
118
+ subject: message.subject,
119
+ text: message.text,
120
+ html: message.html,
121
+ },
122
+ message.idempotencyKey ? { idempotencyKey: message.idempotencyKey } : undefined,
123
+ );
124
+ if (!result.error) {
125
+ return { status: "sent", providerMessageId: result.data?.id ?? null };
126
+ }
127
+ const statusCode = "statusCode" in result.error ? Number(result.error.statusCode) : NaN;
128
+ return clearProviderRefusal(statusCode)
129
+ ? { status: "failed", errorClass: boundedErrorClass(result.error.name, "provider_refused") }
130
+ : {
131
+ status: "outcome_unknown",
132
+ errorClass: boundedErrorClass(result.error.name, "provider_ambiguous"),
133
+ };
134
+ } catch (error) {
135
+ return {
136
+ status: "outcome_unknown",
137
+ errorClass: boundedErrorClass(
138
+ error instanceof Error ? error.name : "transport_error",
139
+ "transport_error",
140
+ ),
141
+ };
142
+ }
143
+ }
144
+ }
145
+
146
+ export function createManagedEmailTransport(settings: Settings): ManagedEmailTransport {
147
+ if (settings.resendApiKey) {
148
+ if (!settings.betterAuthSecret) {
149
+ throw new Error("OPENGENI_BETTER_AUTH_SECRET is required for managed email delivery");
150
+ }
151
+ return new ResendManagedEmailTransport(
152
+ settings.resendApiKey,
153
+ settings.emailFrom,
154
+ settings.betterAuthSecret,
155
+ );
156
+ }
157
+ if (settings.environment === "local" || settings.environment === "test") {
158
+ return new InMemoryManagedEmailTransport({ sender: settings.emailFrom });
159
+ }
160
+ return new UnconfiguredManagedEmailTransport(settings.emailFrom);
161
+ }
162
+
163
+ function clearProviderRefusal(statusCode: number): boolean {
164
+ return statusCode >= 400 && statusCode < 500 && ![408, 409, 425, 429].includes(statusCode);
165
+ }
166
+
167
+ function boundedErrorClass(value: string, fallback: string): string {
168
+ const normalized = value
169
+ .trim()
170
+ .toLowerCase()
171
+ .replaceAll(/[^a-z0-9_-]+/g, "_")
172
+ .slice(0, 64);
173
+ return normalized || fallback;
174
+ }
@@ -0,0 +1,217 @@
1
+ import type { Settings } from "@opengeni/config";
2
+ import type { ManagedEmailTransport } from "@opengeni/core";
3
+
4
+ const encoder = new TextEncoder();
5
+
6
+ /**
7
+ * Prove that the stable invited-user setup bearer can be constructed before an
8
+ * invitation commits. Provider availability is intentionally outside this
9
+ * precondition: the durable delivery journal records a failed or ambiguous
10
+ * transport outcome after the invitation exists.
11
+ */
12
+ export function assertOrganizationUserSetupDeliveryConfigured(
13
+ settings: Settings,
14
+ transport: ManagedEmailTransport,
15
+ ): void {
16
+ requiredSetupSecret(settings);
17
+ requiredPublicBaseUrl(settings);
18
+ assertManagedEmailTransportMetadata(transport);
19
+ }
20
+
21
+ /** Reject an invalid embedded-provider contract before any durable boundary. */
22
+ export function assertManagedEmailTransportMetadata(transport: ManagedEmailTransport): void {
23
+ if (
24
+ transport.sender.trim() !== transport.sender ||
25
+ encoder.encode(transport.sender).byteLength < 3 ||
26
+ encoder.encode(transport.sender).byteLength > 320
27
+ ) {
28
+ throw new Error("Managed email sender is invalid");
29
+ }
30
+ const { scope, retentionSeconds } = transport.idempotency;
31
+ if (
32
+ scope.trim() !== scope ||
33
+ !/^[a-z0-9][a-z0-9:._-]*$/.test(scope) ||
34
+ encoder.encode(scope).byteLength > 200 ||
35
+ !Number.isInteger(retentionSeconds) ||
36
+ retentionSeconds < 0 ||
37
+ retentionSeconds > 31_536_000
38
+ ) {
39
+ throw new Error("Managed email idempotency contract is invalid");
40
+ }
41
+ }
42
+
43
+ export async function deriveOrganizationUserSetupToken(
44
+ settings: Settings,
45
+ input: { invitationId: string; deliveryId: string },
46
+ ): Promise<{ token: string; digest: string; url: string }> {
47
+ const secret = requiredSetupSecret(settings);
48
+ const key = await crypto.subtle.importKey(
49
+ "raw",
50
+ encoder.encode(secret),
51
+ { name: "HMAC", hash: "SHA-256" },
52
+ false,
53
+ ["sign"],
54
+ );
55
+ const signature = await crypto.subtle.sign(
56
+ "HMAC",
57
+ key,
58
+ encoder.encode(
59
+ `opengeni:organization-user-setup-delivery:v1:${input.deliveryId}:${input.invitationId}`,
60
+ ),
61
+ );
62
+ const token = base64Url(new Uint8Array(signature));
63
+ const digest = await sha256Hex(token);
64
+ const url = new URL("/setup-account", requiredPublicBaseUrl(settings));
65
+ url.hash = new URLSearchParams({ token }).toString();
66
+ return { token, digest, url: url.toString() };
67
+ }
68
+
69
+ export type OrganizationUserSetupEmailSnapshot = {
70
+ senderEmail: string;
71
+ recipientEmail: string;
72
+ recipientName: string | null;
73
+ organizationName: string;
74
+ organizationRole: "owner" | "admin" | "member";
75
+ sharedWorkspaceAccess: Array<{
76
+ workspaceId: string;
77
+ workspaceName: string;
78
+ role: "viewer" | "member" | "admin";
79
+ }>;
80
+ setupUrl: string;
81
+ };
82
+
83
+ export function renderOrganizationUserSetupEmail(input: OrganizationUserSetupEmailSnapshot): {
84
+ from: string;
85
+ to: string;
86
+ subject: string;
87
+ text: string;
88
+ html: string;
89
+ } {
90
+ const greeting = input.recipientName ? `Hi ${input.recipientName},` : "Hello,";
91
+ const role = titleCase(input.organizationRole);
92
+ const workspaceSummary =
93
+ input.sharedWorkspaceAccess.length === 0
94
+ ? "No shared workspaces are assigned yet."
95
+ : `Shared workspace access:\n${input.sharedWorkspaceAccess
96
+ .map((workspace) => `- ${workspace.workspaceName}: ${titleCase(workspace.role)}`)
97
+ .join("\n")}`;
98
+ const workspaceHtml =
99
+ input.sharedWorkspaceAccess.length === 0
100
+ ? "<p>No shared workspaces are assigned yet.</p>"
101
+ : `<p>Shared workspace access:</p><ul>${input.sharedWorkspaceAccess
102
+ .map(
103
+ (workspace) =>
104
+ `<li>${escapeHtml(workspace.workspaceName)}: ${escapeHtml(titleCase(workspace.role))}</li>`,
105
+ )
106
+ .join("")}</ul>`;
107
+ return {
108
+ from: input.senderEmail,
109
+ to: input.recipientEmail,
110
+ subject: `Join ${input.organizationName} on OpenGeni`,
111
+ text: `${greeting}\n\nYou have been invited to ${input.organizationName} as ${role}.\n\n${workspaceSummary}\n\nThis invitation grants only the organization role and shared workspace access listed above. It never shares anyone's Personal workspace.\n\nSet up your account: ${input.setupUrl}\n\nIf you already have an OpenGeni account, sign in and accept the invitation instead.`,
112
+ html: `<p>${escapeHtml(greeting)}</p><p>You have been invited to <strong>${escapeHtml(input.organizationName)}</strong> as ${escapeHtml(role)}.</p>${workspaceHtml}<p>This invitation grants only the organization role and shared workspace access listed above. It never shares anyone's Personal workspace.</p><p><a href="${escapeHtml(input.setupUrl)}">Set up your account</a></p><p>If you already have an OpenGeni account, sign in and accept the invitation instead.</p>`,
113
+ };
114
+ }
115
+
116
+ export async function organizationUserSetupPayloadDigest(input: {
117
+ from: string;
118
+ to: string;
119
+ subject: string;
120
+ text: string;
121
+ html: string;
122
+ providerIdempotencyScope: string;
123
+ }): Promise<string> {
124
+ return await sha256Hex(
125
+ JSON.stringify({
126
+ version: 2,
127
+ providerIdempotencyScope: input.providerIdempotencyScope,
128
+ from: input.from,
129
+ to: input.to,
130
+ subject: input.subject,
131
+ text: input.text,
132
+ html: input.html,
133
+ }),
134
+ );
135
+ }
136
+
137
+ export async function organizationUserSetupRequestFingerprint(
138
+ settings: Settings,
139
+ input: { tokenDigest: string; name: string; password: string },
140
+ ): Promise<string> {
141
+ const key = await crypto.subtle.importKey(
142
+ "raw",
143
+ encoder.encode(requiredSetupSecret(settings)),
144
+ { name: "HMAC", hash: "SHA-256" },
145
+ false,
146
+ ["sign"],
147
+ );
148
+ const signature = await crypto.subtle.sign(
149
+ "HMAC",
150
+ key,
151
+ encoder.encode(
152
+ JSON.stringify({
153
+ tokenDigest: input.tokenDigest,
154
+ name: input.name,
155
+ password: input.password,
156
+ }),
157
+ ),
158
+ );
159
+ return hex(new Uint8Array(signature));
160
+ }
161
+
162
+ export async function selfServiceOrganizationSetupRequestFingerprint(input: {
163
+ authUserId: string;
164
+ organizationName: string;
165
+ }): Promise<string> {
166
+ const actorSubjectId = `user:${input.authUserId}`;
167
+ const organizationNameBytes = encoder.encode(input.organizationName);
168
+ return await sha256Hex(
169
+ `opengeni:self-service-organization:v1:${actorSubjectId}:${organizationNameBytes.byteLength}:${input.organizationName}`,
170
+ );
171
+ }
172
+
173
+ export async function organizationUserSetupTokenDigest(token: string): Promise<string> {
174
+ return await sha256Hex(token);
175
+ }
176
+
177
+ function requiredSetupSecret(settings: Settings): string {
178
+ if (!settings.betterAuthSecret) {
179
+ throw new Error("OPENGENI_BETTER_AUTH_SECRET is required for organization user setup");
180
+ }
181
+ return settings.betterAuthSecret;
182
+ }
183
+
184
+ function requiredPublicBaseUrl(settings: Settings): string {
185
+ if (!settings.publicBaseUrl) {
186
+ throw new Error("OPENGENI_PUBLIC_BASE_URL is required for organization user setup");
187
+ }
188
+ return settings.publicBaseUrl;
189
+ }
190
+
191
+ async function sha256Hex(value: string): Promise<string> {
192
+ const digest = await crypto.subtle.digest("SHA-256", encoder.encode(value));
193
+ return hex(new Uint8Array(digest));
194
+ }
195
+
196
+ function hex(bytes: Uint8Array): string {
197
+ return [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
198
+ }
199
+
200
+ function base64Url(bytes: Uint8Array): string {
201
+ let binary = "";
202
+ for (const byte of bytes) binary += String.fromCharCode(byte);
203
+ return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
204
+ }
205
+
206
+ function titleCase(value: string): string {
207
+ return `${value.slice(0, 1).toUpperCase()}${value.slice(1)}`;
208
+ }
209
+
210
+ function escapeHtml(value: string): string {
211
+ return value
212
+ .replaceAll("&", "&amp;")
213
+ .replaceAll("<", "&lt;")
214
+ .replaceAll(">", "&gt;")
215
+ .replaceAll('"', "&quot;")
216
+ .replaceAll("'", "&#039;");
217
+ }
package/src/http/auth.ts CHANGED
@@ -5,6 +5,8 @@ import { installExactPaths, isInstallRedirectPath } from "../routes/install";
5
5
 
6
6
  const githubConnectPathPattern = /^\/v1\/workspaces\/[^/]+\/github\/connect$/;
7
7
  const githubInstallationLinkPathPattern = /^\/v1\/workspaces\/[^/]+\/github\/installations$/;
8
+ const prReviewGithubBrowserPathPattern =
9
+ /^\/v1\/workspaces\/[^/]+\/pr-review\/github\/(?:connect|installations\/select|installations\/[^/]+\/configure)$/;
8
10
 
9
11
  export function requireAccessKey(settings: Settings): MiddlewareHandler {
10
12
  return async (c, next) => {
@@ -43,6 +45,9 @@ function isAuthExempt(c: Context, settings: Settings): boolean {
43
45
  if (c.req.method === "POST" && path.startsWith("/v1/webhooks/automations/")) {
44
46
  return true;
45
47
  }
48
+ if (c.req.method === "POST" && path === "/v1/webhooks/pr-review/github") {
49
+ return true;
50
+ }
46
51
  if (
47
52
  path === "/v1/github/setup" ||
48
53
  path === "/v1/github/install/callback" ||
@@ -51,6 +56,13 @@ function isAuthExempt(c: Context, settings: Settings): boolean {
51
56
  ) {
52
57
  return true;
53
58
  }
59
+ if (
60
+ path === "/v1/pr-review/github/setup" ||
61
+ path === "/v1/pr-review/github/install/callback" ||
62
+ path === "/v1/pr-review/github/oauth/callback"
63
+ ) {
64
+ return true;
65
+ }
54
66
  if (
55
67
  path === "/v1/integrations/oauth/callback" ||
56
68
  path === "/v1/integrations/provider-oauth/callback" ||
@@ -83,6 +95,9 @@ function isAuthExempt(c: Context, settings: Settings): boolean {
83
95
  if (githubConnectPathPattern.test(path)) {
84
96
  return true;
85
97
  }
98
+ if (prReviewGithubBrowserPathPattern.test(path)) {
99
+ return true;
100
+ }
86
101
  // Compatibility endpoint for stale chooser submissions. It remains public
87
102
  // only so already-rendered forms can authenticate their signed account and
88
103
  // workspace state locally before terminating with 410; it does not parse a
package/src/http/sse.ts CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  coalesceSessionEventDeltas,
14
14
  formatSessionEventSse,
15
15
  formatWorkspaceControlEventSse,
16
+ requireSessionEventDurableFanoutCapability,
16
17
  SESSION_EVENT_SSE_FRAME_MAX_BYTES,
17
18
  sessionEventResumeSequence,
18
19
  type EventBus,
@@ -269,12 +270,14 @@ export async function sseSessionStream(
269
270
  signal: AbortSignal,
270
271
  options: SessionSseDeliveryOptions = {},
271
272
  ): Promise<Response> {
273
+ const durableFanout = requireSessionEventDurableFanoutCapability(bus);
272
274
  const heartbeatIntervalMs = resolveHeartbeatInterval(options.heartbeatIntervalMs);
273
275
  let lastSent = after;
274
276
  let bootstrapping = true;
275
277
  let newestBuffered: SessionEvent | null = null;
276
278
  let unsubscribe: (() => void) | null = null;
277
279
  let delivery: LatestWinsDelivery<SessionEvent> | null = null;
280
+ let stopReconnectObservation = () => {};
278
281
  let stopReauthorization = () => {};
279
282
  let heartbeatTimer: ReturnType<typeof setTimeout> | null = null;
280
283
  let detachAbortListener = () => {};
@@ -282,6 +285,8 @@ export async function sseSessionStream(
282
285
  const stopUpstream = () => {
283
286
  closeMetrics();
284
287
  detachAbortListener();
288
+ stopReconnectObservation();
289
+ stopReconnectObservation = () => {};
285
290
  stopReauthorization();
286
291
  if (heartbeatTimer) {
287
292
  clearTimeout(heartbeatTimer);
@@ -316,17 +321,6 @@ export async function sseSessionStream(
316
321
  writeTail = write.catch(() => {});
317
322
  return write;
318
323
  };
319
- const scheduleHeartbeat = () => {
320
- if (channel.stopped()) return;
321
- heartbeatTimer = setTimeout(() => {
322
- heartbeatTimer = null;
323
- void writeFrame(": heartbeat\n\n")
324
- .then(scheduleHeartbeat)
325
- .catch((error) => {
326
- if (!(error instanceof SseStreamStoppedError)) fail(error);
327
- });
328
- }, heartbeatIntervalMs);
329
- };
330
324
  const deliverDurableThrough = async (targetSequence?: number) => {
331
325
  while (true) {
332
326
  if (targetSequence !== undefined && lastSent >= targetSequence) return;
@@ -364,12 +358,66 @@ export async function sseSessionStream(
364
358
  if (targetSequence === undefined && page.length < limit) return;
365
359
  }
366
360
  };
361
+ let durableDeliveryTail = Promise.resolve();
362
+ const reconcileDurableThrough = (targetSequence?: number): Promise<void> => {
363
+ const deliveryRun = durableDeliveryTail.then(() => deliverDurableThrough(targetSequence));
364
+ durableDeliveryTail = deliveryRun.catch(() => {});
365
+ return deliveryRun;
366
+ };
367
+ let newestReconnectGeneration = 0;
368
+ let reconnectReconcilePending = false;
369
+ let reconnectReconcileRunning = false;
370
+ const drainReconnectReconciliation = () => {
371
+ if (
372
+ bootstrapping ||
373
+ reconnectReconcileRunning ||
374
+ !reconnectReconcilePending ||
375
+ channel.stopped()
376
+ ) {
377
+ return;
378
+ }
379
+ reconnectReconcileRunning = true;
380
+ void (async () => {
381
+ while (reconnectReconcilePending && !channel.stopped()) {
382
+ // Multiple reconnects during one durable read collapse into one newest
383
+ // catch-up. Postgres is authoritative, so that later read covers every
384
+ // disconnect window without one query per heartbeat or buffered event.
385
+ reconnectReconcilePending = false;
386
+ await reconcileDurableThrough();
387
+ }
388
+ })()
389
+ .catch((error) => {
390
+ if (!(error instanceof SseStreamStoppedError)) fail(error);
391
+ })
392
+ .finally(() => {
393
+ reconnectReconcileRunning = false;
394
+ drainReconnectReconciliation();
395
+ });
396
+ };
397
+ const scheduleReconnectReconciliation = (generation: number) => {
398
+ if (generation <= newestReconnectGeneration || channel.stopped()) return;
399
+ newestReconnectGeneration = generation;
400
+ reconnectReconcilePending = true;
401
+ drainReconnectReconciliation();
402
+ };
367
403
  const send = async (event: SessionEvent) => {
368
404
  const targetSequence = sessionEventResumeSequence(event);
369
405
  if (targetSequence <= lastSent) return;
370
- await deliverDurableThrough(targetSequence);
406
+ await reconcileDurableThrough(targetSequence);
407
+ };
408
+ const scheduleHeartbeat = () => {
409
+ if (channel.stopped()) return;
410
+ heartbeatTimer = setTimeout(() => {
411
+ heartbeatTimer = null;
412
+ void writeFrame(": heartbeat\n\n")
413
+ .then(scheduleHeartbeat)
414
+ .catch((error) => {
415
+ if (!(error instanceof SseStreamStoppedError)) fail(error);
416
+ });
417
+ }, heartbeatIntervalMs);
371
418
  };
372
419
  delivery = createLatestWinsDelivery(send, fail);
420
+ stopReconnectObservation = durableFanout.subscribeRecovery(scheduleReconnectReconciliation);
373
421
 
374
422
  void (async () => {
375
423
  const release = await bus.subscribe(workspaceId, sessionId, (events) => {
@@ -389,10 +437,11 @@ export async function sseSessionStream(
389
437
  }
390
438
  unsubscribe = release;
391
439
 
392
- await deliverDurableThrough();
440
+ await reconcileDurableThrough();
393
441
  await writeFrame(": connected\n\n");
394
442
  scheduleHeartbeat();
395
443
  bootstrapping = false;
444
+ drainReconnectReconciliation();
396
445
  const buffered = newestBuffered;
397
446
  newestBuffered = null;
398
447
  if (buffered) delivery.publish([buffered]);
@@ -1,4 +1,4 @@
1
- import type { Session } from "@opengeni/contracts";
1
+ import { AUTOMATIC_SESSION_TITLE_FALLBACK, type Session } from "@opengeni/contracts";
2
2
  import type { SlackHomeBlock } from "./slack-bot";
3
3
 
4
4
  const ATTENTION_LIMIT = 5;
@@ -186,7 +186,7 @@ function appendSessionGroup(
186
186
  );
187
187
  for (const session of sessions) {
188
188
  const url = sessionUrl(session.id);
189
- const title = (session.title || session.initialMessage || "Untitled task").slice(0, 180);
189
+ const title = (session.title?.trim() || AUTOMATIC_SESSION_TITLE_FALLBACK).slice(0, 180);
190
190
  blocks.push({
191
191
  type: "section",
192
192
  block_id: `opengeni_home_session_${session.id}`,
@@ -207,6 +207,11 @@ export type SlackMessageBlock =
207
207
  style?: "primary" | "danger";
208
208
  }>;
209
209
  }
210
+ | {
211
+ type: "context";
212
+ block_id?: string;
213
+ elements: Array<{ type: "mrkdwn" | "plain_text"; text: string; emoji?: boolean }>;
214
+ }
210
215
  | { type: "divider" };
211
216
 
212
217
  export type SlackHomeBlock =