@agent-team-foundation/first-tree-hub 0.12.7 → 0.12.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.
- package/dist/cli/index.mjs +39 -4
- package/dist/{client-93HZWg84-MIPzQD9A.mjs → client-CjGIGddS-BrpazWa3.mjs} +2 -2
- package/dist/{client-h5l7mi0m-OEX7MOBg.mjs → client-OMwJMCQt-R1T06ZH6.mjs} +394 -279
- package/dist/{dist-CTkhS6p5.mjs → dist-CnjqakXS.mjs} +39 -10
- package/dist/drizzle/0038_chat_membership_user_state.sql +223 -0
- package/dist/drizzle/0039_drop_chat_participants_subscriptions.sql +26 -0
- package/dist/drizzle/0040_chat_user_state_engagement.sql +24 -0
- package/dist/drizzle/meta/_journal.json +21 -0
- package/dist/{feishu-DJm0EaZP.mjs → feishu-DrnBbl8T.mjs} +1 -1
- package/dist/index.mjs +4 -4
- package/dist/{invitation-C299fxkP-jQiGR5fl.mjs → invitation-C299fxkP-KKslbta2.mjs} +1 -1
- package/dist/{saas-connect-CY2NxeKx.mjs → saas-connect-CXZhK485.mjs} +200 -117
- package/dist/web/assets/{index-JGwkYWtM.js → index-BPMrSv_A.js} +1 -1
- package/dist/web/assets/index-DxAYxUpz.css +1 -0
- package/dist/web/assets/index-ntmzuk5X.js +421 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-BKbK8BhK.css +0 -1
- package/dist/web/assets/index-BNM-YSSu.js +0 -421
|
@@ -2,10 +2,10 @@ import { a as __toCommonJS, o as __toESM, t as __commonJSMin } from "./chunk-BSw
|
|
|
2
2
|
import { A as FIRST_TREE_HUB_ATTR, C as stampOrgScope, D as untrustedAttrs, E as startWsConnectionSpan, M as require_pino, O as withSpan, S as stampChatResource, _ as rootLogger$1, a as buildRateLimitError, c as currentTraceId, g as reportErrorToRoot, i as bodyCaptureOnSendHook, j as redactUrl, k as withWsMessageSpan, l as decodeJwtForTrace, m as observabilityPlugin, n as applyLoggerConfig, o as classifyJoseError, r as attachRequestContext, s as createLogger$1, t as adapterAttrs, u as endWsConnectionSpan, x as stampAgentResource, y as setWsConnectionAttrs } from "./observability-BAScT_5S-BcW9HgkG.mjs";
|
|
3
3
|
import { C as resetConfigMeta, E as setConfigValue, S as resetConfig, T as serverConfigSchema, b as migrateLegacyHome, c as resolveServerUrl, d as DEFAULT_CONFIG_DIR, f as DEFAULT_DATA_DIR$1, g as collectMissingPrompts, h as clientConfigSchema, i as ensureFreshAccessToken, l as saveAgentConfig, m as agentConfigSchema, o as loadCredentials, p as DEFAULT_HOME_DIR$1, u as saveCredentials, v as initConfig, w as resolveConfigReadonly, y as loadAgents } from "./bootstrap-BCZC1ki6.mjs";
|
|
4
4
|
import { a as print, i as blank, n as CLI_USER_AGENT, r as COMMAND_VERSION, s as status, t as cliFetch } from "./cli-fetch--tiwKm5S.mjs";
|
|
5
|
-
import { A as
|
|
5
|
+
import { $ as patchOnboardingSchema, A as defaultRuntimeConfigPayload, B as inboxDeliverFrameSchema$1, C as createAdapterMappingSchema, Ct as updateOrganizationSchema, D as createMemberSchema, E as createMeChatSchema, F as githubCallbackQuerySchema, G as joinByInvitationSchema, H as isOrgSettingNamespace, I as githubDevCallbackQuerySchema, J as messageSourceSchema$1, K as listMeChatsQuerySchema, L as githubStartQuerySchema, M as dryRunAgentRuntimeConfigSchema, O as createOrgFromMeSchema, P as githubAppInstallationClaimBodySchema, Q as patchChatEngagementSchema, R as imageInlineContentSchema, S as createAdapterConfigSchema, St as updateMemberSchema, T as createChatSchema, U as isRedactedEnvValue, V as inboxPollQuerySchema, W as isReservedAgentName$1, X as onboardingEventSchema, Y as notificationQuerySchema, Z as paginationQuerySchema, _ as chatMetadataSchema$1, _t as updateAdapterConfigSchema, a as AGENT_VISIBILITY, at as safeRedirectPath, b as connectTokenExchangeSchema, bt as updateChatSchema, c as MENTION_REGEX, ct as sendMessageSchema, d as addMeChatParticipantsSchema, dt as sessionEventMessageSchema, f as addParticipantSchema, ft as sessionEventSchema$1, g as agentTypeSchema$1, gt as submitQuestionAnswerSchema, h as agentRuntimeConfigPayloadSchema$1, ht as stripCode, i as AGENT_STATUSES, it as runtimeStateMessageSchema, j as delegateFeishuUserSchema, l as ORG_SETTINGS_NAMESPACES$1, lt as sendToAgentSchema, m as agentPinnedMessageSchema$1, mt as sessionStateMessageSchema, n as AGENT_NAME_REGEX$1, nt as rebindAgentSchema, o as CHAT_ENGAGEMENT_STATUSES, p as agentBindRequestSchema, pt as sessionReconcileRequestSchema, q as loginSchema, r as AGENT_SELECTOR_HEADER$1, rt as refreshTokenSchema, st as selfServiceFeishuBotSchema, t as AGENT_BIND_REJECT_REASONS, u as WS_AUTH_FRAME_TIMEOUT_MS, ut as sessionCompletionMessageSchema, vt as updateAgentRuntimeConfigSchema, w as createAgentSchema, wt as wsAuthFrameSchema, x as contextTreeSnapshotSchema, xt as updateClientCapabilitiesSchema, y as clientRegisterSchema, yt as updateAgentSchema, z as inboxAckFrameSchema } from "./dist-CnjqakXS.mjs";
|
|
6
6
|
import { a as ConflictError, c as UnauthorizedError, i as ClientUserMismatchError$1, l as organizations, n as BadRequestError, o as ForbiddenError, r as ClientOrgMismatchError$1, s as NotFoundError, t as AppError, u as users } from "./errors-CF5evtJt-B0NTIVPt.mjs";
|
|
7
7
|
import { n as init_esm, r as trace, t as esm_exports } from "./esm-iadMkGbV.mjs";
|
|
8
|
-
import { $ as
|
|
8
|
+
import { $ as notifyRecipients, A as getPresence, B as listAgentsManagedByUser, C as ensureParticipant, D as getChatDetail, E as getCachedAudience, F as joinAsParticipant, G as listClients, H as listChatParticipantsWithNames, I as joinChat, K as listClientsForOrgAdmin, L as leaveAsParticipant, M as heartbeatInstance, N as inboxEntries, O as getClient, P as invalidateChatAudience, Q as messages, R as leaveChat, S as ensureCanJoin, T as getActivityOverview, U as listChats, V as listAgentsWithRuntime, W as listChatsForMember, X as markSupersededByChat, Y as markStaleAgents, Z as members, _ as createChat, _t as unbindAgent, a as agentVisibilityCondition, at as registerClient, b as disconnectClient, c as assertParticipant, ct as resolveChatMembership, d as chatMembership, dt as sendToAgent$1, et as pendingQuestions, f as chats, ft as serverInstances, g as clients, gt as touchAgent, h as cleanupStalePresence, ht as submitAnswer, i as agentPresence, it as registerChatMessageDispatcher, j as heartbeatClient, k as getOnlineCount, l as bindAgent, lt as retireClient, m as cleanupStaleClients, mt as setRuntimeState, n as addParticipant, nt as recomputeWatchersForAgent, o as agents, ot as removeParticipant, p as claimClient, pt as setOffline, q as listMessages, r as agentChatSessions, rt as recomputeWatchersForMember, s as assertClientOwner, st as resetActivity, t as addChatParticipants, tt as recomputeChatWatchers, u as changeChatType, ut as sendMessage, v as createNotifier, vt as updateClientCapabilities, w as findOrCreateDirectChat, x as editMessage, y as deriveAuthState, yt as upsertSessionState, z as listActiveAgentsPinnedToClient } from "./client-OMwJMCQt-R1T06ZH6.mjs";
|
|
9
9
|
import { a as invitationRedemptions, c as recordRedemption, i as getActiveInvitation, l as rotateInvitation, n as ensureActiveInvitation, o as invitations, r as findActiveByToken, t as buildInviteUrl, u as uuidv7 } from "./invitation-Bg0TRiyx-BsZH4GCS.mjs";
|
|
10
10
|
import { createRequire } from "node:module";
|
|
11
11
|
import { ZodError, z } from "zod";
|
|
@@ -764,6 +764,12 @@ const chatTypeSchema = z.enum([
|
|
|
764
764
|
"group",
|
|
765
765
|
"thread"
|
|
766
766
|
]);
|
|
767
|
+
const chatEngagementStatusSchema = z.enum([
|
|
768
|
+
"active",
|
|
769
|
+
"archived",
|
|
770
|
+
"deleted"
|
|
771
|
+
]);
|
|
772
|
+
z.object({ status: chatEngagementStatusSchema });
|
|
767
773
|
z.object({
|
|
768
774
|
type: chatTypeSchema,
|
|
769
775
|
topic: z.string().max(500).optional(),
|
|
@@ -793,7 +799,8 @@ z.object({
|
|
|
793
799
|
}).extend({
|
|
794
800
|
participants: z.array(chatParticipantSchema),
|
|
795
801
|
title: z.string(),
|
|
796
|
-
firstMessagePreview: z.string().nullable()
|
|
802
|
+
firstMessagePreview: z.string().nullable(),
|
|
803
|
+
engagementStatus: chatEngagementStatusSchema
|
|
797
804
|
});
|
|
798
805
|
z.object({ topic: z.string().trim().max(500).nullable() });
|
|
799
806
|
z.object({ agentId: z.string().min(1) });
|
|
@@ -1242,10 +1249,16 @@ const meChatFilterSchema = z.enum([
|
|
|
1242
1249
|
"watching"
|
|
1243
1250
|
]);
|
|
1244
1251
|
const meChatMembershipKindSchema = z.enum(["participant", "watching"]);
|
|
1252
|
+
const chatEngagementViewSchema = z.enum([
|
|
1253
|
+
"active",
|
|
1254
|
+
"archived",
|
|
1255
|
+
"all"
|
|
1256
|
+
]);
|
|
1245
1257
|
z.object({
|
|
1246
1258
|
cursor: z.string().optional(),
|
|
1247
1259
|
limit: z.coerce.number().int().min(1).max(200).default(50),
|
|
1248
|
-
filter: meChatFilterSchema.default("all")
|
|
1260
|
+
filter: meChatFilterSchema.default("all"),
|
|
1261
|
+
engagement: chatEngagementViewSchema.default("active")
|
|
1249
1262
|
});
|
|
1250
1263
|
const meChatParticipantSchema = z.object({
|
|
1251
1264
|
agentId: z.string(),
|
|
@@ -1263,7 +1276,8 @@ const meChatRowSchema = z.object({
|
|
|
1263
1276
|
lastMessageAt: z.string().nullable(),
|
|
1264
1277
|
lastMessagePreview: z.string().nullable(),
|
|
1265
1278
|
unreadMentionCount: z.number().int(),
|
|
1266
|
-
canReply: z.boolean()
|
|
1279
|
+
canReply: z.boolean(),
|
|
1280
|
+
engagementStatus: chatEngagementStatusSchema
|
|
1267
1281
|
});
|
|
1268
1282
|
z.object({
|
|
1269
1283
|
rows: z.array(meChatRowSchema),
|
|
@@ -3240,7 +3254,9 @@ You are running inside **Agent Hub**, a messaging platform for agent teams.
|
|
|
3240
3254
|
- **Your final text response is automatically delivered** to the chat — just respond normally
|
|
3241
3255
|
- For **proactive communication** (sending to other agents, other chats, or structured data),
|
|
3242
3256
|
use the \`first-tree-hub\` CLI below
|
|
3243
|
-
- **Use your judgment about when to respond.** Not every message requires
|
|
3257
|
+
- **Use your judgment about when to respond.** Not every message requires
|
|
3258
|
+
a reply — if you have nothing new for the recipient, output nothing and
|
|
3259
|
+
the runtime will end the turn silently.
|
|
3244
3260
|
Your role and responsibilities are injected via the Hub-managed system prompt.
|
|
3245
3261
|
|
|
3246
3262
|
## Environment Variables
|
|
@@ -5844,6 +5860,11 @@ function createResultSink(deps) {
|
|
|
5844
5860
|
return { mentions: [trigger.senderId] };
|
|
5845
5861
|
}
|
|
5846
5862
|
return async function forwardResult(text) {
|
|
5863
|
+
if (text.trim().length === 0) {
|
|
5864
|
+
deps.clearTrigger();
|
|
5865
|
+
deps.log("silent turn: agent produced empty output, skipping delivery");
|
|
5866
|
+
return;
|
|
5867
|
+
}
|
|
5847
5868
|
const trigger = deps.getTrigger();
|
|
5848
5869
|
deps.clearTrigger();
|
|
5849
5870
|
const metadata = await buildMetadata(trigger);
|
|
@@ -9422,7 +9443,7 @@ async function onboardCreate(args) {
|
|
|
9422
9443
|
}
|
|
9423
9444
|
const runtimeAgent = args.type === "human" ? args.assistant : args.id;
|
|
9424
9445
|
if (args.feishuBotAppId && args.feishuBotAppSecret) {
|
|
9425
|
-
const { bindFeishuBot } = await import("./feishu-
|
|
9446
|
+
const { bindFeishuBot } = await import("./feishu-DrnBbl8T.mjs").then((n) => n.r);
|
|
9426
9447
|
const targetAgentUuid = args.type === "human" ? assistantUuid : primary.uuid;
|
|
9427
9448
|
if (!targetAgentUuid) print.line(`Warning: Cannot bind Feishu bot — no runtime agent available for "${args.id}".\n`);
|
|
9428
9449
|
else {
|
|
@@ -10635,7 +10656,7 @@ function createFeedbackHandler(config) {
|
|
|
10635
10656
|
return { handle };
|
|
10636
10657
|
}
|
|
10637
10658
|
//#endregion
|
|
10638
|
-
//#region ../server/dist/app-
|
|
10659
|
+
//#region ../server/dist/app-BpskScln.mjs
|
|
10639
10660
|
var import_fastify_opentelemetry = /* @__PURE__ */ __toESM(require_fastify_opentelemetry(), 1);
|
|
10640
10661
|
init_esm();
|
|
10641
10662
|
var __defProp = Object.defineProperty;
|
|
@@ -10742,7 +10763,7 @@ async function requireChatAccess(request, db) {
|
|
|
10742
10763
|
role: caller.role,
|
|
10743
10764
|
humanAgentId: caller.humanAgentId
|
|
10744
10765
|
};
|
|
10745
|
-
const [direct] = await db.select({ chatId:
|
|
10766
|
+
const [direct] = await db.select({ chatId: chatMembership.chatId }).from(chatMembership).where(and(eq(chatMembership.chatId, chatId), eq(chatMembership.agentId, caller.humanAgentId), eq(chatMembership.accessMode, "speaker"))).limit(1);
|
|
10746
10767
|
if (direct) {
|
|
10747
10768
|
stampOrgScope(request, scope);
|
|
10748
10769
|
stampChatResource(request, chat);
|
|
@@ -10751,7 +10772,7 @@ async function requireChatAccess(request, db) {
|
|
|
10751
10772
|
scope
|
|
10752
10773
|
};
|
|
10753
10774
|
}
|
|
10754
|
-
const participantRows = await db.select({ agentId:
|
|
10775
|
+
const participantRows = await db.select({ agentId: chatMembership.agentId }).from(chatMembership).where(and(eq(chatMembership.chatId, chatId), eq(chatMembership.accessMode, "speaker")));
|
|
10755
10776
|
if (participantRows.length === 0) throw new NotFoundError(`Chat "${chatId}" not found`);
|
|
10756
10777
|
const participantIds = participantRows.map((p) => p.agentId);
|
|
10757
10778
|
const [managed] = await db.select({ uuid: agents.uuid }).from(agents).where(and(inArray(agents.uuid, participantIds), eq(agents.managerId, caller.memberId))).limit(1);
|
|
@@ -11843,19 +11864,6 @@ async function findOrCreateChatForChannel(db, data) {
|
|
|
11843
11864
|
return chatId;
|
|
11844
11865
|
});
|
|
11845
11866
|
}
|
|
11846
|
-
/**
|
|
11847
|
-
* Ensure an agent is a participant of a chat (no-op if already). Mode is
|
|
11848
|
-
* derived via the canonical entrypoint — pre-fix this also wrote `mode:`
|
|
11849
|
-
* implicitly via schema default `'full'`, which is wrong for non-human
|
|
11850
|
-
* agents in a group chat (the bug §1.1 of the Phase 1 design doc fixes).
|
|
11851
|
-
*/
|
|
11852
|
-
async function ensureParticipant(db, chatId, agentId) {
|
|
11853
|
-
const [exists] = await db.select({ chatId: chatParticipants.chatId }).from(chatParticipants).where(and(eq(chatParticipants.chatId, chatId), eq(chatParticipants.agentId, agentId))).limit(1);
|
|
11854
|
-
if (!exists) await addChatParticipants(db, chatId, [{
|
|
11855
|
-
agentId,
|
|
11856
|
-
role: "member"
|
|
11857
|
-
}], { onConflictDoNothing: true });
|
|
11858
|
-
}
|
|
11859
11867
|
/** Store a cross-reference between internal and external message. */
|
|
11860
11868
|
async function createMessageReference(db, data) {
|
|
11861
11869
|
await db.insert(adapterMessageReferences).values({
|
|
@@ -11938,9 +11946,9 @@ async function buildClientMessagePayloadsForInbox(db, inboxId, items) {
|
|
|
11938
11946
|
const modeByChat = /* @__PURE__ */ new Map();
|
|
11939
11947
|
if (chatIds.length > 0) {
|
|
11940
11948
|
const rows = await db.select({
|
|
11941
|
-
chatId:
|
|
11942
|
-
mode:
|
|
11943
|
-
}).from(
|
|
11949
|
+
chatId: chatMembership.chatId,
|
|
11950
|
+
mode: chatMembership.mode
|
|
11951
|
+
}).from(chatMembership).where(and(eq(chatMembership.agentId, agentId), inArray(chatMembership.chatId, chatIds), eq(chatMembership.accessMode, "speaker")));
|
|
11944
11952
|
for (const r of rows) modeByChat.set(r.chatId, normaliseMode(r.mode));
|
|
11945
11953
|
}
|
|
11946
11954
|
const inReplyToIds = [...new Set(items.map((it) => it.message.inReplyTo).filter((id) => id !== null))];
|
|
@@ -12341,7 +12349,7 @@ async function prepareImageOutbound(db, notifier, chatId, data) {
|
|
|
12341
12349
|
* chat reply (see `services/message.ts` replyTo routing).
|
|
12342
12350
|
*/
|
|
12343
12351
|
async function collectTargetInboxes(db, chatId, inReplyTo) {
|
|
12344
|
-
const participants = await db.select({ inboxId: agents.inboxId }).from(
|
|
12352
|
+
const participants = await db.select({ inboxId: agents.inboxId }).from(chatMembership).innerJoin(agents, eq(chatMembership.agentId, agents.uuid)).where(and(eq(chatMembership.chatId, chatId), eq(chatMembership.accessMode, "speaker")));
|
|
12345
12353
|
const set = new Set(participants.map((p) => p.inboxId));
|
|
12346
12354
|
if (inReplyTo) {
|
|
12347
12355
|
const [original] = await db.select({ replyToInbox: messages.replyToInbox }).from(messages).where(eq(messages.id, inReplyTo)).limit(1);
|
|
@@ -15267,6 +15275,47 @@ async function bootstrapConfigRoutes(_app) {
|
|
|
15267
15275
|
return { allowedOrg: null };
|
|
15268
15276
|
});
|
|
15269
15277
|
}
|
|
15278
|
+
/**
|
|
15279
|
+
* Per-(chat, agent) user state — independent from membership structure.
|
|
15280
|
+
*
|
|
15281
|
+
* This is the third layer of the chat data model: while `chats` owns
|
|
15282
|
+
* the entity and `chat_membership` owns the structural relation
|
|
15283
|
+
* (who can speak, who watches), this table owns the user's private
|
|
15284
|
+
* state about a chat. The reason it lives apart: structural changes
|
|
15285
|
+
* (speaker ↔ watcher, manager rebind, recompute) must never overwrite
|
|
15286
|
+
* user-private state — physical separation makes that an invariant
|
|
15287
|
+
* rather than a service-layer discipline.
|
|
15288
|
+
*
|
|
15289
|
+
* Columns evolve incrementally as new per-user state is needed.
|
|
15290
|
+
* Currently:
|
|
15291
|
+
* - `last_read_at`, `unread_mention_count` — seeded by PR-A from
|
|
15292
|
+
* the legacy `chat_participants` / `chat_subscriptions` columns.
|
|
15293
|
+
* - `engagement_status` — added in 0040; per-(chat, user) view
|
|
15294
|
+
* state (active / archived / deleted). Auto-revives archived →
|
|
15295
|
+
* active on new message; deleted is sticky (only the user can
|
|
15296
|
+
* restore from the chat detail page).
|
|
15297
|
+
*
|
|
15298
|
+
* Future fields slated for this table: pinned, mute_until, draft,
|
|
15299
|
+
* custom_title, last_seen_at — each as a separate change.
|
|
15300
|
+
*
|
|
15301
|
+
* Rows are lazy-upserted on first user write (markRead / mention
|
|
15302
|
+
* counter bump / engagement transition). Reads use COALESCE for
|
|
15303
|
+
* defaults so callers see `'active'` etc. even when no row exists.
|
|
15304
|
+
* Service-layer integrity (no FK / CHECK / trigger).
|
|
15305
|
+
*
|
|
15306
|
+
* See proposals/chat-data-model-restructure.20260512.md §8.6.
|
|
15307
|
+
*/
|
|
15308
|
+
const chatUserState = pgTable("chat_user_state", {
|
|
15309
|
+
chatId: text("chat_id").notNull(),
|
|
15310
|
+
agentId: text("agent_id").notNull(),
|
|
15311
|
+
lastReadAt: timestamp("last_read_at", { withTimezone: true }),
|
|
15312
|
+
unreadMentionCount: integer("unread_mention_count").notNull().default(0),
|
|
15313
|
+
engagementStatus: text("engagement_status").notNull().default("active")
|
|
15314
|
+
}, (table) => [
|
|
15315
|
+
primaryKey({ columns: [table.chatId, table.agentId] }),
|
|
15316
|
+
index("idx_user_state_agent").on(table.agentId),
|
|
15317
|
+
index("idx_user_state_unread").on(table.agentId).where(sql`unread_mention_count > 0`)
|
|
15318
|
+
]);
|
|
15270
15319
|
/** Extract a plain-text summary from a message's JSONB content field.
|
|
15271
15320
|
* Used as the auto-title fallback in chat list rendering — see
|
|
15272
15321
|
* `me-chat.ts:resolveChatTitle` and `admin/chats.ts:getChat`.
|
|
@@ -15462,7 +15511,7 @@ async function transitionSessionState(db, agentId, chatId, target, from, organiz
|
|
|
15462
15511
|
async function filterSessionsByParticipant(db, sessions, participantAgentId) {
|
|
15463
15512
|
if (sessions.length === 0) return [];
|
|
15464
15513
|
const chatIds = sessions.map((s) => s.chatId);
|
|
15465
|
-
const participantRows = await db.select({ chatId:
|
|
15514
|
+
const participantRows = await db.select({ chatId: chatMembership.chatId }).from(chatMembership).where(and(inArray(chatMembership.chatId, chatIds), eq(chatMembership.agentId, participantAgentId), eq(chatMembership.accessMode, "speaker")));
|
|
15466
15515
|
const allowedChatIds = new Set(participantRows.map((r) => r.chatId));
|
|
15467
15516
|
return sessions.filter((s) => allowedChatIds.has(s.chatId));
|
|
15468
15517
|
}
|
|
@@ -15471,16 +15520,17 @@ async function filterSessionsByParticipant(db, sessions, participantAgentId) {
|
|
|
15471
15520
|
* workspace).
|
|
15472
15521
|
*
|
|
15473
15522
|
* Responsibilities:
|
|
15474
|
-
* - Cursor-paginated conversation list
|
|
15475
|
-
*
|
|
15523
|
+
* - Cursor-paginated conversation list (single-stream JOIN over the
|
|
15524
|
+
* unified `chat_membership` + `chat_user_state` tables).
|
|
15476
15525
|
* - Create a new chat (no dedupe, runs `recomputeChatWatchers` after).
|
|
15477
|
-
* - Add participants (idempotent,
|
|
15478
|
-
*
|
|
15479
|
-
* -
|
|
15480
|
-
* -
|
|
15526
|
+
* - Add participants (idempotent, UPSERT into `chat_membership`,
|
|
15527
|
+
* runs `recomputeChatWatchers` after).
|
|
15528
|
+
* - Mark-read (UPSERT into `chat_user_state`).
|
|
15529
|
+
* - Join → watcher to speaker (delegates to `watcher.ts`).
|
|
15530
|
+
* - Leave → speaker to watcher or detach (delegates to `watcher.ts`).
|
|
15481
15531
|
*
|
|
15482
|
-
* See
|
|
15483
|
-
*
|
|
15532
|
+
* See proposals/chat-data-model-restructure.20260512.md §8 (schema)
|
|
15533
|
+
* and §11.1 (per-route mapping).
|
|
15484
15534
|
*/
|
|
15485
15535
|
function encodeCursor(lastMessageAt, chatId) {
|
|
15486
15536
|
const payload = `${lastMessageAt ? lastMessageAt.toISOString() : ""}|${chatId}`;
|
|
@@ -15504,17 +15554,61 @@ function decodeCursor(cursor) {
|
|
|
15504
15554
|
return null;
|
|
15505
15555
|
}
|
|
15506
15556
|
}
|
|
15557
|
+
const { ACTIVE, ARCHIVED, DELETED } = CHAT_ENGAGEMENT_STATUSES;
|
|
15558
|
+
/**
|
|
15559
|
+
* SQL predicate for each engagement view tab. `deleted` is never a valid view
|
|
15560
|
+
* value — deleted rows are reachable only through `GET /chats/:chatId` + the
|
|
15561
|
+
* Restore banner on the chat detail page.
|
|
15562
|
+
*/
|
|
15563
|
+
const ENGAGEMENT_VIEW_PREDICATE = {
|
|
15564
|
+
active: sql`COALESCE(cus.engagement_status, ${ACTIVE}) = ${ACTIVE}`,
|
|
15565
|
+
archived: sql`COALESCE(cus.engagement_status, ${ACTIVE}) = ${ARCHIVED}`,
|
|
15566
|
+
all: sql`COALESCE(cus.engagement_status, ${ACTIVE}) IN (${ACTIVE}, ${ARCHIVED})`
|
|
15567
|
+
};
|
|
15568
|
+
/**
|
|
15569
|
+
* Write the caller's engagement state for this chat. UPSERT into
|
|
15570
|
+
* `chat_user_state` — the row may not yet exist (the user might not have
|
|
15571
|
+
* marked-read or been @-mentioned), so an INSERT with the engagement value
|
|
15572
|
+
* is the first write; subsequent transitions are UPDATEs.
|
|
15573
|
+
*
|
|
15574
|
+
* Idempotent. Mirrors the UPSERT shape used by `markMeChatRead`.
|
|
15575
|
+
*/
|
|
15576
|
+
async function setChatEngagement(db, chatId, agentId, status) {
|
|
15577
|
+
await db.insert(chatUserState).values({
|
|
15578
|
+
chatId,
|
|
15579
|
+
agentId,
|
|
15580
|
+
unreadMentionCount: 0,
|
|
15581
|
+
engagementStatus: status
|
|
15582
|
+
}).onConflictDoUpdate({
|
|
15583
|
+
target: [chatUserState.chatId, chatUserState.agentId],
|
|
15584
|
+
set: { engagementStatus: status }
|
|
15585
|
+
});
|
|
15586
|
+
}
|
|
15587
|
+
/**
|
|
15588
|
+
* Read the caller's engagement state. Returns `'active'` when no
|
|
15589
|
+
* `chat_user_state` row exists yet (lazy-materialised; matches the SQL
|
|
15590
|
+
* `COALESCE(..., 'active')` used elsewhere).
|
|
15591
|
+
*/
|
|
15592
|
+
async function getCallerEngagement(db, chatId, agentId) {
|
|
15593
|
+
const [row] = await db.select({ engagementStatus: chatUserState.engagementStatus }).from(chatUserState).where(and(eq(chatUserState.chatId, chatId), eq(chatUserState.agentId, agentId))).limit(1);
|
|
15594
|
+
return row?.engagementStatus ?? ACTIVE;
|
|
15595
|
+
}
|
|
15507
15596
|
/**
|
|
15508
15597
|
* GET /me/chats — cursor-paginated conversation list.
|
|
15509
15598
|
*
|
|
15510
15599
|
* SQL strategy:
|
|
15511
|
-
* -
|
|
15512
|
-
*
|
|
15513
|
-
*
|
|
15514
|
-
*
|
|
15600
|
+
* - Single-stream query: `chats JOIN chat_membership LEFT JOIN
|
|
15601
|
+
* chat_user_state`. The membership row carries access_mode
|
|
15602
|
+
* (speaker → "participant" / watcher → "watching"); the user
|
|
15603
|
+
* state row supplies the unread counter (COALESCE → 0 when
|
|
15604
|
+
* row is missing).
|
|
15605
|
+
* - Filter `parent_chat_id IS NULL` (threads excluded in v1).
|
|
15606
|
+
* - Filter `c.organization_id = ?` to defend against historical
|
|
15607
|
+
* cross-org pollution rows that may still reference the caller
|
|
15608
|
+
* (see fix/cross-org-direct-chat-pollution).
|
|
15515
15609
|
* - Sort `(last_message_at DESC NULLS LAST, chat_id DESC)`.
|
|
15516
|
-
* - Cursor narrows the result to rows STRICTLY before
|
|
15517
|
-
* - Followed by a
|
|
15610
|
+
* - Cursor narrows the result to rows STRICTLY before the cursor.
|
|
15611
|
+
* - Followed by a participants-list lookup for the page only.
|
|
15518
15612
|
*/
|
|
15519
15613
|
async function listMeChats(db, humanAgentId, organizationId, query) {
|
|
15520
15614
|
const limit = query.limit;
|
|
@@ -15522,28 +15616,12 @@ async function listMeChats(db, humanAgentId, organizationId, query) {
|
|
|
15522
15616
|
if (query.cursor && !cursor) throw new BadRequestError("Invalid cursor");
|
|
15523
15617
|
const filterUnreadOnly = query.filter === "unread";
|
|
15524
15618
|
const filterWatchingOnly = query.filter === "watching";
|
|
15619
|
+
const engagementPredicate = ENGAGEMENT_VIEW_PREDICATE[query.engagement];
|
|
15525
15620
|
const cursorTsIso = cursor?.lastMessageAt ? cursor.lastMessageAt.toISOString() : null;
|
|
15526
15621
|
const cursorPredicate = !cursor ? sql`TRUE` : cursor.lastMessageAt === null ? sql`(c.last_message_at IS NULL AND c.id < ${cursor.chatId})` : sql`(c.last_message_at IS NULL
|
|
15527
15622
|
OR c.last_message_at < ${cursorTsIso}::timestamptz
|
|
15528
15623
|
OR (c.last_message_at = ${cursorTsIso}::timestamptz AND c.id < ${cursor.chatId}))`;
|
|
15529
15624
|
const rawRows = await db.execute(sql`
|
|
15530
|
-
WITH membership AS (
|
|
15531
|
-
SELECT chat_id, 'participant'::text AS membership_kind, unread_mention_count
|
|
15532
|
-
FROM chat_participants
|
|
15533
|
-
WHERE agent_id = ${humanAgentId}
|
|
15534
|
-
UNION ALL
|
|
15535
|
-
SELECT chat_id, 'watching'::text AS membership_kind, unread_mention_count
|
|
15536
|
-
FROM chat_subscriptions
|
|
15537
|
-
WHERE agent_id = ${humanAgentId}
|
|
15538
|
-
),
|
|
15539
|
-
/* Resolve duplicates (should not happen post-invariant-1, but cheap) by
|
|
15540
|
-
preferring the participant row. */
|
|
15541
|
-
deduped AS (
|
|
15542
|
-
SELECT DISTINCT ON (chat_id)
|
|
15543
|
-
chat_id, membership_kind, unread_mention_count
|
|
15544
|
-
FROM membership
|
|
15545
|
-
ORDER BY chat_id, CASE WHEN membership_kind = 'participant' THEN 0 ELSE 1 END
|
|
15546
|
-
)
|
|
15547
15625
|
SELECT
|
|
15548
15626
|
c.id AS chat_id,
|
|
15549
15627
|
c.type AS type,
|
|
@@ -15551,20 +15629,26 @@ async function listMeChats(db, humanAgentId, organizationId, query) {
|
|
|
15551
15629
|
c.parent_chat_id AS parent_chat_id,
|
|
15552
15630
|
c.last_message_at AS last_message_at,
|
|
15553
15631
|
c.last_message_preview AS last_message_preview,
|
|
15554
|
-
(SELECT count(*) FROM
|
|
15555
|
-
|
|
15556
|
-
|
|
15632
|
+
(SELECT count(*) FROM chat_membership
|
|
15633
|
+
WHERE chat_id = c.id AND access_mode = 'speaker') AS participant_count,
|
|
15634
|
+
cm.access_mode AS access_mode,
|
|
15635
|
+
COALESCE(cus.unread_mention_count, 0) AS unread_mention_count,
|
|
15636
|
+
COALESCE(cus.engagement_status, ${ACTIVE}) AS engagement_status
|
|
15557
15637
|
FROM chats c
|
|
15558
|
-
JOIN
|
|
15638
|
+
JOIN chat_membership cm
|
|
15639
|
+
ON cm.chat_id = c.id AND cm.agent_id = ${humanAgentId}
|
|
15640
|
+
LEFT JOIN chat_user_state cus
|
|
15641
|
+
ON cus.chat_id = c.id AND cus.agent_id = ${humanAgentId}
|
|
15559
15642
|
WHERE c.parent_chat_id IS NULL
|
|
15560
|
-
/* Scope to the caller's org. Without this, cross-org dirty
|
|
15561
|
-
whose
|
|
15562
|
-
(historical pollution — see
|
|
15563
|
-
would leak into the
|
|
15643
|
+
/* Scope to the caller's org. Without this, cross-org dirty
|
|
15644
|
+
chats whose chat_membership still references the caller's
|
|
15645
|
+
human agent (historical pollution — see
|
|
15646
|
+
fix/cross-org-direct-chat-pollution) would leak into the
|
|
15647
|
+
list and 404 on click via requireChatAccess. */
|
|
15564
15648
|
AND c.organization_id = ${organizationId}
|
|
15565
|
-
|
|
15566
|
-
AND (${!
|
|
15567
|
-
AND
|
|
15649
|
+
AND (${!filterUnreadOnly}::bool OR COALESCE(cus.unread_mention_count, 0) > 0)
|
|
15650
|
+
AND (${!filterWatchingOnly}::bool OR cm.access_mode = 'watcher')
|
|
15651
|
+
AND ${engagementPredicate}
|
|
15568
15652
|
AND ${cursorPredicate}
|
|
15569
15653
|
ORDER BY c.last_message_at DESC NULLS LAST, c.id DESC
|
|
15570
15654
|
LIMIT ${limit + 1}
|
|
@@ -15583,11 +15667,11 @@ async function listMeChats(db, humanAgentId, organizationId, query) {
|
|
|
15583
15667
|
};
|
|
15584
15668
|
const chatIds = pageRaw.map((r) => r.chat_id);
|
|
15585
15669
|
const participantRows = await db.select({
|
|
15586
|
-
chatId:
|
|
15587
|
-
agentId:
|
|
15670
|
+
chatId: chatMembership.chatId,
|
|
15671
|
+
agentId: chatMembership.agentId,
|
|
15588
15672
|
displayName: agents.displayName,
|
|
15589
15673
|
type: agents.type
|
|
15590
|
-
}).from(
|
|
15674
|
+
}).from(chatMembership).innerJoin(agents, eq(chatMembership.agentId, agents.uuid)).where(and(inArray(chatMembership.chatId, chatIds), eq(chatMembership.accessMode, "speaker")));
|
|
15591
15675
|
const participantsByChat = /* @__PURE__ */ new Map();
|
|
15592
15676
|
for (const p of participantRows) {
|
|
15593
15677
|
const list = participantsByChat.get(p.chatId) ?? [];
|
|
@@ -15611,10 +15695,11 @@ async function listMeChats(db, humanAgentId, organizationId, query) {
|
|
|
15611
15695
|
rows: pageRaw.map((r) => {
|
|
15612
15696
|
const participants = participantsByChat.get(r.chat_id) ?? [];
|
|
15613
15697
|
const title = resolveChatTitle(r.topic, firstMessageSummary.get(r.chat_id) ?? null, participants, humanAgentId);
|
|
15698
|
+
const isSpeaker = r.access_mode === "speaker";
|
|
15614
15699
|
return {
|
|
15615
15700
|
chatId: r.chat_id,
|
|
15616
15701
|
type: r.type,
|
|
15617
|
-
membershipKind:
|
|
15702
|
+
membershipKind: isSpeaker ? "participant" : "watching",
|
|
15618
15703
|
title,
|
|
15619
15704
|
topic: r.topic,
|
|
15620
15705
|
participants,
|
|
@@ -15622,7 +15707,8 @@ async function listMeChats(db, humanAgentId, organizationId, query) {
|
|
|
15622
15707
|
lastMessageAt: toDate(r.last_message_at)?.toISOString() ?? null,
|
|
15623
15708
|
lastMessagePreview: r.last_message_preview,
|
|
15624
15709
|
unreadMentionCount: r.unread_mention_count,
|
|
15625
|
-
canReply:
|
|
15710
|
+
canReply: isSpeaker,
|
|
15711
|
+
engagementStatus: r.engagement_status
|
|
15626
15712
|
};
|
|
15627
15713
|
}),
|
|
15628
15714
|
nextCursor
|
|
@@ -15634,11 +15720,6 @@ async function listMeChats(db, humanAgentId, organizationId, query) {
|
|
|
15634
15720
|
* 1. `chat.topic` (manual, set via `PATCH /chats/:chatId`)
|
|
15635
15721
|
* 2. First message summary (auto, ≤ 50 chars from `extractSummary`)
|
|
15636
15722
|
* 3. Participant join (fallback when chat has no messages yet)
|
|
15637
|
-
*
|
|
15638
|
-
* The first-message fallback is the chat-first equivalent of how
|
|
15639
|
-
* ChatGPT / Claude.ai name conversations from the user's opening
|
|
15640
|
-
* prompt — gives same-agent multi-chats distinct identities and
|
|
15641
|
-
* removes the "title duplicates participants chip row" anti-pattern.
|
|
15642
15723
|
*/
|
|
15643
15724
|
function resolveChatTitle(topic, firstMessageSummary, participants, selfAgentId) {
|
|
15644
15725
|
if (topic && topic.length > 0) return topic;
|
|
@@ -15692,7 +15773,7 @@ async function addMeChatParticipants(db, chatId, callerHumanAgentId, callerOrgan
|
|
|
15692
15773
|
}).from(chats).where(eq(chats.id, chatId)).limit(1);
|
|
15693
15774
|
if (!chat) throw new NotFoundError(`Chat "${chatId}" not found`);
|
|
15694
15775
|
if (chat.organizationId !== callerOrganizationId) throw new NotFoundError(`Chat "${chatId}" not found`);
|
|
15695
|
-
const [callerRow] = await db.select({ chatId:
|
|
15776
|
+
const [callerRow] = await db.select({ chatId: chatMembership.chatId }).from(chatMembership).where(and(eq(chatMembership.chatId, chatId), eq(chatMembership.agentId, callerHumanAgentId), eq(chatMembership.accessMode, "speaker"))).limit(1);
|
|
15696
15777
|
if (!callerRow) throw new NotFoundError(`Chat "${chatId}" not found`);
|
|
15697
15778
|
const found = await db.select({
|
|
15698
15779
|
uuid: agents.uuid,
|
|
@@ -15706,45 +15787,36 @@ async function addMeChatParticipants(db, chatId, callerHumanAgentId, callerOrgan
|
|
|
15706
15787
|
const crossOrg = found.filter((a) => a.organizationId !== chat.organizationId);
|
|
15707
15788
|
if (crossOrg.length > 0) throw new BadRequestError(`Cross-organization participant rejected: ${crossOrg.map((a) => a.uuid).join(", ")}`);
|
|
15708
15789
|
await db.transaction(async (tx) => {
|
|
15709
|
-
const
|
|
15710
|
-
const
|
|
15711
|
-
const
|
|
15712
|
-
if (
|
|
15790
|
+
const existingSpeakers = await tx.select({ agentId: chatMembership.agentId }).from(chatMembership).where(and(eq(chatMembership.chatId, chatId), eq(chatMembership.accessMode, "speaker")));
|
|
15791
|
+
const existingSpeakerSet = new Set(existingSpeakers.map((e) => e.agentId));
|
|
15792
|
+
const toUpsert = distinct.filter((id) => !existingSpeakerSet.has(id));
|
|
15793
|
+
if (toUpsert.length === 0) {
|
|
15713
15794
|
await recomputeChatWatchers(tx, chatId);
|
|
15714
15795
|
return;
|
|
15715
15796
|
}
|
|
15716
|
-
if (
|
|
15717
|
-
|
|
15718
|
-
agentId
|
|
15719
|
-
|
|
15720
|
-
|
|
15721
|
-
});
|
|
15722
|
-
const carriedByAgent = new Map(carriedRows.map((r) => [r.agentId, r]));
|
|
15723
|
-
await addChatParticipants(tx, chatId, toInsert.map((agentId) => {
|
|
15724
|
-
const carried = carriedByAgent.get(agentId);
|
|
15725
|
-
return {
|
|
15726
|
-
agentId,
|
|
15727
|
-
role: "member",
|
|
15728
|
-
carriedReadState: carried ? {
|
|
15729
|
-
lastReadAt: carried.lastReadAt,
|
|
15730
|
-
unreadMentionCount: carried.unreadMentionCount
|
|
15731
|
-
} : void 0
|
|
15732
|
-
};
|
|
15733
|
-
}), { onConflictDoNothing: true });
|
|
15797
|
+
if (existingSpeakers.length + toUpsert.length >= 3 && chat.type === "direct") await changeChatType(tx, chatId, "group");
|
|
15798
|
+
await addChatParticipants(tx, chatId, toUpsert.map((agentId) => ({
|
|
15799
|
+
agentId,
|
|
15800
|
+
role: "member"
|
|
15801
|
+
})), { upgradeWatcherToSpeaker: true });
|
|
15734
15802
|
await recomputeChatWatchers(tx, chatId);
|
|
15735
15803
|
});
|
|
15736
15804
|
invalidateChatAudience(chatId);
|
|
15737
15805
|
}
|
|
15738
15806
|
async function markMeChatRead(db, chatId, humanAgentId) {
|
|
15739
15807
|
const now = /* @__PURE__ */ new Date();
|
|
15740
|
-
await db.
|
|
15741
|
-
|
|
15742
|
-
|
|
15743
|
-
}).where(and(eq(chatParticipants.chatId, chatId), eq(chatParticipants.agentId, humanAgentId)));
|
|
15744
|
-
await db.update(chatSubscriptions).set({
|
|
15808
|
+
await db.insert(chatUserState).values({
|
|
15809
|
+
chatId,
|
|
15810
|
+
agentId: humanAgentId,
|
|
15745
15811
|
lastReadAt: now,
|
|
15746
15812
|
unreadMentionCount: 0
|
|
15747
|
-
}).
|
|
15813
|
+
}).onConflictDoUpdate({
|
|
15814
|
+
target: [chatUserState.chatId, chatUserState.agentId],
|
|
15815
|
+
set: {
|
|
15816
|
+
lastReadAt: now,
|
|
15817
|
+
unreadMentionCount: 0
|
|
15818
|
+
}
|
|
15819
|
+
});
|
|
15748
15820
|
return {
|
|
15749
15821
|
chatId,
|
|
15750
15822
|
lastReadAt: now.toISOString(),
|
|
@@ -15770,7 +15842,7 @@ async function leaveMeChat(db, chatId, humanAgentId) {
|
|
|
15770
15842
|
async function chatRoutes(app) {
|
|
15771
15843
|
app.get("/:chatId", async (request) => {
|
|
15772
15844
|
const { chat, scope } = await requireChatAccess(request, app.db);
|
|
15773
|
-
const participants = await app.db.select().from(
|
|
15845
|
+
const participants = await app.db.select().from(chatMembership).where(and(eq(chatMembership.chatId, chat.id), eq(chatMembership.accessMode, "speaker")));
|
|
15774
15846
|
const firstMsgRows = await app.db.execute(sql`
|
|
15775
15847
|
SELECT content FROM messages
|
|
15776
15848
|
WHERE chat_id = ${chat.id}
|
|
@@ -15794,10 +15866,12 @@ async function chatRoutes(app) {
|
|
|
15794
15866
|
};
|
|
15795
15867
|
});
|
|
15796
15868
|
const title = resolveChatTitle(chat.topic, firstMessagePreview, participantsForTitle, scope.humanAgentId);
|
|
15869
|
+
const engagementStatus = await getCallerEngagement(app.db, chat.id, scope.humanAgentId);
|
|
15797
15870
|
return {
|
|
15798
15871
|
...chat,
|
|
15799
15872
|
title,
|
|
15800
15873
|
firstMessagePreview,
|
|
15874
|
+
engagementStatus,
|
|
15801
15875
|
createdAt: chat.createdAt.toISOString(),
|
|
15802
15876
|
updatedAt: chat.updatedAt.toISOString(),
|
|
15803
15877
|
participants: participants.map((p) => ({
|
|
@@ -15808,6 +15882,15 @@ async function chatRoutes(app) {
|
|
|
15808
15882
|
}))
|
|
15809
15883
|
};
|
|
15810
15884
|
});
|
|
15885
|
+
app.post("/:chatId/engagement", { config: { otelRecordBody: true } }, async (request, reply) => {
|
|
15886
|
+
const { scope } = await requireChatAccess(request, app.db);
|
|
15887
|
+
const body = patchChatEngagementSchema.parse(request.body);
|
|
15888
|
+
await setChatEngagement(app.db, request.params.chatId, scope.humanAgentId, body.status);
|
|
15889
|
+
return reply.status(200).send({
|
|
15890
|
+
chatId: request.params.chatId,
|
|
15891
|
+
engagementStatus: body.status
|
|
15892
|
+
});
|
|
15893
|
+
});
|
|
15811
15894
|
app.patch("/:chatId", { config: { otelRecordBody: true } }, async (request) => {
|
|
15812
15895
|
await requireChatAccess(request, app.db);
|
|
15813
15896
|
const body = updateChatSchema.parse(request.body);
|
|
@@ -15900,7 +15983,7 @@ async function chatRoutes(app) {
|
|
|
15900
15983
|
app.post("/:chatId/messages", async (request, reply) => {
|
|
15901
15984
|
const { scope } = await requireChatAccess(request, app.db);
|
|
15902
15985
|
const body = sendMessageSchema.parse(request.body);
|
|
15903
|
-
await ensureParticipant
|
|
15986
|
+
await ensureParticipant(app.db, request.params.chatId, scope.humanAgentId);
|
|
15904
15987
|
const prepared = await prepareImageOutbound(app.db, app.notifier, request.params.chatId, {
|
|
15905
15988
|
...body,
|
|
15906
15989
|
source: "hub_ui"
|
|
@@ -15927,7 +16010,7 @@ async function chatRoutes(app) {
|
|
|
15927
16010
|
app.post("/:chatId/questions/:correlationId/answer", { config: { otelRecordBody: false } }, async (request, reply) => {
|
|
15928
16011
|
const { scope } = await requireChatAccess(request, app.db);
|
|
15929
16012
|
const body = submitQuestionAnswerSchema.parse(request.body);
|
|
15930
|
-
await ensureParticipant
|
|
16013
|
+
await ensureParticipant(app.db, request.params.chatId, scope.humanAgentId);
|
|
15931
16014
|
const result = await submitAnswer(app.db, app.notifier, {
|
|
15932
16015
|
correlationId: request.params.correlationId,
|
|
15933
16016
|
chatId: request.params.chatId,
|
|
@@ -17324,7 +17407,7 @@ async function healthzRoutes(app) {
|
|
|
17324
17407
|
* `api/orgs/invitations.ts` (Class B, admin-gated).
|
|
17325
17408
|
*/
|
|
17326
17409
|
async function publicInvitationRoutes(app) {
|
|
17327
|
-
const { previewInvitation } = await import("./invitation-C299fxkP-
|
|
17410
|
+
const { previewInvitation } = await import("./invitation-C299fxkP-KKslbta2.mjs");
|
|
17328
17411
|
app.get("/:token/preview", async (request, reply) => {
|
|
17329
17412
|
if (!request.params.token) throw new UnauthorizedError("Token required");
|
|
17330
17413
|
const preview = await previewInvitation(app.db, request.params.token);
|
|
@@ -17590,7 +17673,7 @@ async function meRoutes(app) {
|
|
|
17590
17673
|
*/
|
|
17591
17674
|
app.get("/me/pinned-agents", async (request) => {
|
|
17592
17675
|
const { userId } = requireUser(request);
|
|
17593
|
-
const { listMyPinnedAgents } = await import("./client-
|
|
17676
|
+
const { listMyPinnedAgents } = await import("./client-CjGIGddS-BrpazWa3.mjs");
|
|
17594
17677
|
return listMyPinnedAgents(app.db, { userId });
|
|
17595
17678
|
});
|
|
17596
17679
|
/**
|
|
@@ -18000,7 +18083,7 @@ async function orgChatRoutes(app) {
|
|
|
18000
18083
|
createdAt: chats.createdAt,
|
|
18001
18084
|
updatedAt: chats.updatedAt,
|
|
18002
18085
|
participantCount: sql`(
|
|
18003
|
-
SELECT count(*)::int FROM
|
|
18086
|
+
SELECT count(*)::int FROM chat_membership WHERE chat_id = ${chats.id} AND access_mode = 'speaker'
|
|
18004
18087
|
)`
|
|
18005
18088
|
}).from(chats).where(and(...conditions)).orderBy(desc(chats.createdAt)).limit(query.limit + 1);
|
|
18006
18089
|
const hasMore = rows.length > query.limit;
|
|
@@ -19526,8 +19609,8 @@ var schema_exports = /* @__PURE__ */ __exportAll({
|
|
|
19526
19609
|
agentPresence: () => agentPresence,
|
|
19527
19610
|
agents: () => agents,
|
|
19528
19611
|
authIdentities: () => authIdentities,
|
|
19529
|
-
|
|
19530
|
-
|
|
19612
|
+
chatMembership: () => chatMembership,
|
|
19613
|
+
chatUserState: () => chatUserState,
|
|
19531
19614
|
chats: () => chats,
|
|
19532
19615
|
clients: () => clients,
|
|
19533
19616
|
githubAppInstallations: () => githubAppInstallations,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{c as r,j as e,U as o,L as s,A as i,F as a,r as l}from"./index-
|
|
1
|
+
import{c as r,j as e,U as o,L as s,A as i,F as a,r as l}from"./index-ntmzuk5X.js";/**
|
|
2
2
|
* @license lucide-react v0.577.0 - ISC
|
|
3
3
|
*
|
|
4
4
|
* This source code is licensed under the ISC license.
|