@agent-team-foundation/first-tree-hub 0.13.0 → 0.14.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.
@@ -1,11 +1,11 @@
1
1
  import { a as __toCommonJS, o as __toESM, t as __commonJSMin } from "./chunk-BSw8zbkd.mjs";
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
- 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-Cya2OoHz.mjs";
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-C15ZBOCC.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 { $ as notificationQuerySchema, A as createMemberSchema, B as githubDevCallbackQuerySchema, C as connectTokenExchangeSchema, Ct as updateChatSchema, D as createAgentSchema, Dt as wsAuthFrameSchema, E as createAdapterMappingSchema, Et as updateOrganizationSchema, F as dryRunAgentRuntimeConfigSchema, G as inboxPollQuerySchema, H as imageInlineContentSchema, J as isReservedAgentName$1, K as isOrgSettingNamespace, L as githubAppInstallationClaimBodySchema, N as defaultRuntimeConfigPayload, O as createChatSchema, P as delegateFeishuUserSchema, Q as messageSourceSchema$1, R as githubAppInstallationPermissionsSchema$1, S as clientRegisterSchema, St as updateAgentSchema, T as createAdapterConfigSchema, Tt as updateMemberSchema, U as inboxAckFrameSchema, V as githubStartQuerySchema, W as inboxDeliverFrameSchema$1, X as listMeChatsQuerySchema, Y as joinByInvitationSchema, Z as loginSchema, _ as agentPinnedMessageSchema$1, _t as sessionStateMessageSchema, a as AGENT_TYPES, b as chatMetadataSchema$1, bt as updateAdapterConfigSchema, ct as runtimeStateMessageSchema, d as NOTIFICATION_TYPES, dt as selfServiceFeishuBotSchema, et as onboardingEventSchema, f as ORG_SETTINGS_NAMESPACES$1, ft as sendMessageSchema, g as agentBindRequestSchema, gt as sessionReconcileRequestSchema, h as addParticipantSchema, ht as sessionEventSchema$1, i as AGENT_STATUSES, j as createOrgFromMeSchema, k as createMeChatSchema, lt as safeRedirectPath, m as addMeChatParticipantsSchema, mt as sessionEventMessageSchema, n as AGENT_NAME_REGEX$1, nt as patchChatEngagementSchema, o as AGENT_VISIBILITY, ot as rebindAgentSchema, p as WS_AUTH_FRAME_TIMEOUT_MS, pt as sendToAgentSchema, q as isRedactedEnvValue, r as AGENT_SELECTOR_HEADER$1, rt as patchOnboardingSchema, s as CHAT_ENGAGEMENT_STATUSES, st as refreshTokenSchema, t as AGENT_BIND_REJECT_REASONS, tt as paginationQuerySchema, u as MENTION_REGEX, v as agentRuntimeConfigPayloadSchema$1, vt as stripCode, w as contextTreeSnapshotSchema, wt as updateClientCapabilitiesSchema, xt as updateAgentRuntimeConfigSchema, y as agentTypeSchema$1, yt as submitQuestionAnswerSchema, z as githubCallbackQuerySchema } from "./dist-C8yStx2L.mjs";
5
+ import { $ as listMeChatSourceCountsQuerySchema, A as createMeChatSchema, At as updateOrganizationSchema, B as githubAppInstallationClaimBodySchema, C as clientRegisterSchema, Ct as submitQuestionAnswerSchema, D as createAdapterMappingSchema, Dt as updateChatSchema, E as createAdapterConfigSchema, Et as updateAgentSchema, F as delegateFeishuUserSchema, G as imageInlineContentSchema, H as githubCallbackQuerySchema, I as dryRunAgentRuntimeConfigSchema, J as inboxPollQuerySchema, K as inboxAckFrameSchema, M as createOrgFromMeSchema, O as createAgentSchema, Ot as updateClientCapabilitiesSchema, P as defaultRuntimeConfigPayload, Q as joinByInvitationSchema, R as getMeDocResponseSchema, St as stripCode, T as contextTreeSnapshotSchema, Tt as updateAgentRuntimeConfigSchema, U as githubDevCallbackQuerySchema, V as githubAppInstallationPermissionsSchema$1, W as githubStartQuerySchema, X as isRedactedEnvValue, Y as isOrgSettingNamespace, Z as isReservedAgentName$1, _ as agentBindRequestSchema, _t as sendToAgentSchema, a as AGENT_TYPES, at as paginationQuerySchema, b as agentTypeSchema$1, bt as sessionReconcileRequestSchema, d as MENTION_REGEX, dt as refreshTokenSchema, et as listMeChatsQuerySchema, f as NOTIFICATION_TYPES, ft as runtimeStateMessageSchema, g as addParticipantSchema, gt as sendMessageSchema, h as addMeChatParticipantsSchema, ht as selfServiceFeishuBotSchema, i as AGENT_STATUSES, it as onboardingEventSchema, j as createMemberSchema, jt as wsAuthFrameSchema, k as createChatSchema, kt as updateMemberSchema, l as GITHUB_ENTITY_TYPES, m as WS_AUTH_FRAME_TIMEOUT_MS, n as AGENT_NAME_REGEX$1, nt as messageSourceSchema$1, o as AGENT_VISIBILITY, ot as patchChatEngagementSchema, p as ORG_SETTINGS_NAMESPACES$1, pt as safeRedirectPath, q as inboxDeliverFrameSchema$1, r as AGENT_SELECTOR_HEADER$1, rt as notificationQuerySchema, s as CHAT_ENGAGEMENT_STATUSES, st as patchOnboardingSchema, t as AGENT_BIND_REJECT_REASONS, tt as loginSchema, ut as rebindAgentSchema, v as agentPinnedMessageSchema$1, vt as sessionEventMessageSchema, w as connectTokenExchangeSchema, wt as updateAdapterConfigSchema, x as chatMetadataSchema$1, xt as sessionStateMessageSchema, y as agentRuntimeConfigPayloadSchema$1, yt as sessionEventSchema$1, z as getMeDocSchema } from "./dist-1XGLJMOq.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-LPcARA4K-Dbrptiyz.mjs";
7
7
  import { n as init_esm, r as trace, t as esm_exports } from "./esm-iadMkGbV.mjs";
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-BH4CmUL0-CybE3kuP.mjs";
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-RM_03B_l-DiEIa9xe.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-DZO4NX3P-BPxTeHf-.mjs";
10
10
  import { createRequire } from "node:module";
11
11
  import { ZodError, z } from "zod";
@@ -16,7 +16,7 @@ import { EventEmitter } from "node:events";
16
16
  import { closeSync, copyFileSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, watch, writeFileSync, writeSync } from "node:fs";
17
17
  import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
18
18
  import WebSocket from "ws";
19
- import { chmod, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
19
+ import { chmod, mkdir, readFile, readdir, realpath, rm, stat, writeFile } from "node:fs/promises";
20
20
  import { parse, stringify } from "yaml";
21
21
  import { query } from "@anthropic-ai/claude-agent-sdk";
22
22
  import { execFile, execFileSync, execSync, spawn, spawnSync } from "node:child_process";
@@ -659,6 +659,16 @@ const agentTypeSchema = z.enum([
659
659
  "autonomous_agent"
660
660
  ]);
661
661
  const agentVisibilitySchema = z.enum(["private", "organization"]);
662
+ const avatarColorTokenSchema = z.enum([
663
+ "hue-0",
664
+ "hue-1",
665
+ "hue-2",
666
+ "hue-3",
667
+ "hue-4",
668
+ "hue-5",
669
+ "hue-6",
670
+ "hue-7"
671
+ ]);
662
672
  const agentSourceSchema = z.enum(["admin-api", "portal"]);
663
673
  z.enum(["active", "suspended"]);
664
674
  /**
@@ -705,7 +715,8 @@ z.object({
705
715
  visibility: agentVisibilitySchema.optional(),
706
716
  metadata: z.record(z.string(), z.unknown()).optional(),
707
717
  managerId: z.string().nullable().optional(),
708
- clientId: z.string().min(1).max(100).nullable().optional()
718
+ clientId: z.string().min(1).max(100).nullable().optional(),
719
+ avatarColorToken: avatarColorTokenSchema.nullable().optional()
709
720
  });
710
721
  z.object({
711
722
  clientId: z.string().min(1).max(100),
@@ -727,6 +738,8 @@ z.object({
727
738
  managerId: z.string().nullable(),
728
739
  clientId: z.string().nullable(),
729
740
  runtimeProvider: runtimeProviderSchema,
741
+ avatarColorToken: z.string().nullable(),
742
+ avatarImageUrl: z.string().nullable(),
730
743
  presenceStatus: presenceStatusSchema.optional(),
731
744
  createdAt: z.string(),
732
745
  updatedAt: z.string()
@@ -788,6 +801,14 @@ const chatMetadataSchema = z.discriminatedUnion("source", [githubChatMetadataSch
788
801
  * sneak through `{ source: "github" }` without the required fields.
789
802
  */
790
803
  const optionalChatMetadataSchema = z.union([z.object({}).strict(), chatMetadataSchema]);
804
+ const chatSourceSchema = z.enum([
805
+ "manual",
806
+ "github_issue",
807
+ "github_pull_request",
808
+ "github_discussion",
809
+ "github_commit",
810
+ "feishu"
811
+ ]);
791
812
  const chatTypeSchema = z.enum(["direct", "group"]);
792
813
  const chatEngagementStatusSchema = z.enum([
793
814
  "active",
@@ -999,6 +1020,11 @@ const contextTreeSummarySchema = z.object({
999
1020
  removedCount: z.number().int().nonnegative(),
1000
1021
  changedNodeCount: z.number().int().nonnegative()
1001
1022
  });
1023
+ const contextTreeUsageSummarySchema = z.object({
1024
+ windowDays: z.number().int().positive(),
1025
+ agentCount: z.number().int().nonnegative(),
1026
+ usageCount: z.number().int().nonnegative()
1027
+ });
1002
1028
  z.object({
1003
1029
  repo: z.string().nullable(),
1004
1030
  branch: z.string().nullable(),
@@ -1007,6 +1033,7 @@ z.object({
1007
1033
  snapshotStatus: contextTreeSnapshotStatusSchema,
1008
1034
  contextStatus: contextTreeStatusSchema,
1009
1035
  summary: contextTreeSummarySchema,
1036
+ usage: contextTreeUsageSummarySchema,
1010
1037
  updates: z.array(contextTreeUpdateSchema),
1011
1038
  nodes: z.array(contextTreeNodeSchema),
1012
1039
  edges: z.array(contextTreeEdgeSchema),
@@ -1283,12 +1310,15 @@ z.object({
1283
1310
  cursor: z.string().optional(),
1284
1311
  limit: z.coerce.number().int().min(1).max(200).default(50),
1285
1312
  filter: meChatFilterSchema.default("all"),
1286
- engagement: chatEngagementViewSchema.default("active")
1313
+ engagement: chatEngagementViewSchema.default("active"),
1314
+ source: chatSourceSchema.optional()
1287
1315
  });
1288
1316
  const meChatParticipantSchema = z.object({
1289
1317
  agentId: z.string(),
1290
1318
  displayName: z.string(),
1291
- type: z.string()
1319
+ type: z.string(),
1320
+ avatarColorToken: z.string().nullable(),
1321
+ avatarImageUrl: z.string().nullable()
1292
1322
  });
1293
1323
  /**
1294
1324
  * Live activity hint surfaced in the conversation row's time slot. Derived
@@ -1348,6 +1378,47 @@ z.object({
1348
1378
  type: z.literal("chat:message"),
1349
1379
  chatId: z.string()
1350
1380
  });
1381
+ /**
1382
+ * Per-source aggregate for the conversation-list tag bar.
1383
+ *
1384
+ * - `chatCount` — number of chats the caller is in for this source. Used
1385
+ * to hide tags whose count is 0 ("don't render a PR tag if there are no
1386
+ * PRs").
1387
+ * - `unreadChatCount` — number of chats whose `unread_mention_count > 0`.
1388
+ * This is "chats with unread mentions", NOT "total mention count", so
1389
+ * the badge on each tag matches the semantics of the existing `unread`
1390
+ * filter pill (`totalUnread` in `pages/workspace/conversations`) — a
1391
+ * `2` on the PR tag means "2 PR chats have unread mentions", which is
1392
+ * what a user expects to click into.
1393
+ *
1394
+ * The map ALWAYS contains the `manual` key (the default tab is always
1395
+ * available, even at zero counts); other keys are present only when the
1396
+ * caller has at least one chat for that source.
1397
+ */
1398
+ const chatSourceCountSchema = z.object({
1399
+ chatCount: z.number().int().nonnegative(),
1400
+ unreadChatCount: z.number().int().nonnegative()
1401
+ });
1402
+ z.object({ engagement: chatEngagementViewSchema.default("active") });
1403
+ z.object({ counts: z.partialRecord(chatSourceSchema, chatSourceCountSchema) });
1404
+ const workspaceDocRefSchema = z.object({
1405
+ type: z.literal("workspace"),
1406
+ chatId: z.string().trim().min(1),
1407
+ agentId: z.string().trim().min(1),
1408
+ basePath: z.string().trim().optional(),
1409
+ path: z.string().trim().min(1)
1410
+ });
1411
+ const documentContextSchema = z.object({ basePath: z.string().trim().min(1) });
1412
+ z.object({
1413
+ agentId: z.string().trim().min(1),
1414
+ basePath: z.string().trim().optional(),
1415
+ path: z.string().trim().min(1)
1416
+ });
1417
+ z.object({
1418
+ ref: workspaceDocRefSchema,
1419
+ path: z.string(),
1420
+ content: z.string()
1421
+ });
1351
1422
  z.enum([
1352
1423
  "connect",
1353
1424
  "create_agent",
@@ -1399,7 +1470,8 @@ z.object({
1399
1470
  organizationId: z.string(),
1400
1471
  organizationName: z.string(),
1401
1472
  role: z.enum(["admin", "member"]),
1402
- agentId: z.string()
1473
+ agentId: z.string(),
1474
+ orgHasOtherMembers: z.boolean()
1403
1475
  });
1404
1476
  const memberRoleSchema = z.enum(["admin", "member"]);
1405
1477
  const memberSchema = z.object({
@@ -1774,7 +1846,8 @@ const sessionEventKind = z.enum([
1774
1846
  "error",
1775
1847
  "assistant_text",
1776
1848
  "thinking",
1777
- "turn_end"
1849
+ "turn_end",
1850
+ "context_tree_usage"
1778
1851
  ]);
1779
1852
  const toolCallEventPayload = z.object({
1780
1853
  toolUseId: z.string(),
@@ -1816,6 +1889,10 @@ const thinkingEventPayload = z.object({});
1816
1889
  * completed turns to show only the final result message.
1817
1890
  */
1818
1891
  const turnEndEventPayload = z.object({ status: z.enum(["success", "error"]) });
1892
+ const contextTreeUsageEventPayload = z.object({
1893
+ purpose: z.literal("design_decision"),
1894
+ treeRepoUrl: z.string().nullable()
1895
+ });
1819
1896
  const sessionEventSchema = z.discriminatedUnion("kind", [
1820
1897
  z.object({
1821
1898
  kind: z.literal("tool_call"),
@@ -1836,6 +1913,10 @@ const sessionEventSchema = z.discriminatedUnion("kind", [
1836
1913
  z.object({
1837
1914
  kind: z.literal("turn_end"),
1838
1915
  payload: turnEndEventPayload
1916
+ }),
1917
+ z.object({
1918
+ kind: z.literal("context_tree_usage"),
1919
+ payload: contextTreeUsageEventPayload
1839
1920
  })
1840
1921
  ]);
1841
1922
  z.object({
@@ -1849,7 +1930,8 @@ z.object({
1849
1930
  errorEventPayload,
1850
1931
  assistantTextEventPayload,
1851
1932
  thinkingEventPayload,
1852
- turnEndEventPayload
1933
+ turnEndEventPayload,
1934
+ contextTreeUsageEventPayload
1853
1935
  ]),
1854
1936
  createdAt: z.string()
1855
1937
  });
@@ -2018,6 +2100,7 @@ defineConfig({
2018
2100
  host: field(z.string().default("127.0.0.1"), { env: "FIRST_TREE_HUB_HOST" }),
2019
2101
  publicUrl: field(z.string().optional(), { env: "FIRST_TREE_HUB_PUBLIC_URL" })
2020
2102
  },
2103
+ workspace: { root: field(z.string().default(join(DEFAULT_DATA_DIR, "workspaces")), { env: "FIRST_TREE_HUB_WORKSPACES_ROOT" }) },
2021
2104
  secrets: {
2022
2105
  jwtSecret: field(z.string(), {
2023
2106
  env: "FIRST_TREE_HUB_JWT_SECRET",
@@ -4691,6 +4774,7 @@ const createClaudeCodeHandler = (config) => {
4691
4774
  /** Worktrees materialised for this session — each entry removed on shutdown. */
4692
4775
  const ownedWorktrees = [];
4693
4776
  async function toSDKUserMessage(message, sessionCtx, sessionId) {
4777
+ emitContextTreeUsage(sessionCtx);
4694
4778
  if (message.format === "file") {
4695
4779
  const senderLabel = message.senderId ? await sessionCtx.resolveSenderLabel(message.senderId) : "";
4696
4780
  const prefix = senderLabel ? `[From: ${senderLabel}]\n\n` : "";
@@ -4753,6 +4837,16 @@ const createClaudeCodeHandler = (config) => {
4753
4837
  session_id: sessionId
4754
4838
  };
4755
4839
  }
4840
+ function emitContextTreeUsage(sessionCtx) {
4841
+ if (!contextTreePath) return;
4842
+ sessionCtx.emitEvent({
4843
+ kind: "context_tree_usage",
4844
+ payload: {
4845
+ purpose: "design_decision",
4846
+ treeRepoUrl: contextTreeRepoUrl
4847
+ }
4848
+ });
4849
+ }
4756
4850
  /**
4757
4851
  * Build env for the child Claude Code process.
4758
4852
  *
@@ -5356,6 +5450,16 @@ const createCodexHandler = (config) => {
5356
5450
  function toCodexInput(message, sessionCtx) {
5357
5451
  return sessionCtx.formatInboundContent(message).then((text) => text);
5358
5452
  }
5453
+ function emitContextTreeUsage(sessionCtx) {
5454
+ if (!contextTreePath) return;
5455
+ sessionCtx.emitEvent({
5456
+ kind: "context_tree_usage",
5457
+ payload: {
5458
+ purpose: "design_decision",
5459
+ treeRepoUrl: contextTreeRepoUrl
5460
+ }
5461
+ });
5462
+ }
5359
5463
  async function prepareGitWorktrees(payload, workspaceCwd, sessionCtx) {
5360
5464
  if (!gitMirrorManager) return;
5361
5465
  const branchAgentKey = agentName ?? sessionCtx.agent.agentId;
@@ -5484,6 +5588,7 @@ const createCodexHandler = (config) => {
5484
5588
  if (!activeThread) return;
5485
5589
  const abort = new AbortController();
5486
5590
  currentAbort = abort;
5591
+ emitContextTreeUsage(sessionCtx);
5487
5592
  sessionCtx.setRuntimeState("working");
5488
5593
  const assistantTexts = [];
5489
5594
  let turnFailed = false;
@@ -5968,13 +6073,17 @@ var Deduplicator = class {
5968
6073
  };
5969
6074
  function createResultSink(deps) {
5970
6075
  async function buildMetadata(trigger) {
5971
- if (!trigger || trigger.senderId === deps.agent.agentId) return void 0;
5972
- const participants = await deps.participants.get();
5973
- if (participants.length <= 2) {
5974
- const peer = participants.find((p) => p.agentId === trigger.senderId);
5975
- if (peer && peer.mode !== "mention_only") return void 0;
5976
- }
5977
- return { mentions: [trigger.senderId] };
6076
+ const metadata = {};
6077
+ const documentBasePath = await deps.getDocumentBasePath?.();
6078
+ if (documentBasePath) metadata.documentContext = documentContextSchema.parse({ basePath: documentBasePath });
6079
+ if (trigger && trigger.senderId !== deps.agent.agentId) {
6080
+ const participants = await deps.participants.get();
6081
+ if (participants.length <= 2) {
6082
+ const peer = participants.find((p) => p.agentId === trigger.senderId);
6083
+ if (!peer || peer.mode === "mention_only") metadata.mentions = [trigger.senderId];
6084
+ } else metadata.mentions = [trigger.senderId];
6085
+ }
6086
+ return Object.keys(metadata).length > 0 ? metadata : void 0;
5978
6087
  }
5979
6088
  return async function forwardResult(text) {
5980
6089
  if (text.trim().length === 0) {
@@ -6065,6 +6174,16 @@ var SessionRegistry = class {
6065
6174
  }
6066
6175
  }
6067
6176
  };
6177
+ function documentBasePathFromRuntimeConfig(payload) {
6178
+ if (payload.gitRepos.length !== 1) return null;
6179
+ const repo = payload.gitRepos[0];
6180
+ if (!repo) return null;
6181
+ const localPath = repoLocalPath(repo).trim();
6182
+ return localPath.length > 0 ? localPath : null;
6183
+ }
6184
+ function repoLocalPath(repo) {
6185
+ return repo.localPath ?? deriveRepoLocalPath(repo.url);
6186
+ }
6068
6187
  /**
6069
6188
  * Manages per-chat session entries with session-oriented handler lifecycle.
6070
6189
  *
@@ -6554,7 +6673,8 @@ var SessionManager = class {
6554
6673
  this.currentTrigger.delete(chatId);
6555
6674
  },
6556
6675
  log,
6557
- participants
6676
+ participants,
6677
+ getDocumentBasePath: () => this.resolveDocumentBasePath(log)
6558
6678
  });
6559
6679
  const envCtx = {
6560
6680
  sdk: this.config.sdk,
@@ -6582,6 +6702,16 @@ var SessionManager = class {
6582
6702
  resolveSenderLabel: async (senderId) => resolveSenderLabel(senderId, await participants.get())
6583
6703
  };
6584
6704
  }
6705
+ async resolveDocumentBasePath(log) {
6706
+ if (!this.config.agentConfigCache) return null;
6707
+ try {
6708
+ const { payload } = await this.config.agentConfigCache.refreshIfNewer(this.config.agentIdentity.agentId, 0);
6709
+ return documentBasePathFromRuntimeConfig(payload);
6710
+ } catch (err) {
6711
+ log(`document preview base path unavailable: ${err instanceof Error ? err.message : String(err)}`);
6712
+ return null;
6713
+ }
6714
+ }
6585
6715
  /** Update per-session runtime state and recompute aggregate. Only active sessions may update. */
6586
6716
  setSessionRuntimeState(chatId, state) {
6587
6717
  const session = this.sessions.get(chatId);
@@ -9553,7 +9683,7 @@ async function onboardCreate(args) {
9553
9683
  }
9554
9684
  const runtimeAgent = args.type === "human" ? args.assistant : args.id;
9555
9685
  if (args.feishuBotAppId && args.feishuBotAppSecret) {
9556
- const { bindFeishuBot } = await import("./feishu-D_vnqC6a.mjs").then((n) => n.r);
9686
+ const { bindFeishuBot } = await import("./feishu-BGx71p5s.mjs").then((n) => n.r);
9557
9687
  const targetAgentUuid = args.type === "human" ? assistantUuid : primary.uuid;
9558
9688
  if (!targetAgentUuid) print.line(`Warning: Cannot bind Feishu bot — no runtime agent available for "${args.id}".\n`);
9559
9689
  else {
@@ -10766,7 +10896,7 @@ function createFeedbackHandler(config) {
10766
10896
  return { handle };
10767
10897
  }
10768
10898
  //#endregion
10769
- //#region ../server/dist/app-DQVUb4ZY.mjs
10899
+ //#region ../server/dist/app-l2iy80P2.mjs
10770
10900
  var import_fastify_opentelemetry = /* @__PURE__ */ __toESM(require_fastify_opentelemetry(), 1);
10771
10901
  init_esm();
10772
10902
  var __defProp = Object.defineProperty;
@@ -11376,6 +11506,21 @@ async function ensureDefaultOrganization(db) {
11376
11506
  */
11377
11507
  const RESERVED_AGENT_NAME_PREFIX = "__";
11378
11508
  /**
11509
+ * Derive the relative URL clients should use to fetch a manager-uploaded
11510
+ * avatar image. Returns `null` when no image is set. Embeds the upload
11511
+ * timestamp as `?v=<epoch>` so a fresh upload busts any browser cache
11512
+ * that may have memoised the previous version.
11513
+ *
11514
+ * Auth: the image route is intentionally public read — the URL leaks no
11515
+ * more than the agent's UUID, which is already required to address it.
11516
+ * Keeping it unauthenticated lets `<img src>` render without bespoke
11517
+ * fetch-and-blob plumbing.
11518
+ */
11519
+ function agentAvatarImageUrl(uuid, updatedAt) {
11520
+ if (!updatedAt) return null;
11521
+ return `/api/v1/agents/${uuid}/avatar?v=${updatedAt.getTime()}`;
11522
+ }
11523
+ /**
11379
11524
  * True iff `clients.metadata.capabilities` is a non-empty object — i.e. the
11380
11525
  * client has reported at least one runtime probe result. Used to distinguish
11381
11526
  * "we don't know what's installed yet" (empty / never reported) from
@@ -11634,6 +11779,8 @@ async function listAgentsForAdmin(db, scope, limit, cursor) {
11634
11779
  managerId: agents.managerId,
11635
11780
  clientId: agents.clientId,
11636
11781
  runtimeProvider: agents.runtimeProvider,
11782
+ avatarColorToken: agents.avatarColorToken,
11783
+ avatarImageUpdatedAt: agents.avatarImageUpdatedAt,
11637
11784
  createdAt: agents.createdAt,
11638
11785
  updatedAt: agents.updatedAt,
11639
11786
  presenceStatus: agentPresence.status,
@@ -11672,6 +11819,8 @@ async function listAgentsForMember(db, scope, limit, cursor, type) {
11672
11819
  managerId: agents.managerId,
11673
11820
  clientId: agents.clientId,
11674
11821
  runtimeProvider: agents.runtimeProvider,
11822
+ avatarColorToken: agents.avatarColorToken,
11823
+ avatarImageUpdatedAt: agents.avatarImageUpdatedAt,
11675
11824
  createdAt: agents.createdAt,
11676
11825
  updatedAt: agents.updatedAt,
11677
11826
  presenceStatus: agentPresence.status,
@@ -11708,6 +11857,7 @@ async function updateAgent(db, uuid, data) {
11708
11857
  }
11709
11858
  if (data.visibility !== void 0) updates.visibility = data.visibility;
11710
11859
  if (data.metadata !== void 0) updates.metadata = data.metadata;
11860
+ if (data.avatarColorToken !== void 0) updates.avatarColorToken = data.avatarColorToken;
11711
11861
  if (data.managerId !== void 0) {
11712
11862
  if (data.managerId === null) throw new BadRequestError("managerId cannot be cleared — every agent must have a manager");
11713
11863
  const [manager] = await db.select({
@@ -11814,6 +11964,63 @@ async function deleteAgent(db, uuid) {
11814
11964
  if (!agent) throw new Error("Unexpected: UPDATE RETURNING produced no row");
11815
11965
  return agent;
11816
11966
  }
11967
+ /**
11968
+ * Supported avatar-image MIME types. The web client always uploads WEBP after
11969
+ * its own resize step; we accept PNG/JPEG too so a caller using the raw HTTP
11970
+ * API (curl, scripts) doesn't have to re-encode. Anything else is rejected at
11971
+ * the boundary — we never store an unknown content type.
11972
+ */
11973
+ const SUPPORTED_AVATAR_IMAGE_MIMES = [
11974
+ "image/webp",
11975
+ "image/png",
11976
+ "image/jpeg"
11977
+ ];
11978
+ /** Hard server-side ceiling for the stored bytea blob. Client pre-resizes to ~50KB. */
11979
+ const MAX_AVATAR_IMAGE_BYTES = 512 * 1024;
11980
+ function isSupportedAvatarMime(mime) {
11981
+ return SUPPORTED_AVATAR_IMAGE_MIMES.find((m) => m === mime) !== void 0;
11982
+ }
11983
+ /**
11984
+ * Fetch the avatar image blob for an agent. Returns `null` when no image
11985
+ * is set (the column is NULL). The data + mime pair is always coherent
11986
+ * (set/cleared together by the service writes below).
11987
+ */
11988
+ async function getAgentAvatarImage(db, uuid) {
11989
+ const [row] = await db.select({
11990
+ data: agents.avatarImageData,
11991
+ mime: agents.avatarImageMime,
11992
+ updatedAt: agents.avatarImageUpdatedAt
11993
+ }).from(agents).where(and(eq(agents.uuid, uuid), ne(agents.status, AGENT_STATUSES.DELETED))).limit(1);
11994
+ if (!row || !row.data || !row.mime || !row.updatedAt) return null;
11995
+ return {
11996
+ data: row.data,
11997
+ mime: row.mime,
11998
+ updatedAt: row.updatedAt
11999
+ };
12000
+ }
12001
+ /** Replace (or set) an agent's avatar image. Validates mime + size. */
12002
+ async function setAgentAvatarImage(db, uuid, data, mime) {
12003
+ if (!isSupportedAvatarMime(mime)) throw new BadRequestError(`Unsupported avatar image type "${mime}". Use PNG, JPEG, or WEBP.`);
12004
+ if (data.length === 0) throw new BadRequestError("Avatar image payload is empty.");
12005
+ if (data.length > 524288) throw new BadRequestError(`Avatar image is too large (${data.length} bytes; max ${MAX_AVATAR_IMAGE_BYTES}).`);
12006
+ const now = /* @__PURE__ */ new Date();
12007
+ if ((await db.update(agents).set({
12008
+ avatarImageData: data,
12009
+ avatarImageMime: mime,
12010
+ avatarImageUpdatedAt: now,
12011
+ updatedAt: now
12012
+ }).where(and(eq(agents.uuid, uuid), ne(agents.status, AGENT_STATUSES.DELETED))).returning({ uuid: agents.uuid })).length === 0) throw new NotFoundError(`Agent "${uuid}" not found`);
12013
+ return now;
12014
+ }
12015
+ /** Clear an agent's avatar image (falls back to color + initial). */
12016
+ async function clearAgentAvatarImage(db, uuid) {
12017
+ if ((await db.update(agents).set({
12018
+ avatarImageData: null,
12019
+ avatarImageMime: null,
12020
+ avatarImageUpdatedAt: null,
12021
+ updatedAt: /* @__PURE__ */ new Date()
12022
+ }).where(and(eq(agents.uuid, uuid), ne(agents.status, AGENT_STATUSES.DELETED))).returning({ uuid: agents.uuid })).length === 0) throw new NotFoundError(`Agent "${uuid}" not found`);
12023
+ }
11817
12024
  const log$5 = createLogger$1("AgentFeishuBot");
11818
12025
  async function agentFeishuBotRoutes(app) {
11819
12026
  /**
@@ -13122,6 +13329,18 @@ async function listEvents(db, agentId, chatId, options) {
13122
13329
  async function clearEvents(db, agentId, chatId) {
13123
13330
  await db.delete(sessionEvents).where(and(eq(sessionEvents.agentId, agentId), eq(sessionEvents.chatId, chatId)));
13124
13331
  }
13332
+ async function summarizeContextTreeUsage(db, organizationId, windowDays) {
13333
+ const since = /* @__PURE__ */ new Date(Date.now() - windowDays * 24 * 60 * 60 * 1e3);
13334
+ const [row] = await db.select({
13335
+ agentCount: sql`count(distinct ${sessionEvents.agentId})::int`,
13336
+ usageCount: sql`count(*)::int`
13337
+ }).from(sessionEvents).innerJoin(agents, eq(agents.uuid, sessionEvents.agentId)).where(and(eq(agents.organizationId, organizationId), eq(sessionEvents.kind, "context_tree_usage"), gte(sessionEvents.createdAt, since)));
13338
+ return {
13339
+ windowDays,
13340
+ agentCount: row?.agentCount ?? 0,
13341
+ usageCount: row?.usageCount ?? 0
13342
+ };
13343
+ }
13125
13344
  /**
13126
13345
  * Default per-agent in-flight cap when `server.inbox.maxInFlightPerAgent` is
13127
13346
  * unset. Mirrors the schema default so a hub running without an explicit
@@ -13772,6 +13991,22 @@ async function agentActivityRoutes(app) {
13772
13991
  });
13773
13992
  }
13774
13993
  /**
13994
+ * Project a DB agent row into its wire shape. Strips the inline image
13995
+ * `avatarImageData` (large bytea, only meant for the image-serve route)
13996
+ * and synthesises the public `avatarImageUrl` from the upload timestamp.
13997
+ * `createdAt`/`updatedAt` are coerced to ISO strings so the response is
13998
+ * pure JSON.
13999
+ */
14000
+ function serializeAgent(agent) {
14001
+ const { avatarImageData: _data, avatarImageMime: _mime, avatarImageUpdatedAt, createdAt, updatedAt, ...rest } = agent;
14002
+ return {
14003
+ ...rest,
14004
+ createdAt: createdAt.toISOString(),
14005
+ updatedAt: updatedAt.toISOString(),
14006
+ avatarImageUrl: agentAvatarImageUrl(agent.uuid, avatarImageUpdatedAt ?? null)
14007
+ };
14008
+ }
14009
+ /**
13775
14010
  * Class C — resource-scoped per-agent routes. Mounted at
13776
14011
  * `/api/v1/agents/:uuid/...`. The agent's UUID locates its org
13777
14012
  * intrinsically; `requireAgentAccess` resolves the caller's membership in
@@ -13800,11 +14035,7 @@ async function agentRoutes(app) {
13800
14035
  }
13801
14036
  app.get("/:uuid", async (request) => {
13802
14037
  const { agent } = await requireAgentAccess(request, app.db, "visible");
13803
- return {
13804
- ...agent,
13805
- createdAt: agent.createdAt.toISOString(),
13806
- updatedAt: agent.updatedAt.toISOString()
13807
- };
14038
+ return serializeAgent(agent);
13808
14039
  });
13809
14040
  app.patch("/:uuid", { config: { otelRecordBody: true } }, async (request) => {
13810
14041
  const { scope } = await requireAgentAccess(request, app.db, "manage");
@@ -13813,22 +14044,14 @@ async function agentRoutes(app) {
13813
14044
  const before = body.clientId !== void 0 ? await getAgent(app.db, request.params.uuid) : null;
13814
14045
  const agent = await updateAgent(app.db, request.params.uuid, body);
13815
14046
  if (before && before.clientId === null && agent.clientId !== null) notifyClientAgentPinned(agent);
13816
- return {
13817
- ...agent,
13818
- createdAt: agent.createdAt.toISOString(),
13819
- updatedAt: agent.updatedAt.toISOString()
13820
- };
14047
+ return serializeAgent(agent);
13821
14048
  });
13822
14049
  app.patch("/:uuid/rebind", { config: { otelRecordBody: true } }, async (request) => {
13823
14050
  await requireAgentAccess(request, app.db, "manage");
13824
14051
  const body = rebindAgentSchema.parse(request.body);
13825
14052
  const agent = await rebindAgent(app.db, request.params.uuid, body);
13826
14053
  notifyClientAgentPinned(agent);
13827
- return {
13828
- ...agent,
13829
- createdAt: agent.createdAt.toISOString(),
13830
- updatedAt: agent.updatedAt.toISOString()
13831
- };
14054
+ return serializeAgent(agent);
13832
14055
  });
13833
14056
  app.post("/:uuid/disconnect", async (request, reply) => {
13834
14057
  await requireAgentAccess(request, app.db, "manage");
@@ -13838,27 +14061,35 @@ async function agentRoutes(app) {
13838
14061
  });
13839
14062
  app.post("/:uuid/suspend", async (request) => {
13840
14063
  await requireAgentAccess(request, app.db, "manage");
13841
- const agent = await suspendAgent(app.db, request.params.uuid);
13842
- return {
13843
- ...agent,
13844
- createdAt: agent.createdAt.toISOString(),
13845
- updatedAt: agent.updatedAt.toISOString()
13846
- };
14064
+ return serializeAgent(await suspendAgent(app.db, request.params.uuid));
13847
14065
  });
13848
14066
  app.post("/:uuid/reactivate", async (request) => {
13849
14067
  await requireAgentAccess(request, app.db, "manage");
13850
- const agent = await reactivateAgent(app.db, request.params.uuid);
13851
- return {
13852
- ...agent,
13853
- createdAt: agent.createdAt.toISOString(),
13854
- updatedAt: agent.updatedAt.toISOString()
13855
- };
14068
+ return serializeAgent(await reactivateAgent(app.db, request.params.uuid));
13856
14069
  });
13857
14070
  app.delete("/:uuid", async (request, reply) => {
13858
14071
  await requireAgentAccess(request, app.db, "manage");
13859
14072
  await deleteAgent(app.db, request.params.uuid);
13860
14073
  return reply.status(204).send();
13861
14074
  });
14075
+ app.addContentTypeParser(/^image\//, { parseAs: "buffer" }, (_req, body, done) => {
14076
+ done(null, body);
14077
+ });
14078
+ app.put("/:uuid/avatar", { bodyLimit: MAX_AVATAR_IMAGE_BYTES + 1024 }, async (request, reply) => {
14079
+ await requireAgentAccess(request, app.db, "manage");
14080
+ const contentType = request.headers["content-type"];
14081
+ if (typeof contentType !== "string" || !contentType.startsWith("image/")) throw new BadRequestError(`Avatar upload requires an image/* Content-Type. Supported: ${SUPPORTED_AVATAR_IMAGE_MIMES.join(", ")}.`);
14082
+ const mime = contentType.split(";")[0]?.trim() ?? "";
14083
+ const body = request.body;
14084
+ if (!Buffer.isBuffer(body)) throw new BadRequestError("Avatar upload body must be raw image bytes.");
14085
+ const updatedAt = await setAgentAvatarImage(app.db, request.params.uuid, body, mime);
14086
+ return reply.status(200).send({ avatarImageUrl: agentAvatarImageUrl(request.params.uuid, updatedAt) });
14087
+ });
14088
+ app.delete("/:uuid/avatar", async (request, reply) => {
14089
+ await requireAgentAccess(request, app.db, "manage");
14090
+ await clearAgentAvatarImage(app.db, request.params.uuid);
14091
+ return reply.status(204).send();
14092
+ });
13862
14093
  app.post("/:uuid/test", async (request, reply) => {
13863
14094
  const { uuid } = request.params;
13864
14095
  const { agent: targetAgent } = await requireAgentAccess(request, app.db, "manage");
@@ -13967,6 +14198,23 @@ async function agentRoutes(app) {
13967
14198
  });
13968
14199
  }
13969
14200
  /**
14201
+ * Public read-only route for agent avatar images. Mounted outside the
14202
+ * member-JWT auth scope so `<img src>` works without bespoke fetch-and-blob
14203
+ * plumbing. Reading an avatar leaks no more than the agent's UUID — which
14204
+ * is already required to address the route — and the UUID itself is only
14205
+ * exposed through authenticated agent-list calls.
14206
+ */
14207
+ async function publicAgentAvatarRoutes(app) {
14208
+ app.get("/:uuid/avatar", async (request, reply) => {
14209
+ const image = await getAgentAvatarImage(app.db, request.params.uuid);
14210
+ if (!image) return reply.status(404).send({ error: "Avatar not set" });
14211
+ reply.header("Content-Type", image.mime);
14212
+ reply.header("Cache-Control", "public, max-age=2592000, immutable");
14213
+ reply.header("ETag", `"${image.updatedAt.getTime()}"`);
14214
+ return reply.send(image.data);
14215
+ });
14216
+ }
14217
+ /**
13970
14218
  * Class C — `/api/v1/agents/:uuid/config`. Runtime config (system prompt,
13971
14219
  * tools, env) is behavior-sensitive — gated on `manage`, not `visible`.
13972
14220
  */
@@ -15012,6 +15260,22 @@ async function listActiveMemberships(db, userId) {
15012
15260
  }).from(members).innerJoin(organizations, eq(members.organizationId, organizations.id)).where(and(eq(members.userId, userId), eq(members.status, "active"))).orderBy(desc(members.createdAt));
15013
15261
  }
15014
15262
  /**
15263
+ * Count ACTIVE members per org, restricted to the given org IDs. Returns a
15264
+ * Map keyed by `organizationId`; orgs absent from the result simply have
15265
+ * zero active members (shouldn't happen in practice — the caller always
15266
+ * passes orgs the user is a member of — but the Map shape lets callers do
15267
+ * `counts.get(orgId) ?? 0` defensively). Used by `/me` to surface
15268
+ * `orgHasOtherMembers` per membership without N+1 queries.
15269
+ */
15270
+ async function countActiveMembersByOrgs(db, organizationIds) {
15271
+ if (organizationIds.length === 0) return /* @__PURE__ */ new Map();
15272
+ const rows = await db.select({
15273
+ organizationId: members.organizationId,
15274
+ count: sql`count(*)::int`
15275
+ }).from(members).where(and(inArray(members.organizationId, organizationIds), eq(members.status, "active"))).groupBy(members.organizationId);
15276
+ return new Map(rows.map((r) => [r.organizationId, r.count]));
15277
+ }
15278
+ /**
15015
15279
  * Pick the most recently joined active membership — used after OAuth login
15016
15280
  * when the user already has at least one team but no `next` was specified.
15017
15281
  */
@@ -15803,16 +16067,16 @@ function decodeCursor(cursor) {
15803
16067
  return null;
15804
16068
  }
15805
16069
  }
15806
- const { ACTIVE, ARCHIVED, DELETED } = CHAT_ENGAGEMENT_STATUSES;
16070
+ const { ACTIVE: ACTIVE$1, ARCHIVED: ARCHIVED$1, DELETED } = CHAT_ENGAGEMENT_STATUSES;
15807
16071
  /**
15808
16072
  * SQL predicate for each engagement view tab. `deleted` is never a valid view
15809
16073
  * value — deleted rows are reachable only through `GET /chats/:chatId` + the
15810
16074
  * Restore banner on the chat detail page.
15811
16075
  */
15812
16076
  const ENGAGEMENT_VIEW_PREDICATE = {
15813
- active: sql`COALESCE(cus.engagement_status, ${ACTIVE}) = ${ACTIVE}`,
15814
- archived: sql`COALESCE(cus.engagement_status, ${ACTIVE}) = ${ARCHIVED}`,
15815
- all: sql`COALESCE(cus.engagement_status, ${ACTIVE}) IN (${ACTIVE}, ${ARCHIVED})`
16077
+ active: sql`COALESCE(cus.engagement_status, ${ACTIVE$1}) = ${ACTIVE$1}`,
16078
+ archived: sql`COALESCE(cus.engagement_status, ${ACTIVE$1}) = ${ARCHIVED$1}`,
16079
+ all: sql`COALESCE(cus.engagement_status, ${ACTIVE$1}) IN (${ACTIVE$1}, ${ARCHIVED$1})`
15816
16080
  };
15817
16081
  /**
15818
16082
  * Write the caller's engagement state for this chat. UPSERT into
@@ -15840,7 +16104,26 @@ async function setChatEngagement(db, chatId, agentId, status) {
15840
16104
  */
15841
16105
  async function getCallerEngagement(db, chatId, agentId) {
15842
16106
  const [row] = await db.select({ engagementStatus: chatUserState.engagementStatus }).from(chatUserState).where(and(eq(chatUserState.chatId, chatId), eq(chatUserState.agentId, agentId))).limit(1);
15843
- return row?.engagementStatus ?? ACTIVE;
16107
+ return row?.engagementStatus ?? ACTIVE$1;
16108
+ }
16109
+ const KNOWN_NON_MANUAL_PREDICATE = sql`(
16110
+ (c.metadata->>'source' = 'github' AND c.metadata->>'entityType' IN (${sql.join(GITHUB_ENTITY_TYPES.map((t) => sql`${t}`), sql.raw(", "))}))
16111
+ OR c.metadata->>'source' = 'feishu'
16112
+ )`;
16113
+ const chatSourceSqlExpression = sql`CASE
16114
+ ${sql.join(GITHUB_ENTITY_TYPES.map((t) => sql`WHEN c.metadata->>'source' = 'github' AND c.metadata->>'entityType' = ${t} THEN ${`github_${t}`}`), sql.raw("\n "))}
16115
+ WHEN c.metadata->>'source' = 'feishu' THEN 'feishu'
16116
+ ELSE 'manual'
16117
+ END`;
16118
+ function sourceFilterSql(source) {
16119
+ switch (source) {
16120
+ case "manual": return sql`(${KNOWN_NON_MANUAL_PREDICATE}) IS NOT TRUE`;
16121
+ case "github_issue": return sql`(c.metadata->>'source' = 'github' AND c.metadata->>'entityType' = 'issue')`;
16122
+ case "github_pull_request": return sql`(c.metadata->>'source' = 'github' AND c.metadata->>'entityType' = 'pull_request')`;
16123
+ case "github_discussion": return sql`(c.metadata->>'source' = 'github' AND c.metadata->>'entityType' = 'discussion')`;
16124
+ case "github_commit": return sql`(c.metadata->>'source' = 'github' AND c.metadata->>'entityType' = 'commit')`;
16125
+ case "feishu": return sql`(c.metadata->>'source' = 'feishu')`;
16126
+ }
15844
16127
  }
15845
16128
  /**
15846
16129
  * GET /me/chats — cursor-paginated conversation list.
@@ -15866,6 +16149,7 @@ async function listMeChats(db, humanAgentId, organizationId, query) {
15866
16149
  const filterUnreadOnly = query.filter === "unread";
15867
16150
  const filterWatchingOnly = query.filter === "watching";
15868
16151
  const engagementPredicate = ENGAGEMENT_VIEW_PREDICATE[query.engagement];
16152
+ const sourcePredicate = query.source ? sourceFilterSql(query.source) : sql`TRUE`;
15869
16153
  const cursorTsIso = cursor?.lastMessageAt ? cursor.lastMessageAt.toISOString() : null;
15870
16154
  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
15871
16155
  OR c.last_message_at < ${cursorTsIso}::timestamptz
@@ -15882,7 +16166,7 @@ async function listMeChats(db, humanAgentId, organizationId, query) {
15882
16166
  WHERE chat_id = c.id AND access_mode = 'speaker') AS participant_count,
15883
16167
  cm.access_mode AS access_mode,
15884
16168
  COALESCE(cus.unread_mention_count, 0) AS unread_mention_count,
15885
- COALESCE(cus.engagement_status, ${ACTIVE}) AS engagement_status
16169
+ COALESCE(cus.engagement_status, ${ACTIVE$1}) AS engagement_status
15886
16170
  FROM chats c
15887
16171
  JOIN chat_membership cm
15888
16172
  ON cm.chat_id = c.id AND cm.agent_id = ${humanAgentId}
@@ -15898,6 +16182,7 @@ async function listMeChats(db, humanAgentId, organizationId, query) {
15898
16182
  AND (${!filterUnreadOnly}::bool OR COALESCE(cus.unread_mention_count, 0) > 0)
15899
16183
  AND (${!filterWatchingOnly}::bool OR cm.access_mode = 'watcher')
15900
16184
  AND ${engagementPredicate}
16185
+ AND ${sourcePredicate}
15901
16186
  AND ${cursorPredicate}
15902
16187
  ORDER BY c.last_message_at DESC NULLS LAST, c.id DESC
15903
16188
  LIMIT ${limit + 1}
@@ -15920,6 +16205,8 @@ async function listMeChats(db, humanAgentId, organizationId, query) {
15920
16205
  agentId: chatMembership.agentId,
15921
16206
  displayName: agents.displayName,
15922
16207
  type: agents.type,
16208
+ avatarColorToken: agents.avatarColorToken,
16209
+ avatarImageUpdatedAt: agents.avatarImageUpdatedAt,
15923
16210
  sessionState: agentChatSessions.state
15924
16211
  }).from(chatMembership).innerJoin(agents, eq(chatMembership.agentId, agents.uuid)).leftJoin(agentChatSessions, and(eq(agentChatSessions.agentId, chatMembership.agentId), eq(agentChatSessions.chatId, chatMembership.chatId))).where(and(inArray(chatMembership.chatId, chatIds), eq(chatMembership.accessMode, "speaker")));
15925
16212
  const participantsByChat = /* @__PURE__ */ new Map();
@@ -15929,7 +16216,9 @@ async function listMeChats(db, humanAgentId, organizationId, query) {
15929
16216
  list.push({
15930
16217
  agentId: p.agentId,
15931
16218
  displayName: p.displayName,
15932
- type: p.type
16219
+ type: p.type,
16220
+ avatarColorToken: p.avatarColorToken,
16221
+ avatarImageUrl: agentAvatarImageUrl(p.agentId, p.avatarImageUpdatedAt)
15933
16222
  });
15934
16223
  participantsByChat.set(p.chatId, list);
15935
16224
  if (p.sessionState === "active") {
@@ -16177,6 +16466,66 @@ async function leaveMeChat(db, chatId, humanAgentId) {
16177
16466
  return result;
16178
16467
  }
16179
16468
  /**
16469
+ * Used by future bell-badge / list-pill counts. The partial index
16470
+ * `idx_user_state_unread WHERE unread_mention_count > 0` bounds the
16471
+ * driving scan; we then join `chat_membership` + `chats` so the badge
16472
+ * stays consistent with `listMeChats`.
16473
+ *
16474
+ * Why the joins (not just a single-table count): per §11.4 a user's
16475
+ * `chat_user_state` row is **preserved on detach** so read state
16476
+ * survives a leave/rejoin cycle. Without the membership join, any
16477
+ * preserved row with `unread_mention_count > 0` would keep
16478
+ * contributing to the badge even though the chat no longer appears in
16479
+ * the list. The `chats` join applies the same org-scoping +
16480
+ * `parent_chat_id IS NULL` filter as `listMeChats` so the two counts
16481
+ * cannot drift in the cross-org pollution or nested-chat cases either.
16482
+ *
16483
+ * Engagement parity: deleted chats are excluded from `listMeChats`
16484
+ * (any `engagement` view), so the badge must exclude them too — otherwise
16485
+ * the user sees an unread red dot for a chat they've removed from view.
16486
+ */
16487
+ /**
16488
+ * Per-source aggregate for the conversation-list tag bar.
16489
+ *
16490
+ * Returns one row per source the caller has at least one chat for, plus an
16491
+ * always-present `manual` entry (zero counts when there are no manual chats —
16492
+ * the workspace UI uses `manual` as its default tab and must render it even
16493
+ * when empty).
16494
+ *
16495
+ * Filtering matches `listMeChats` for the corresponding tab so the badges
16496
+ * cannot drift from the list: same membership join, same `parent_chat_id IS
16497
+ * NULL` and `organization_id` scopes, same engagement view, same
16498
+ * `chat_user_state.unread_mention_count` source.
16499
+ */
16500
+ async function listMeChatSourceCounts(db, humanAgentId, organizationId, query) {
16501
+ const engagementPredicate = ENGAGEMENT_VIEW_PREDICATE[query.engagement];
16502
+ const rows = await db.execute(sql`
16503
+ SELECT
16504
+ ${chatSourceSqlExpression} AS source,
16505
+ count(*)::int AS chat_count,
16506
+ count(*) FILTER (WHERE COALESCE(cus.unread_mention_count, 0) > 0)::int AS unread_chat_count
16507
+ FROM chats c
16508
+ JOIN chat_membership cm
16509
+ ON cm.chat_id = c.id AND cm.agent_id = ${humanAgentId}
16510
+ LEFT JOIN chat_user_state cus
16511
+ ON cus.chat_id = c.id AND cus.agent_id = ${humanAgentId}
16512
+ WHERE c.parent_chat_id IS NULL
16513
+ AND c.organization_id = ${organizationId}
16514
+ AND ${engagementPredicate}
16515
+ GROUP BY 1
16516
+ `);
16517
+ const counts = {};
16518
+ for (const row of rows) counts[row.source] = {
16519
+ chatCount: Number(row.chat_count),
16520
+ unreadChatCount: Number(row.unread_chat_count)
16521
+ };
16522
+ if (!counts.manual) counts.manual = {
16523
+ chatCount: 0,
16524
+ unreadChatCount: 0
16525
+ };
16526
+ return { counts };
16527
+ }
16528
+ /**
16180
16529
  * Class C — resource-scoped chat routes. Mounted at
16181
16530
  * `/api/v1/chats/:chatId/...`. The chat's `organizationId` locates its
16182
16531
  * org; `requireChatAccess` resolves the caller's membership in that org
@@ -16197,7 +16546,9 @@ async function chatRoutes(app) {
16197
16546
  const agentRows = participantAgentIds.length > 0 ? await app.db.select({
16198
16547
  agentId: agents.uuid,
16199
16548
  displayName: agents.displayName,
16200
- type: agents.type
16549
+ type: agents.type,
16550
+ avatarColorToken: agents.avatarColorToken,
16551
+ avatarImageUpdatedAt: agents.avatarImageUpdatedAt
16201
16552
  }).from(agents).where(inArray(agents.uuid, participantAgentIds)) : [];
16202
16553
  const agentMeta = new Map(agentRows.map((a) => [a.agentId, a]));
16203
16554
  const participantsForTitle = participants.map((p) => {
@@ -16205,7 +16556,9 @@ async function chatRoutes(app) {
16205
16556
  return {
16206
16557
  agentId: p.agentId,
16207
16558
  displayName: meta?.displayName ?? p.agentId,
16208
- type: meta?.type ?? "unknown"
16559
+ type: meta?.type ?? "unknown",
16560
+ avatarColorToken: meta?.avatarColorToken ?? null,
16561
+ avatarImageUrl: agentAvatarImageUrl(p.agentId, meta?.avatarImageUpdatedAt ?? null)
16209
16562
  };
16210
16563
  });
16211
16564
  const title = resolveChatTitle(chat.topic, firstMessagePreview, participantsForTitle, scope.humanAgentId);
@@ -16684,6 +17037,9 @@ const WINDOW_DAYS = {
16684
17037
  "7d": 7,
16685
17038
  "30d": 30
16686
17039
  };
17040
+ function contextTreeSnapshotWindowDays(window) {
17041
+ return WINDOW_DAYS[window];
17042
+ }
16687
17043
  const snapshotCache = /* @__PURE__ */ new Map();
16688
17044
  const remoteSyncPromises = /* @__PURE__ */ new Map();
16689
17045
  const remoteLastSyncedAt = /* @__PURE__ */ new Map();
@@ -16727,6 +17083,7 @@ async function getContextTreeSnapshot(binding, window = CONTEXT_TREE_SNAPSHOT_WI
16727
17083
  snapshotStatus: statusWarning?.stale ? "stale" : "active",
16728
17084
  contextStatus: contextStatus(statusWarning),
16729
17085
  summary,
17086
+ usage: emptyUsageSummary(window),
16730
17087
  updates,
16731
17088
  nodes: nodesWithGhosts,
16732
17089
  edges: tree.edges,
@@ -16981,6 +17338,13 @@ function withSnapshotStatus(snapshot, syncedAt, warning) {
16981
17338
  contextStatus: warning ? contextStatus(warning) : snapshot.contextStatus
16982
17339
  };
16983
17340
  }
17341
+ function emptyUsageSummary(window) {
17342
+ return {
17343
+ windowDays: WINDOW_DAYS[window],
17344
+ agentCount: 0,
17345
+ usageCount: 0
17346
+ };
17347
+ }
16984
17348
  function isSafeBranchName(branch) {
16985
17349
  if (branch.startsWith("-")) return false;
16986
17350
  if (branch.includes("..") || branch.includes("@{") || branch.includes("\\")) return false;
@@ -17011,6 +17375,7 @@ function unavailableSnapshot(repo, branch, detail) {
17011
17375
  removedCount: 0,
17012
17376
  changedNodeCount: 0
17013
17377
  },
17378
+ usage: emptyUsageSummary(CONTEXT_TREE_SNAPSHOT_WINDOWS.SEVEN_DAYS),
17014
17379
  updates: [],
17015
17380
  nodes: [],
17016
17381
  edges: [],
@@ -17586,11 +17951,16 @@ async function contextTreeSnapshotRoutes(app) {
17586
17951
  const orgId = await resolveUserPrimaryOrgId(app.db, userId);
17587
17952
  const binding = orgId ? await getOrgContextTree(app.db, orgId) : {};
17588
17953
  const githubToken = contextTreeGithubTokenForRepo(binding.repo, app.config.contextTreeSync);
17954
+ const window = query.window ?? "7d";
17589
17955
  const snapshot = await getContextTreeSnapshot({
17590
17956
  ...binding,
17591
17957
  githubToken
17592
- }, query.window ?? "7d");
17593
- return contextTreeSnapshotSchema.parse(snapshot);
17958
+ }, window);
17959
+ const usage = orgId ? await summarizeContextTreeUsage(app.db, orgId, contextTreeSnapshotWindowDays(window)) : snapshot.usage;
17960
+ return contextTreeSnapshotSchema.parse({
17961
+ ...snapshot,
17962
+ usage
17963
+ });
17594
17964
  });
17595
17965
  }
17596
17966
  function contextTreeGithubTokenForRepo(repo, syncConfig) {
@@ -17719,7 +18089,7 @@ async function healthzRoutes(app) {
17719
18089
  * `api/orgs/invitations.ts` (Class B, admin-gated).
17720
18090
  */
17721
18091
  async function publicInvitationRoutes(app) {
17722
- const { previewInvitation } = await import("./invitation-CNv7gfFF-D93KQte0.mjs");
18092
+ const { previewInvitation } = await import("./invitation-CNv7gfFF-DOFZ75wb.mjs");
17723
18093
  app.get("/:token/preview", async (request, reply) => {
17724
18094
  if (!request.params.token) throw new UnauthorizedError("Token required");
17725
18095
  const preview = await previewInvitation(app.db, request.params.token);
@@ -17798,6 +18168,7 @@ async function meRoutes(app) {
17798
18168
  createdAt: m.createdAt
17799
18169
  })));
17800
18170
  const defaultOrgId = defaultMembership ? memberships.find((m) => m.memberId === defaultMembership.id)?.organizationId ?? null : null;
18171
+ const memberCounts = await countActiveMembersByOrgs(app.db, memberships.map((mb) => mb.organizationId));
17801
18172
  let inviteUrl = null;
17802
18173
  if (defaultOrgId) {
17803
18174
  if (memberships.find((m) => m.organizationId === defaultOrgId)?.role === "admin") {
@@ -17814,7 +18185,8 @@ async function meRoutes(app) {
17814
18185
  organizationId: mb.organizationId,
17815
18186
  organizationName: mb.orgDisplayName,
17816
18187
  role: mb.role,
17817
- agentId: mb.agentId
18188
+ agentId: mb.agentId,
18189
+ orgHasOtherMembers: (memberCounts.get(mb.organizationId) ?? 1) > 1
17818
18190
  })),
17819
18191
  onboarding: {
17820
18192
  step: onboardingStep,
@@ -18006,7 +18378,7 @@ async function meRoutes(app) {
18006
18378
  */
18007
18379
  app.get("/me/pinned-agents", async (request) => {
18008
18380
  const { userId } = requireUser(request);
18009
- const { listMyPinnedAgents } = await import("./client-h4KZ3b9o-CQyibXig.mjs");
18381
+ const { listMyPinnedAgents } = await import("./client-gSnsRu5W-v_mC1sRY.mjs");
18010
18382
  return listMyPinnedAgents(app.db, { userId });
18011
18383
  });
18012
18384
  /**
@@ -18136,6 +18508,80 @@ async function inferOnboardingStep(app, userId) {
18136
18508
  if (!hasAgent) return "create_agent";
18137
18509
  return "completed";
18138
18510
  }
18511
+ const MAX_DOC_BYTES = 5 * 1024 * 1024;
18512
+ async function getMeDocPreview(input) {
18513
+ const workspaceRootReal = await realpathOrNotFound(join(input.workspacesRoot ?? join(DEFAULT_DATA_DIR$1, "workspaces"), input.agentName, input.chatId));
18514
+ const candidate = resolve(workspaceRootReal, join(input.basePath ?? "", input.path));
18515
+ if (extname(assertInsideWorkspace(workspaceRootReal, candidate)).toLowerCase() !== ".md") throw new ForbiddenError("Document preview only supports markdown files in the agent workspace");
18516
+ let fileStat;
18517
+ try {
18518
+ fileStat = await stat(candidate);
18519
+ } catch {
18520
+ throw new NotFoundError("Document not found");
18521
+ }
18522
+ if (!fileStat.isFile()) throw new NotFoundError("Document not found");
18523
+ if (fileStat.size > MAX_DOC_BYTES) throw new AppError(413, "Document is larger than the 5MB preview limit");
18524
+ const fileReal = await realpath(candidate);
18525
+ const normalizedPath = assertInsideWorkspace(workspaceRootReal, fileReal);
18526
+ const refPath = normalizeRefPath(input.path);
18527
+ const normalizedBasePath = input.basePath ? normalizeRefPath(input.basePath) : void 0;
18528
+ return {
18529
+ ref: {
18530
+ type: "workspace",
18531
+ chatId: input.chatId,
18532
+ agentId: input.agentId,
18533
+ ...normalizedBasePath ? { basePath: normalizedBasePath } : {},
18534
+ path: refPath
18535
+ },
18536
+ path: normalizedPath,
18537
+ content: await readFile(fileReal, "utf8")
18538
+ };
18539
+ }
18540
+ async function realpathOrNotFound(path) {
18541
+ try {
18542
+ return await realpath(path);
18543
+ } catch {
18544
+ throw new NotFoundError("Document not found");
18545
+ }
18546
+ }
18547
+ function assertInsideWorkspace(workspaceRoot, target) {
18548
+ const rel = relative(workspaceRoot, target);
18549
+ if (!rel || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) throw new ForbiddenError("Document path must stay inside the agent workspace");
18550
+ return rel.split(sep).join("/");
18551
+ }
18552
+ function normalizeRefPath(path) {
18553
+ const parts = [];
18554
+ for (const part of path.split(/[\\/]/)) {
18555
+ if (!part || part === ".") continue;
18556
+ if (part === "..") {
18557
+ if (parts.length === 0) throw new ForbiddenError("Document path must stay inside the agent workspace");
18558
+ parts.pop();
18559
+ continue;
18560
+ }
18561
+ parts.push(part);
18562
+ }
18563
+ if (parts.length === 0) throw new ForbiddenError("Document path must name a markdown file");
18564
+ return parts.join("/");
18565
+ }
18566
+ async function meDocsRoutes(app, options = {}) {
18567
+ app.get("/chats/:chatId/docs/preview", async (request) => {
18568
+ await requireChatAccess(request, app.db);
18569
+ const query = getMeDocSchema.parse(request.query);
18570
+ const [participant] = await app.db.select({ agentId: chatMembership.agentId }).from(chatMembership).where(and(eq(chatMembership.chatId, request.params.chatId), eq(chatMembership.agentId, query.agentId), eq(chatMembership.accessMode, "speaker"))).limit(1);
18571
+ if (!participant) throw new NotFoundError("Document not found");
18572
+ const [agent] = await app.db.select({ name: agents.name }).from(agents).where(eq(agents.uuid, query.agentId)).limit(1);
18573
+ if (!agent?.name) throw new NotFoundError("Document not found");
18574
+ const preview = await getMeDocPreview({
18575
+ chatId: request.params.chatId,
18576
+ agentId: query.agentId,
18577
+ agentName: agent.name,
18578
+ basePath: query.basePath,
18579
+ path: query.path,
18580
+ workspacesRoot: options.workspacesRoot
18581
+ });
18582
+ return getMeDocResponseSchema.parse(preview);
18583
+ });
18584
+ }
18139
18585
  /**
18140
18586
  * Resolve the caller's active membership in `:orgId` (from the URL) and
18141
18587
  * return the full `OrgScope`. The type signature requires
@@ -18316,7 +18762,7 @@ async function orgAgentRoutes(app) {
18316
18762
  const { type } = listAgentsFilterSchema.parse(request.query);
18317
18763
  const result = await listAgentsForMember(app.db, scope, query.limit, query.cursor, type);
18318
18764
  return {
18319
- items: result.items.map((a) => ({
18765
+ items: result.items.map(({ avatarImageUpdatedAt, ...a }) => ({
18320
18766
  ...a,
18321
18767
  managerId: a.managerId ?? null,
18322
18768
  presenceStatus: a.presenceStatus ?? "offline",
@@ -18325,7 +18771,8 @@ async function orgAgentRoutes(app) {
18325
18771
  clientId: a.clientId ?? null,
18326
18772
  runtimeType: a.runtimeType ?? null,
18327
18773
  runtimeState: a.runtimeState ?? null,
18328
- activeSessions: a.activeSessions ?? null
18774
+ activeSessions: a.activeSessions ?? null,
18775
+ avatarImageUrl: agentAvatarImageUrl(a.uuid, avatarImageUpdatedAt)
18329
18776
  })),
18330
18777
  nextCursor: result.nextCursor
18331
18778
  };
@@ -18341,7 +18788,7 @@ async function orgAgentRoutes(app) {
18341
18788
  const query = paginationQuerySchema.parse(request.query);
18342
18789
  const result = await listAgentsForAdmin(app.db, scope, query.limit, query.cursor);
18343
18790
  return {
18344
- items: result.items.map((a) => ({
18791
+ items: result.items.map(({ avatarImageUpdatedAt, ...a }) => ({
18345
18792
  ...a,
18346
18793
  managerId: a.managerId ?? null,
18347
18794
  presenceStatus: a.presenceStatus ?? "offline",
@@ -18350,7 +18797,8 @@ async function orgAgentRoutes(app) {
18350
18797
  clientId: a.clientId ?? null,
18351
18798
  runtimeType: a.runtimeType ?? null,
18352
18799
  runtimeState: a.runtimeState ?? null,
18353
- activeSessions: a.activeSessions ?? null
18800
+ activeSessions: a.activeSessions ?? null,
18801
+ avatarImageUrl: agentAvatarImageUrl(a.uuid, avatarImageUpdatedAt)
18354
18802
  })),
18355
18803
  nextCursor: result.nextCursor
18356
18804
  };
@@ -18443,6 +18891,17 @@ async function orgChatRoutes(app) {
18443
18891
  return listMeChats(app.db, scope.humanAgentId, scope.organizationId, query);
18444
18892
  });
18445
18893
  /**
18894
+ * GET /orgs/:orgId/chats/source-counts — per-source aggregate powering the
18895
+ * conversation-list tag bar (Manual / GitHub PR / GitHub Issue / Feishu).
18896
+ * Returns counts only for sources the caller has chats in, plus an
18897
+ * always-present `manual` entry. Same engagement view filter as the list.
18898
+ */
18899
+ app.get("/source-counts", async (request) => {
18900
+ const scope = await requireOrgMembership(request, app.db);
18901
+ const query = listMeChatSourceCountsQuerySchema.parse(request.query);
18902
+ return listMeChatSourceCounts(app.db, scope.humanAgentId, scope.organizationId, query);
18903
+ });
18904
+ /**
18446
18905
  * POST /orgs/:orgId/chats — create a new chat. The :orgId path param
18447
18906
  * makes the org explicit; visibility of every requested participant is
18448
18907
  * verified before the service layer touches the DB.
@@ -18496,11 +18955,16 @@ async function orgContextTreeSnapshotRoutes(app) {
18496
18955
  const scope = await requireOrgMembership(request, app.db);
18497
18956
  const binding = await getOrgContextTree(app.db, scope.organizationId);
18498
18957
  const githubToken = contextTreeGithubTokenForRepo(binding.repo, app.config.contextTreeSync);
18958
+ const window = query.window ?? "7d";
18499
18959
  const snapshot = await getContextTreeSnapshot({
18500
18960
  ...binding,
18501
18961
  githubToken
18502
- }, query.window ?? "7d");
18503
- return contextTreeSnapshotSchema.parse(snapshot);
18962
+ }, window);
18963
+ const usage = await summarizeContextTreeUsage(app.db, scope.organizationId, contextTreeSnapshotWindowDays(window));
18964
+ return contextTreeSnapshotSchema.parse({
18965
+ ...snapshot,
18966
+ usage
18967
+ });
18504
18968
  });
18505
18969
  }
18506
18970
  function orgIdParam(params) {
@@ -19141,6 +19605,64 @@ const githubEntityChatMappings = pgTable("github_entity_chat_mappings", {
19141
19605
  table.entityType,
19142
19606
  table.entityKey
19143
19607
  ] }), index("idx_github_entity_chat_mappings_chat").on(table.chatId)]);
19608
+ /**
19609
+ * Auto-archive chats when one of their bound pull requests is merged.
19610
+ *
19611
+ * Trigger: GitHub `pull_request.closed` webhook with `merged === true`. The
19612
+ * webhook handler calls this service on a bypass branch — the normalize /
19613
+ * audience / deliver pipeline is unaffected (PR closed events still drop in
19614
+ * Stage 1).
19615
+ *
19616
+ * Algorithm: a merged PR flips every chat bound to it into the user's
19617
+ * archived view, with no inspection of sibling PR state. Multi-PR chats can
19618
+ * be temporarily archived while siblings are still open; any later activity
19619
+ * on those siblings produces a normal delivery message, which the existing
19620
+ * chat-projection auto-revives back to `active`. This trades perfect timing
19621
+ * for zero local state, no GitHub API calls, and no schema changes.
19622
+ *
19623
+ * Per-user safety: writes use an UPSERT guarded with
19624
+ * `setWhere = engagement_status = 'active'`, so only the implicit-active or
19625
+ * explicitly-active rows flip. User-manually `deleted` and already-`archived`
19626
+ * rows are left alone. Idempotent under GitHub webhook retries.
19627
+ */
19628
+ const { ACTIVE, ARCHIVED } = CHAT_ENGAGEMENT_STATUSES;
19629
+ async function archiveChatsForMergedPr(db, input) {
19630
+ if (!input.repoFullName || !Number.isFinite(input.prNumber) || input.prNumber <= 0) return {
19631
+ chats: 0,
19632
+ rowsConsidered: 0
19633
+ };
19634
+ const entityKey = `${input.repoFullName}#${input.prNumber}`;
19635
+ const rows = await db.select({
19636
+ chatId: githubEntityChatMappings.chatId,
19637
+ humanAgentId: githubEntityChatMappings.humanAgentId
19638
+ }).from(githubEntityChatMappings).where(and(eq(githubEntityChatMappings.organizationId, input.organizationId), eq(githubEntityChatMappings.entityType, "pull_request"), eq(githubEntityChatMappings.entityKey, entityKey)));
19639
+ if (rows.length === 0) return {
19640
+ chats: 0,
19641
+ rowsConsidered: 0
19642
+ };
19643
+ const seen = /* @__PURE__ */ new Set();
19644
+ const targets = [];
19645
+ for (const row of rows) {
19646
+ const key = `${row.chatId}|${row.humanAgentId}`;
19647
+ if (seen.has(key)) continue;
19648
+ seen.add(key);
19649
+ targets.push(row);
19650
+ }
19651
+ for (const { chatId, humanAgentId } of targets) await db.insert(chatUserState).values({
19652
+ chatId,
19653
+ agentId: humanAgentId,
19654
+ unreadMentionCount: 0,
19655
+ engagementStatus: ARCHIVED
19656
+ }).onConflictDoUpdate({
19657
+ target: [chatUserState.chatId, chatUserState.agentId],
19658
+ set: { engagementStatus: ARCHIVED },
19659
+ setWhere: eq(chatUserState.engagementStatus, ACTIVE)
19660
+ });
19661
+ return {
19662
+ chats: new Set(targets.map((t) => t.chatId)).size,
19663
+ rowsConsidered: targets.length
19664
+ };
19665
+ }
19144
19666
  function evaluateDelegateTarget(target, sourceOrgId) {
19145
19667
  if (!target) return "not_found";
19146
19668
  if (target.organizationId !== sourceOrgId) return "cross_org";
@@ -20286,6 +20808,28 @@ async function githubAppWebhookRoutes(app) {
20286
20808
  };
20287
20809
  const deliveryHeader = request.headers["x-github-delivery"];
20288
20810
  const deliveryId = typeof deliveryHeader === "string" && deliveryHeader.length > 0 ? deliveryHeader : null;
20811
+ if (eventType === "pull_request" && isRecord(payload) && payload.action === "closed") {
20812
+ const pr = isRecord(payload.pull_request) ? payload.pull_request : null;
20813
+ const repoFullName = readString$1((isRecord(payload.repository) ? payload.repository : null)?.full_name);
20814
+ const prNumber = readNumber$1(pr?.number);
20815
+ if (pr?.merged === true && repoFullName && prNumber !== null) try {
20816
+ const stats = await archiveChatsForMergedPr(app.db, {
20817
+ organizationId: installation.hubOrganizationId,
20818
+ repoFullName,
20819
+ prNumber
20820
+ });
20821
+ log$1.info({
20822
+ entityKey: `${repoFullName}#${prNumber}`,
20823
+ ...stats
20824
+ }, "auto-archived chats on PR merge");
20825
+ } catch (err) {
20826
+ log$1.error({
20827
+ err,
20828
+ repoFullName,
20829
+ prNumber
20830
+ }, "failed to auto-archive chats on PR merge");
20831
+ }
20832
+ }
20289
20833
  const event = normalizeGithubEvent(eventType, payload, source, deliveryId);
20290
20834
  if (!event) {
20291
20835
  log$1.debug({
@@ -21886,12 +22430,14 @@ async function buildApp(config) {
21886
22430
  await api.register(githubOauthRoutes, { prefix: "/auth/github" });
21887
22431
  await api.register(publicInvitationRoutes, { prefix: "/invitations" });
21888
22432
  await api.register(bootstrapConfigRoutes, { prefix: "/bootstrap" });
22433
+ await api.register(publicAgentAvatarRoutes, { prefix: "/agents" });
21889
22434
  await api.register(userScope("contextTreeScope", async (scope) => {
21890
22435
  await scope.register(contextTreeInfoRoutes);
21891
22436
  await scope.register(contextTreeSnapshotRoutes);
21892
22437
  }), { prefix: "/context-tree" });
21893
22438
  await api.register(userScope("meRoutesScope", async (scope) => {
21894
22439
  await scope.register(meRoutes);
22440
+ await scope.register(meDocsRoutes, { workspacesRoot: config.workspace.root });
21895
22441
  }), { prefix: "" });
21896
22442
  await api.register(userScope("orgsScope", async (scope) => {
21897
22443
  await scope.register(orgIdentityRoutes);