@agent-team-foundation/first-tree-hub 0.12.4 → 0.12.5

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.
@@ -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-C_K2CKXC.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 runtimeStateMessageSchema, B as isReservedAgentName$1, C as createChatSchema, D as defaultRuntimeConfigPayload, E as createOrgFromMeSchema, F as inboxAckFrameSchema, G as notificationQuerySchema, H as listMeChatsQuerySchema, I as inboxDeliverFrameSchema$1, J as patchOnboardingSchema, K as onboardingEventSchema, L as inboxPollQuerySchema, M as githubDevCallbackQuerySchema, N as githubStartQuerySchema, O as delegateFeishuUserSchema, P as imageInlineContentSchema, Q as refreshTokenSchema, R as isOrgSettingNamespace, S as createAgentSchema, T as createMemberSchema, U as loginSchema, V as joinByInvitationSchema, W as messageSourceSchema$1, Z as rebindAgentSchema, _ as clientRegisterSchema, _t as updateMemberSchema, a as AGENT_VISIBILITY, at as sessionCompletionMessageSchema, b as createAdapterConfigSchema, c as ORG_SETTINGS_NAMESPACES$1, ct as sessionReconcileRequestSchema, d as addParticipantSchema, dt as submitQuestionAnswerSchema, et as safeRedirectPath, f as agentBindRequestSchema, ft as updateAdapterConfigSchema, gt as updateClientCapabilitiesSchema, h as agentTypeSchema$1, ht as updateChatSchema, i as AGENT_STATUSES, it as sendToAgentSchema, j as githubCallbackQuerySchema, k as dryRunAgentRuntimeConfigSchema, l as WS_AUTH_FRAME_TIMEOUT_MS, lt as sessionStateMessageSchema, m as agentRuntimeConfigPayloadSchema$1, mt as updateAgentSchema, n as AGENT_NAME_REGEX$1, nt as selfServiceFeishuBotSchema, ot as sessionEventMessageSchema, p as agentPinnedMessageSchema$1, pt as updateAgentRuntimeConfigSchema, q as paginationQuerySchema, r as AGENT_SELECTOR_HEADER$1, rt as sendMessageSchema, s as MENTION_REGEX, st as sessionEventSchema$1, t as AGENT_BIND_REJECT_REASONS, u as addMeChatParticipantsSchema, ut as stripCode, v as connectTokenExchangeSchema, vt as updateOrganizationSchema, w as createMeChatSchema, x as createAdapterMappingSchema, y as contextTreeSnapshotSchema, yt as wsAuthFrameSchema, z as isRedactedEnvValue } from "./dist-CMhywpXB.mjs";
5
+ import { $ as refreshTokenSchema, A as dryRunAgentRuntimeConfigSchema, B as isRedactedEnvValue, C as createAgentSchema, D as createOrgFromMeSchema, E as createMemberSchema, F as imageInlineContentSchema, G as messageSourceSchema$1, H as joinByInvitationSchema, I as inboxAckFrameSchema, J as paginationQuerySchema, K as notificationQuerySchema, L as inboxDeliverFrameSchema$1, M as githubCallbackQuerySchema, N as githubDevCallbackQuerySchema, O as defaultRuntimeConfigPayload, P as githubStartQuerySchema, Q as rebindAgentSchema, R as inboxPollQuerySchema, S as createAdapterMappingSchema, T as createMeChatSchema, U as listMeChatsQuerySchema, V as isReservedAgentName$1, W as loginSchema, Y as patchOnboardingSchema, _t as updateClientCapabilitiesSchema, a as AGENT_VISIBILITY, at as sendToAgentSchema, b as contextTreeSnapshotSchema, bt as wsAuthFrameSchema, c as ORG_SETTINGS_NAMESPACES$1, ct as sessionEventSchema$1, d as addParticipantSchema, dt as stripCode, et as runtimeStateMessageSchema, f as agentBindRequestSchema, ft as submitQuestionAnswerSchema, g as chatMetadataSchema$1, gt as updateChatSchema, h as agentTypeSchema$1, ht as updateAgentSchema, i as AGENT_STATUSES, it as sendMessageSchema, k as delegateFeishuUserSchema, l as WS_AUTH_FRAME_TIMEOUT_MS, lt as sessionReconcileRequestSchema, m as agentRuntimeConfigPayloadSchema$1, mt as updateAgentRuntimeConfigSchema, n as AGENT_NAME_REGEX$1, ot as sessionCompletionMessageSchema, p as agentPinnedMessageSchema$1, pt as updateAdapterConfigSchema, q as onboardingEventSchema, r as AGENT_SELECTOR_HEADER$1, rt as selfServiceFeishuBotSchema, s as MENTION_REGEX, st as sessionEventMessageSchema, t as AGENT_BIND_REJECT_REASONS, tt as safeRedirectPath, u as addMeChatParticipantsSchema, ut as sessionStateMessageSchema, v as clientRegisterSchema, vt as updateMemberSchema, w as createChatSchema, x as createAdapterConfigSchema, y as connectTokenExchangeSchema, yt as updateOrganizationSchema, z as isOrgSettingNamespace } from "./dist-BwPlBZWi.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 pendingQuestions, A as heartbeatClient, B as listAgentsWithRuntime, C as findOrCreateDirectChat, D as getClient, E as getChatDetail, F as joinChat, G as listClientsForOrgAdmin, H as listChats, I as leaveAsParticipant, J as markStaleAgents, K as listMessages, L as leaveChat, M as inboxEntries, N as invalidateChatAudience, O as getOnlineCount, P as joinAsParticipant, Q as notifyRecipients, R as listActiveAgentsPinnedToClient, S as ensureParticipant$1, T as getCachedAudience, U as listChatsForMember, V as listChatParticipantsWithNames, W as listClients, X as members, Y as markSupersededByChat, Z as messages, _ as createNotifier, _t as updateClientCapabilities, a as agents, at as removeParticipant, b as editMessage, c as bindAgent, ct as retireClient, d as chats, dt as serverInstances, et as recomputeChatWatchers, f as claimClient, ft as setOffline, g as createChat, gt as unbindAgent, h as clients, ht as touchAgent, i as agentVisibilityCondition, it as registerClient, j as heartbeatInstance, k as getPresence, l as chatParticipants, lt as sendMessage, m as cleanupStalePresence, mt as submitAnswer, n as agentChatSessions, nt as recomputeWatchersForMember, o as assertClientOwner, ot as resetActivity, p as cleanupStaleClients, pt as setRuntimeState, r as agentPresence, rt as registerChatMessageDispatcher, s as assertParticipant, st as resolveChatMembership, t as addParticipant, tt as recomputeWatchersForAgent, u as chatSubscriptions, ut as sendToAgent$1, v as deriveAuthState, vt as upsertSessionState, w as getActivityOverview, x as ensureCanJoin, y as disconnectClient, z as listAgentsManagedByUser } from "./client-D1TDiik_-NV_lkhfI.mjs";
8
+ import { $ as pendingQuestions, A as heartbeatClient, B as listAgentsWithRuntime, C as findOrCreateDirectChat, D as getClient, E as getChatDetail, F as joinChat, G as listClientsForOrgAdmin, H as listChats, I as leaveAsParticipant, J as markStaleAgents, K as listMessages, L as leaveChat, M as inboxEntries, N as invalidateChatAudience, O as getOnlineCount, P as joinAsParticipant, Q as notifyRecipients, R as listActiveAgentsPinnedToClient, S as ensureParticipant$1, T as getCachedAudience, U as listChatsForMember, V as listChatParticipantsWithNames, W as listClients, X as members, Y as markSupersededByChat, Z as messages, _ as createNotifier, _t as updateClientCapabilities, a as agents, at as removeParticipant, b as editMessage, c as bindAgent, ct as retireClient, d as chats, dt as serverInstances, et as recomputeChatWatchers, f as claimClient, ft as setOffline, g as createChat, gt as unbindAgent, h as clients, ht as touchAgent, i as agentVisibilityCondition, it as registerClient, j as heartbeatInstance, k as getPresence, l as chatParticipants, lt as sendMessage, m as cleanupStalePresence, mt as submitAnswer, n as agentChatSessions, nt as recomputeWatchersForMember, o as assertClientOwner, ot as resetActivity, p as cleanupStaleClients, pt as setRuntimeState, r as agentPresence, rt as registerChatMessageDispatcher, s as assertParticipant, st as resolveChatMembership, t as addParticipant, tt as recomputeWatchersForAgent, u as chatSubscriptions, ut as sendToAgent$1, v as deriveAuthState, vt as upsertSessionState, w as getActivityOverview, x as ensureCanJoin, y as disconnectClient, z as listAgentsManagedByUser } from "./client-DL5vHhvQ-CnYGq2x-.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";
@@ -730,6 +730,30 @@ z.object({
730
730
  expiresIn: z.number(),
731
731
  command: z.string()
732
732
  });
733
+ const githubEntityTypeSchema = z.enum([
734
+ "issue",
735
+ "pull_request",
736
+ "discussion",
737
+ "commit"
738
+ ]);
739
+ const githubChatMetadataSchema = z.object({
740
+ source: z.literal("github"),
741
+ entityType: githubEntityTypeSchema,
742
+ entityKey: z.string().min(1),
743
+ entityUrl: z.string().url().optional()
744
+ });
745
+ const feishuChatMetadataSchema = z.object({
746
+ source: z.literal("feishu"),
747
+ externalChannelId: z.string().min(1)
748
+ });
749
+ const chatMetadataSchema = z.discriminatedUnion("source", [githubChatMetadataSchema, feishuChatMetadataSchema]);
750
+ /**
751
+ * `createChat` callers may not set metadata at all (admin-created group chats,
752
+ * me-chats, …), so the input schema accepts either an empty object or one of
753
+ * the typed variants. The empty `{}` arm is `.strict()` so a caller cannot
754
+ * sneak through `{ source: "github" }` without the required fields.
755
+ */
756
+ const optionalChatMetadataSchema = z.union([z.object({}).strict(), chatMetadataSchema]);
733
757
  const chatTypeSchema = z.enum([
734
758
  "direct",
735
759
  "group",
@@ -739,7 +763,7 @@ z.object({
739
763
  type: chatTypeSchema,
740
764
  topic: z.string().max(500).optional(),
741
765
  participantIds: z.array(z.string()).min(1),
742
- metadata: z.record(z.string(), z.unknown()).optional()
766
+ metadata: optionalChatMetadataSchema.optional()
743
767
  });
744
768
  const chatParticipantSchema = z.object({
745
769
  agentId: z.string(),
@@ -3031,7 +3055,8 @@ function generateToolsDoc() {
3031
3055
  You are running inside **Agent Hub**, a messaging platform for agent teams.
3032
3056
 
3033
3057
  - Messages from other team members arrive as your prompt input
3034
- - Each message includes a \`[From: sender-id]\` header so you know who sent it
3058
+ - Each message includes a \`[From: <agent-name>]\` header that name is also
3059
+ what you pass back to \`agent send\` to reply to or address that agent
3035
3060
  - **Your final text response is automatically delivered** to the chat — just respond normally
3036
3061
  - For **proactive communication** (sending to other agents, other chats, or structured data),
3037
3062
  use the \`first-tree-hub\` CLI below
@@ -3046,8 +3071,8 @@ These are injected automatically when the agent process starts:
3046
3071
  |----------|-------------|
3047
3072
  | \`FIRST_TREE_HUB_SERVER_URL\` | Server address for API calls |
3048
3073
  | \`FIRST_TREE_HUB_ACCESS_TOKEN\` | User member access JWT (short-lived) |
3049
- | \`FIRST_TREE_HUB_AGENT_ID\` | Your agent UUID — send as \`X-Agent-Id\` |
3050
- | \`FIRST_TREE_HUB_CHAT_ID\` | Current chat context ID |
3074
+ | \`FIRST_TREE_HUB_AGENT_ID\` | YOUR own agent UUID. The CLI reads it to identify you as the sender never pass it as a \`send\` target. |
3075
+ | \`FIRST_TREE_HUB_CHAT_ID\` | The chat this session is currently bound to. The CLI uses it to route messages — you don't need to pass it manually. |
3051
3076
 
3052
3077
  The \`first-tree-hub\` CLI reads these automatically — no extra setup needed.
3053
3078
 
@@ -3057,13 +3082,18 @@ Use the \`first-tree-hub agent send\` CLI — it reads the env vars above and
3057
3082
  attaches the \`Authorization\` + \`X-Agent-Id\` headers automatically:
3058
3083
 
3059
3084
  \`\`\`bash
3060
- # Send to another agent — target is the agent NAME, NOT a uuid.
3061
- # Names are stable (set on creation, immutable, unique in the org).
3085
+ # Send to another agent — first positional argument is the recipient's NAME
3086
+ # (NOT a uuid; uuids in chat history / participant lists are not accepted).
3062
3087
  # Run \`first-tree-hub agent list\` to see available names.
3088
+ #
3089
+ # Routing: if the recipient is a participant of your current chat (typically
3090
+ # the case in a group chat where someone @-mentioned you to talk to them),
3091
+ # the message stays in that chat. Otherwise it falls back to a direct chat
3092
+ # between you and the recipient. You don't need to think about which.
3063
3093
  first-tree-hub agent send <agentName> "your message"
3064
3094
 
3065
- # Send to a chat (target is a chat UUID; use this when replying into a
3066
- # specific chat, e.g. a group where you were mentioned)
3095
+ # Send into a specific chat by id use this only when you explicitly want
3096
+ # to address a chat your current session is NOT bound to.
3067
3097
  first-tree-hub agent send --chat <chatId> "your message"
3068
3098
 
3069
3099
  # Send markdown (default format is text)
@@ -6922,6 +6952,36 @@ function resolveReplyToFromEnv(env, override) {
6922
6952
  replyToChat: override.replyToChat ?? (envComplete ? envChatId : void 0)
6923
6953
  };
6924
6954
  }
6955
+ function resolveSenderName(input) {
6956
+ const { override, envAgentId, agents } = input;
6957
+ if (agents.size === 0) return { kind: "none" };
6958
+ if (override) return {
6959
+ kind: "ok",
6960
+ name: override
6961
+ };
6962
+ if (envAgentId) {
6963
+ for (const [name, cfg] of agents) if (cfg.agentId === envAgentId) return {
6964
+ kind: "ok",
6965
+ name
6966
+ };
6967
+ return {
6968
+ kind: "envMismatch",
6969
+ envAgentId,
6970
+ available: [...agents.keys()]
6971
+ };
6972
+ }
6973
+ if (agents.size === 1) {
6974
+ const [only] = [...agents.keys()];
6975
+ if (only) return {
6976
+ kind: "ok",
6977
+ name: only
6978
+ };
6979
+ }
6980
+ return {
6981
+ kind: "ambiguous",
6982
+ available: [...agents.keys()]
6983
+ };
6984
+ }
6925
6985
  //#endregion
6926
6986
  //#region src/core/admin.ts
6927
6987
  /**
@@ -9091,7 +9151,7 @@ async function onboardCreate(args) {
9091
9151
  }
9092
9152
  const runtimeAgent = args.type === "human" ? args.assistant : args.id;
9093
9153
  if (args.feishuBotAppId && args.feishuBotAppSecret) {
9094
- const { bindFeishuBot } = await import("./feishu-tkZS0vvL.mjs").then((n) => n.r);
9154
+ const { bindFeishuBot } = await import("./feishu-CKGzIamp.mjs").then((n) => n.r);
9095
9155
  const targetAgentUuid = args.type === "human" ? assistantUuid : primary.uuid;
9096
9156
  if (!targetAgentUuid) print.line(`Warning: Cannot bind Feishu bot — no runtime agent available for "${args.id}".\n`);
9097
9157
  else {
@@ -10304,7 +10364,7 @@ function createFeedbackHandler(config) {
10304
10364
  return { handle };
10305
10365
  }
10306
10366
  //#endregion
10307
- //#region ../server/dist/app-D4vCx0C0.mjs
10367
+ //#region ../server/dist/app-kJNM9Cf1.mjs
10308
10368
  var import_fastify_opentelemetry = /* @__PURE__ */ __toESM(require_fastify_opentelemetry(), 1);
10309
10369
  init_esm();
10310
10370
  var __defProp = Object.defineProperty;
@@ -11470,16 +11530,17 @@ async function findOrCreateChatForChannel(db, data) {
11470
11530
  return db.transaction(async (tx) => {
11471
11531
  const [botAgent] = await tx.select({ organizationId: agents.organizationId }).from(agents).where(eq(agents.uuid, data.botAgentId)).limit(1);
11472
11532
  const orgId = botAgent?.organizationId ?? await resolveDefaultOrgId(db);
11533
+ const metadata = chatMetadataSchema$1.parse({
11534
+ source: data.platform,
11535
+ externalChannelId: data.externalChannelId
11536
+ });
11473
11537
  await tx.insert(chats).values({
11474
11538
  id: chatId,
11475
11539
  organizationId: orgId,
11476
11540
  type: internalType,
11477
11541
  topic: data.topic ?? null,
11478
11542
  lifecyclePolicy: "adapter_managed",
11479
- metadata: {
11480
- source: data.platform,
11481
- externalChannelId: data.externalChannelId
11482
- }
11543
+ metadata
11483
11544
  });
11484
11545
  const participants = data.botAgentId === data.senderAgentId ? [{
11485
11546
  chatId,
@@ -16403,7 +16464,7 @@ async function healthzRoutes(app) {
16403
16464
  * `api/orgs/invitations.ts` (Class B, admin-gated).
16404
16465
  */
16405
16466
  async function publicInvitationRoutes(app) {
16406
- const { previewInvitation } = await import("./invitation-C299fxkP-CZRV665C.mjs");
16467
+ const { previewInvitation } = await import("./invitation-C299fxkP-Dts66QTU.mjs");
16407
16468
  app.get("/:token/preview", async (request, reply) => {
16408
16469
  if (!request.params.token) throw new UnauthorizedError("Token required");
16409
16470
  const preview = await previewInvitation(app.db, request.params.token);
@@ -16583,7 +16644,7 @@ async function meRoutes(app) {
16583
16644
  */
16584
16645
  app.get("/me/pinned-agents", async (request) => {
16585
16646
  const { userId } = requireUser(request);
16586
- const { listMyPinnedAgents } = await import("./client-0RrgrMjR-CylTJGEb.mjs");
16647
+ const { listMyPinnedAgents } = await import("./client-DSM_opoz-BH5eegXb.mjs");
16587
16648
  return listMyPinnedAgents(app.db, { userId });
16588
16649
  });
16589
16650
  /**
@@ -17616,58 +17677,379 @@ async function sessionRoutes(app) {
17616
17677
  });
17617
17678
  });
17618
17679
  }
17619
- const log$1 = createLogger$1("GithubWebhook");
17620
- const GITHUB_ADAPTER_ID = "github-adapter";
17621
- function verifySignature(secret, rawBody, signatureHeader) {
17622
- const expected = `sha256=${createHmac("sha256", secret).update(rawBody).digest("hex")}`;
17623
- const expectedBuf = Buffer.from(expected, "utf8");
17624
- const receivedBuf = Buffer.from(signatureHeader, "utf8");
17625
- if (expectedBuf.length !== receivedBuf.length || !timingSafeEqual(expectedBuf, receivedBuf)) throw new UnauthorizedError("Invalid webhook signature");
17680
+ function isRecord(value) {
17681
+ return typeof value === "object" && value !== null && !Array.isArray(value);
17626
17682
  }
17627
- async function ensureGitHubAdapterAgent(db, organizationId) {
17628
- const [existing] = await db.select({ uuid: agents.uuid }).from(agents).where(and(eq(agents.organizationId, organizationId), eq(agents.name, GITHUB_ADAPTER_ID))).limit(1);
17629
- if (existing) return existing.uuid;
17630
- try {
17631
- return (await createAgent(db, {
17632
- name: GITHUB_ADAPTER_ID,
17633
- type: "autonomous_agent",
17634
- displayName: "GitHub Adapter",
17635
- organizationId,
17636
- metadata: {
17637
- source: "github",
17638
- managed: true
17639
- }
17640
- })).uuid;
17641
- } catch (err) {
17642
- if (err instanceof ConflictError) {
17643
- const [created] = await db.select({ uuid: agents.uuid }).from(agents).where(and(eq(agents.organizationId, organizationId), eq(agents.name, GITHUB_ADAPTER_ID))).limit(1);
17644
- if (created) return created.uuid;
17683
+ /** Pull `repository.full_name` ("owner/repo") from a webhook payload, or null. */
17684
+ function repoFullName(payload) {
17685
+ if (!isRecord(payload)) return null;
17686
+ const repo = isRecord(payload.repository) ? payload.repository : null;
17687
+ return typeof repo?.full_name === "string" && repo.full_name.length > 0 ? repo.full_name : null;
17688
+ }
17689
+ function readNumber(value) {
17690
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
17691
+ }
17692
+ function readString(value) {
17693
+ return typeof value === "string" && value.length > 0 ? value : null;
17694
+ }
17695
+ /**
17696
+ * Resolve the entity that a GitHub webhook event belongs to.
17697
+ *
17698
+ * Returns `null` when the event isn't a clustering candidate (event type
17699
+ * outside the §4.1 "core" list, malformed payload). Caller is expected to
17700
+ * skip such events.
17701
+ *
17702
+ * Notes
17703
+ * - `commit_comment` falls back to a `commit` entity keyed on `<repo>@<sha>`
17704
+ * when no associated PR is in the payload — the design hedges on "optionally
17705
+ * resolve to a PR", but doing so requires an extra GitHub API call which we
17706
+ * defer to Phase 1+.
17707
+ */
17708
+ function extractEventEntity(eventType, payload) {
17709
+ if (!isRecord(payload)) return null;
17710
+ const repo = repoFullName(payload);
17711
+ if (!repo) return null;
17712
+ switch (eventType) {
17713
+ case "issues":
17714
+ case "issue_comment": {
17715
+ const issue = isRecord(payload.issue) ? payload.issue : null;
17716
+ const number = readNumber(issue?.number);
17717
+ if (number === null) return null;
17718
+ return {
17719
+ type: "issue",
17720
+ key: `${repo}#${number}`,
17721
+ title: readString(issue?.title) ?? void 0,
17722
+ url: readString(issue?.html_url) ?? void 0
17723
+ };
17645
17724
  }
17646
- throw err;
17725
+ case "pull_request":
17726
+ case "pull_request_review":
17727
+ case "pull_request_review_comment": {
17728
+ const pr = isRecord(payload.pull_request) ? payload.pull_request : null;
17729
+ const number = readNumber(pr?.number);
17730
+ if (number === null) return null;
17731
+ return {
17732
+ type: "pull_request",
17733
+ key: `${repo}#${number}`,
17734
+ title: readString(pr?.title) ?? void 0,
17735
+ url: readString(pr?.html_url) ?? void 0
17736
+ };
17737
+ }
17738
+ case "discussion":
17739
+ case "discussion_comment": {
17740
+ const disc = isRecord(payload.discussion) ? payload.discussion : null;
17741
+ const number = readNumber(disc?.number);
17742
+ if (number === null) return null;
17743
+ return {
17744
+ type: "discussion",
17745
+ key: `${repo}#discussion-${number}`,
17746
+ title: readString(disc?.title) ?? void 0,
17747
+ url: readString(disc?.html_url) ?? void 0
17748
+ };
17749
+ }
17750
+ case "commit_comment": {
17751
+ const comment = isRecord(payload.comment) ? payload.comment : null;
17752
+ const sha = readString(comment?.commit_id);
17753
+ if (!sha) return null;
17754
+ return {
17755
+ type: "commit",
17756
+ key: `${repo}@${sha}`,
17757
+ url: readString(comment?.html_url) ?? void 0
17758
+ };
17759
+ }
17760
+ default: return null;
17647
17761
  }
17648
17762
  }
17649
- async function findTargetAgent(db, organizationId, repoFullName) {
17650
- const allAgents = await db.select({
17651
- id: agents.uuid,
17652
- name: agents.name,
17653
- metadata: agents.metadata,
17654
- type: agents.type
17655
- }).from(agents).where(and(eq(agents.organizationId, organizationId), eq(agents.status, "active")));
17656
- for (const agent of allAgents) {
17657
- if (agent.name === GITHUB_ADAPTER_ID) continue;
17658
- const meta = agent.metadata;
17659
- if (meta && typeof meta === "object" && "github" in meta) {
17660
- const github = meta.github;
17661
- if (isRecord(github) && "repos" in github) {
17662
- const repos = github.repos;
17663
- if (Array.isArray(repos) && repos.includes(repoFullName)) return agent.id;
17664
- }
17665
- }
17763
+ /**
17764
+ * Closing-keyword regex from
17765
+ * https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue
17766
+ * — `close[sd]? | fix(es|ed)? | resolve[sd]?`. Cross-repo `org/repo#N` is
17767
+ * deliberately excluded (out of scope for Phase 0; see §4.5).
17768
+ */
17769
+ const FIXES_KEYWORDS_RE = /\b(?:close[sd]?|fix(?:es|ed)?|resolve[sd]?)\s+#(\d+)\b/gi;
17770
+ /**
17771
+ * Parse `Fixes #N` / `Closes #N` / `Resolves #N` references out of a PR body.
17772
+ * Returns ordered, deduplicated entity references for issues in the same repo
17773
+ * (cross-repo refs ignored per §4.5).
17774
+ *
17775
+ * Caller is expected to pass `repoFullName` so we can build the entity key.
17776
+ */
17777
+ function parseFixesRefs(text, repoFullName) {
17778
+ if (!text) return [];
17779
+ const seen = /* @__PURE__ */ new Set();
17780
+ const out = [];
17781
+ for (const match of text.matchAll(FIXES_KEYWORDS_RE)) {
17782
+ const num = match[1];
17783
+ if (!num) continue;
17784
+ const key = `${repoFullName}#${num}`;
17785
+ if (seen.has(key)) continue;
17786
+ seen.add(key);
17787
+ out.push({
17788
+ type: "issue",
17789
+ key
17790
+ });
17666
17791
  }
17667
- return null;
17792
+ return out;
17668
17793
  }
17669
- function isRecord(value) {
17670
- return typeof value === "object" && value !== null && !Array.isArray(value);
17794
+ /**
17795
+ * Pick a chat-title prefix from (entity, eventType, action).
17796
+ *
17797
+ * PR review-flow events (`pull_request.review_requested`,
17798
+ * `pull_request_review.*`, `pull_request_review_comment.*`) collapse into a
17799
+ * single "PR Review" prefix so a chat first-touched by a review event is
17800
+ * visibly distinct from one first-touched by `pull_request.opened`. Everything
17801
+ * else just renders the entity type.
17802
+ *
17803
+ * Note: chat titles are written once at chat creation (see
17804
+ * `github-entity-chat.ts::createEntityChat`) — subsequent events for the same
17805
+ * entity reuse the existing title even if their (event, action) maps to a
17806
+ * different prefix. This matches the "entity is the container" semantic.
17807
+ */
17808
+ function entityTitlePrefix(entity, eventType, action) {
17809
+ if (eventType === "pull_request" && action === "review_requested") return "PR Review";
17810
+ if (eventType === "pull_request_review") return "PR Review";
17811
+ if (eventType === "pull_request_review_comment") return "PR Review";
17812
+ switch (entity.type) {
17813
+ case "issue": return "Issue";
17814
+ case "pull_request": return "PR";
17815
+ case "discussion": return "Discussion";
17816
+ case "commit": return "Commit";
17817
+ }
17818
+ }
17819
+ /**
17820
+ * Strip the leading `owner/` segment from an entity key so the chat title
17821
+ * stays compact. `owner/repo#42` → `repo#42`; `owner/repo@abc1234` →
17822
+ * `repo@abc1234`. The full `owner/repo#N` form is still used as the
17823
+ * clustering primary key (`github_entity_chat_mappings.entity_key`); only the
17824
+ * display string is shortened.
17825
+ */
17826
+ function shortEntityKey(key) {
17827
+ const slash = key.indexOf("/");
17828
+ return slash === -1 ? key : key.slice(slash + 1);
17829
+ }
17830
+ /**
17831
+ * Render a chat topic from an entity. Used as the chat title; kept short so
17832
+ * the chat-list row doesn't truncate aggressively.
17833
+ *
17834
+ * formatEntityTitle({ type: "pull_request", key: "owner/repo#307", title: "Improve overview" }, "pull_request", "opened")
17835
+ * → "PR repo#307: Improve overview"
17836
+ * formatEntityTitle(<same>, "pull_request", "review_requested")
17837
+ * → "PR Review repo#307: Improve overview"
17838
+ */
17839
+ function formatEntityTitle(entity, eventType, action) {
17840
+ const head = `${entityTitlePrefix(entity, eventType, action)} ${shortEntityKey(entity.key)}`;
17841
+ if (entity.title && entity.title.length > 0) return `${head}: ${entity.title}`;
17842
+ return head;
17843
+ }
17844
+ const SILENT_EVENT_TYPES = new Set([
17845
+ "workflow_run",
17846
+ "workflow_job",
17847
+ "check_run",
17848
+ "check_suite",
17849
+ "status",
17850
+ "push",
17851
+ "create",
17852
+ "delete",
17853
+ "fork",
17854
+ "watch",
17855
+ "release",
17856
+ "label",
17857
+ "label_created",
17858
+ "label_deleted",
17859
+ "reaction",
17860
+ "member",
17861
+ "membership",
17862
+ "team",
17863
+ "team_add",
17864
+ "organization",
17865
+ "org_block",
17866
+ "project",
17867
+ "project_card",
17868
+ "project_column"
17869
+ ]);
17870
+ /**
17871
+ * Per-event-type action-level filters. Frequent low-signal actions that would
17872
+ * otherwise spam an entity chat. `synchronize` (PR branch push) is the most
17873
+ * common offender — it fires on every commit push to a PR branch and never
17874
+ * carries new conversation.
17875
+ */
17876
+ const SILENT_ACTIONS = {
17877
+ issues: new Set([
17878
+ "labeled",
17879
+ "unlabeled",
17880
+ "milestoned",
17881
+ "demilestoned",
17882
+ "pinned",
17883
+ "unpinned"
17884
+ ]),
17885
+ pull_request: new Set([
17886
+ "labeled",
17887
+ "unlabeled",
17888
+ "auto_merge_enabled",
17889
+ "auto_merge_disabled",
17890
+ "synchronize"
17891
+ ])
17892
+ };
17893
+ /** True iff the event should be silently 200-OKed without further routing. */
17894
+ function shouldSilent(eventType, payload) {
17895
+ if (SILENT_EVENT_TYPES.has(eventType)) return true;
17896
+ if (!isRecord(payload)) return false;
17897
+ if (readString((isRecord(payload.sender) ? payload.sender : null)?.type) === "Bot") return true;
17898
+ const action = readString(payload.action);
17899
+ if (!action) return false;
17900
+ return SILENT_ACTIONS[eventType]?.has(action) ?? false;
17901
+ }
17902
+ /**
17903
+ * GitHub-specific webhook entity → chat clustering (Phase 0).
17904
+ *
17905
+ * Each `(organization, human_agent, delegate_agent, entity)` tuple resolves to
17906
+ * exactly one chat. Future external sources (Linear, Slack, …) get their own
17907
+ * tables — their entity models differ enough that a generic table would slip
17908
+ * back into untyped jsonb.
17909
+ *
17910
+ * `bound_via` distinguishes the first-touch row (`direct`) from a row written
17911
+ * by the `Fixes #N` linker (`fixes_link`). Routing logic ignores the
17912
+ * distinction; it exists for audit and future strategy tweaks.
17913
+ */
17914
+ const githubEntityChatMappings = pgTable("github_entity_chat_mappings", {
17915
+ organizationId: text("organization_id").notNull().references(() => organizations.id),
17916
+ humanAgentId: text("human_agent_id").notNull().references(() => agents.uuid, { onDelete: "cascade" }),
17917
+ delegateAgentId: text("delegate_agent_id").notNull().references(() => agents.uuid, { onDelete: "cascade" }),
17918
+ entityType: text("entity_type").notNull(),
17919
+ entityKey: text("entity_key").notNull(),
17920
+ chatId: text("chat_id").notNull().references(() => chats.id, { onDelete: "cascade" }),
17921
+ boundAt: timestamp("bound_at", { withTimezone: true }).notNull().defaultNow(),
17922
+ boundVia: text("bound_via").notNull()
17923
+ }, (table) => [primaryKey({ columns: [
17924
+ table.organizationId,
17925
+ table.humanAgentId,
17926
+ table.delegateAgentId,
17927
+ table.entityType,
17928
+ table.entityKey
17929
+ ] }), index("idx_github_entity_chat_mappings_chat").on(table.chatId)]);
17930
+ /**
17931
+ * Resolve which chat a GitHub event for (human, delegate, entity) belongs to.
17932
+ *
17933
+ * Three-step strategy from docs/webhook-routing-design.md §4.4:
17934
+ * a. Direct hit — entity already bound; reuse that chat.
17935
+ * b. Fixes-link — any related entity (parsed from `Fixes #N` in a PR body)
17936
+ * already bound; write a `fixes_link` row for this entity pointing at
17937
+ * the same chat, return it.
17938
+ * c. Miss — create a fresh chat via the canonical `createChat` entrypoint
17939
+ * and write a `direct` mapping row.
17940
+ *
17941
+ * Concurrent webhook deliveries for a never-before-seen entity race on (c);
17942
+ * the composite primary key + ON CONFLICT DO NOTHING ensures only one row
17943
+ * survives. The losing caller falls back to a re-read so the chat stays
17944
+ * unique.
17945
+ */
17946
+ async function resolveTargetChat(db, params) {
17947
+ const { organizationId, humanAgentId, delegateAgentId, entity, relatedEntities, eventType, action } = params;
17948
+ const direct = await lookupMapping(db, organizationId, humanAgentId, delegateAgentId, entity);
17949
+ if (direct) return {
17950
+ chatId: direct.chatId,
17951
+ created: false,
17952
+ boundVia: direct.boundVia
17953
+ };
17954
+ for (const ref of relatedEntities) {
17955
+ const linked = await lookupMapping(db, organizationId, humanAgentId, delegateAgentId, ref);
17956
+ if (!linked) continue;
17957
+ const inserted = await insertMappingIfAbsent(db, {
17958
+ organizationId,
17959
+ humanAgentId,
17960
+ delegateAgentId,
17961
+ entity,
17962
+ chatId: linked.chatId,
17963
+ boundVia: "fixes_link"
17964
+ });
17965
+ return {
17966
+ chatId: inserted.chatId,
17967
+ created: false,
17968
+ boundVia: inserted.boundVia
17969
+ };
17970
+ }
17971
+ const chat = await createEntityChat(db, humanAgentId, delegateAgentId, entity, eventType, action);
17972
+ const inserted = await insertMappingIfAbsent(db, {
17973
+ organizationId,
17974
+ humanAgentId,
17975
+ delegateAgentId,
17976
+ entity,
17977
+ chatId: chat.id,
17978
+ boundVia: "direct"
17979
+ });
17980
+ return {
17981
+ chatId: inserted.chatId,
17982
+ created: inserted.chatId === chat.id,
17983
+ boundVia: inserted.boundVia
17984
+ };
17985
+ }
17986
+ async function lookupMapping(db, organizationId, humanAgentId, delegateAgentId, entity) {
17987
+ const [row] = await db.select({
17988
+ chatId: githubEntityChatMappings.chatId,
17989
+ boundVia: githubEntityChatMappings.boundVia
17990
+ }).from(githubEntityChatMappings).where(and(eq(githubEntityChatMappings.organizationId, organizationId), eq(githubEntityChatMappings.humanAgentId, humanAgentId), eq(githubEntityChatMappings.delegateAgentId, delegateAgentId), eq(githubEntityChatMappings.entityType, entity.type), eq(githubEntityChatMappings.entityKey, entity.key))).limit(1);
17991
+ if (!row) return null;
17992
+ return {
17993
+ chatId: row.chatId,
17994
+ boundVia: row.boundVia === "fixes_link" ? "fixes_link" : "direct"
17995
+ };
17996
+ }
17997
+ async function insertMappingIfAbsent(db, params) {
17998
+ const [inserted] = await db.insert(githubEntityChatMappings).values({
17999
+ organizationId: params.organizationId,
18000
+ humanAgentId: params.humanAgentId,
18001
+ delegateAgentId: params.delegateAgentId,
18002
+ entityType: params.entity.type,
18003
+ entityKey: params.entity.key,
18004
+ chatId: params.chatId,
18005
+ boundVia: params.boundVia
18006
+ }).onConflictDoNothing({ target: [
18007
+ githubEntityChatMappings.organizationId,
18008
+ githubEntityChatMappings.humanAgentId,
18009
+ githubEntityChatMappings.delegateAgentId,
18010
+ githubEntityChatMappings.entityType,
18011
+ githubEntityChatMappings.entityKey
18012
+ ] }).returning({
18013
+ chatId: githubEntityChatMappings.chatId,
18014
+ boundVia: githubEntityChatMappings.boundVia
18015
+ });
18016
+ if (inserted) return {
18017
+ chatId: inserted.chatId,
18018
+ boundVia: inserted.boundVia === "fixes_link" ? "fixes_link" : "direct"
18019
+ };
18020
+ const winner = await lookupMapping(db, params.organizationId, params.humanAgentId, params.delegateAgentId, params.entity);
18021
+ if (!winner) throw new Error("Unexpected: mapping insert conflicted but row not visible on re-read");
18022
+ return winner;
18023
+ }
18024
+ /**
18025
+ * Create a fresh chat for a (human, delegate, entity) tuple. Goes through the
18026
+ * canonical `createChat` so:
18027
+ * - cross-org participants are rejected (BadRequestError)
18028
+ * - direct agent-only chats automatically get `mode=mention_only`
18029
+ * - watcher rows are recomputed
18030
+ * - a future addParticipant call would upgrade the chat to `group` via
18031
+ * `maybeUpgradeDirectToGroup` instead of raw INSERT shortcuts
18032
+ */
18033
+ async function createEntityChat(db, humanAgentId, delegateAgentId, entity, eventType, action) {
18034
+ const metadata = chatMetadataSchema$1.parse({
18035
+ source: "github",
18036
+ entityType: entity.type,
18037
+ entityKey: entity.key,
18038
+ ...entity.url ? { entityUrl: entity.url } : {}
18039
+ });
18040
+ return { id: (await createChat(db, humanAgentId, {
18041
+ type: "direct",
18042
+ participantIds: [delegateAgentId],
18043
+ topic: formatEntityTitle(entity, eventType, action),
18044
+ metadata
18045
+ })).id };
18046
+ }
18047
+ const log$1 = createLogger$1("GithubWebhook");
18048
+ function verifySignature(secret, rawBody, signatureHeader) {
18049
+ const expected = `sha256=${createHmac("sha256", secret).update(rawBody).digest("hex")}`;
18050
+ const expectedBuf = Buffer.from(expected, "utf8");
18051
+ const receivedBuf = Buffer.from(signatureHeader, "utf8");
18052
+ if (expectedBuf.length !== receivedBuf.length || !timingSafeEqual(expectedBuf, receivedBuf)) throw new UnauthorizedError("Invalid webhook signature");
17671
18053
  }
17672
18054
  /** Extract unique @mentions from text. Returns lowercase usernames.
17673
18055
  * Excludes email patterns (user@example.com) and team mentions (@org/team). */
@@ -17708,10 +18090,18 @@ function evaluateDelegateTarget(target, sourceOrgId) {
17708
18090
  }
17709
18091
  /**
17710
18092
  * Route @mentions to delegate agents.
17711
- * For each mentioned user who has delegate_mention configured,
17712
- * send a card message from the mentioned user to their delegate.
18093
+ *
18094
+ * For each mentioned GitHub user who maps to an agent with `delegate_mention`
18095
+ * configured, resolve which chat the event belongs to (via §4.4's
18096
+ * entity-clustering rules) and post a card from the human-bound agent to its
18097
+ * delegate.
18098
+ *
18099
+ * The entity argument is the §4.2 entity for the current event; `relatedRefs`
18100
+ * is the parsed `Fixes #N` list (empty for non-PR events). Both are
18101
+ * pre-computed by the caller so the heavy parsing doesn't run once per
18102
+ * mention.
17713
18103
  */
17714
- async function routeMentionDelegations(app, organizationId, mentionedNames, ctx) {
18104
+ async function routeMentionDelegations(app, organizationId, mentionedNames, ctx, entity, relatedRefs) {
17715
18105
  if (mentionedNames.length === 0) return 0;
17716
18106
  const delegates = await app.db.select({
17717
18107
  id: agents.uuid,
@@ -17740,8 +18130,24 @@ async function routeMentionDelegations(app, organizationId, mentionedNames, ctx)
17740
18130
  continue;
17741
18131
  }
17742
18132
  try {
17743
- const chat = await findOrCreateDirectChat(app.db, agent.id, agent.delegateMention);
17744
- const { message: msg, recipients } = await sendMessage(app.db, chat.id, agent.id, {
18133
+ const resolved = await resolveTargetChat(app.db, {
18134
+ organizationId,
18135
+ humanAgentId: agent.id,
18136
+ delegateAgentId: agent.delegateMention,
18137
+ entity,
18138
+ relatedEntities: relatedRefs,
18139
+ eventType: ctx.event,
18140
+ action: ctx.action ?? ""
18141
+ });
18142
+ log$1.info({
18143
+ chatId: resolved.chatId,
18144
+ entityType: entity.type,
18145
+ entityKey: entity.key,
18146
+ boundVia: resolved.boundVia,
18147
+ created: resolved.created,
18148
+ humanAgent: agent.name
18149
+ }, "resolved entity chat");
18150
+ const { message: msg, recipients } = await sendMessage(app.db, resolved.chatId, agent.id, {
17745
18151
  format: "card",
17746
18152
  content: {
17747
18153
  type: "github_mention",
@@ -17752,13 +18158,20 @@ async function routeMentionDelegations(app, organizationId, mentionedNames, ctx)
17752
18158
  sender: ctx.sender,
17753
18159
  title: ctx.title,
17754
18160
  body: ctx.body,
17755
- url: ctx.url
18161
+ url: ctx.url,
18162
+ entity: {
18163
+ type: entity.type,
18164
+ key: entity.key,
18165
+ url: entity.url ?? null
18166
+ }
17756
18167
  },
17757
18168
  metadata: {
17758
18169
  source: "github",
17759
18170
  event: "mention_delegation",
17760
18171
  mentionedUser: agent.name,
17761
- action: ctx.action
18172
+ action: ctx.action,
18173
+ entityType: entity.type,
18174
+ entityKey: entity.key
17762
18175
  }
17763
18176
  });
17764
18177
  notifyRecipients(app.notifier, recipients, msg.id);
@@ -17773,43 +18186,6 @@ async function routeMentionDelegations(app, organizationId, mentionedNames, ctx)
17773
18186
  }
17774
18187
  return routed;
17775
18188
  }
17776
- function parseIssuesPayload(body) {
17777
- if (!isRecord(body)) throw new BadRequestError("Invalid payload: expected object");
17778
- if (typeof body.action !== "string") throw new BadRequestError("Invalid payload: missing action");
17779
- if (!isRecord(body.issue)) throw new BadRequestError("Invalid payload: missing issue");
17780
- if (!isRecord(body.repository)) throw new BadRequestError("Invalid payload: missing repository");
17781
- if (!isRecord(body.sender)) throw new BadRequestError("Invalid payload: missing sender");
17782
- const issue = body.issue;
17783
- const labels = Array.isArray(issue.labels) ? issue.labels.filter((l) => isRecord(l) && typeof l.name === "string") : [];
17784
- return {
17785
- action: body.action,
17786
- issue: {
17787
- number: typeof issue.number === "number" ? issue.number : 0,
17788
- title: typeof issue.title === "string" ? issue.title : "",
17789
- body: typeof issue.body === "string" ? issue.body : null,
17790
- html_url: typeof issue.html_url === "string" ? issue.html_url : "",
17791
- labels,
17792
- state: typeof issue.state === "string" ? issue.state : "open"
17793
- },
17794
- repository: { full_name: typeof body.repository.full_name === "string" ? body.repository.full_name : "" },
17795
- sender: { login: typeof body.sender.login === "string" ? body.sender.login : "" }
17796
- };
17797
- }
17798
- function parseIssueCommentPayload(body) {
17799
- const base = parseIssuesPayload(body);
17800
- if (!isRecord(body)) throw new BadRequestError("Invalid payload: expected object");
17801
- if (!isRecord(body.comment)) throw new BadRequestError("Invalid payload: missing comment");
17802
- const comment = body.comment;
17803
- const commentUser = isRecord(comment.user) ? comment.user : { login: "" };
17804
- return {
17805
- ...base,
17806
- comment: {
17807
- body: typeof comment.body === "string" ? comment.body : "",
17808
- html_url: typeof comment.html_url === "string" ? comment.html_url : "",
17809
- user: { login: typeof commentUser.login === "string" ? commentUser.login : "" }
17810
- }
17811
- };
17812
- }
17813
18189
  async function githubWebhookRoutes(app) {
17814
18190
  app.addContentTypeParser("application/json", { parseAs: "buffer" }, (_request, body, done) => {
17815
18191
  done(null, body);
@@ -17839,6 +18215,11 @@ async function githubWebhookRoutes(app) {
17839
18215
  ok: true,
17840
18216
  event: "ping"
17841
18217
  });
18218
+ if (shouldSilent(eventType, payload)) return reply.status(200).send({
18219
+ ok: true,
18220
+ event: eventType,
18221
+ silent: true
18222
+ });
17842
18223
  const deliveryHeader = request.headers["x-github-delivery"];
17843
18224
  const deliveryId = typeof deliveryHeader === "string" && deliveryHeader.length > 0 ? deliveryHeader : null;
17844
18225
  if (deliveryId) {
@@ -17855,16 +18236,17 @@ async function githubWebhookRoutes(app) {
17855
18236
  }
17856
18237
  }
17857
18238
  try {
17858
- if (eventType === "issues") return await handleIssuesEvent(app, orgId, eventType, payload, reply);
17859
- if (eventType === "issue_comment") return await handleIssueCommentEvent(app, orgId, eventType, payload, reply);
17860
- let mentionsRouted = 0;
17861
- const allowedActions = MENTION_ACTIONS[eventType];
17862
18239
  const action = isRecord(payload) && typeof payload.action === "string" ? payload.action : void 0;
17863
- if (allowedActions && action && allowedActions.includes(action)) mentionsRouted = await handleMentionDelegation(app, orgId, eventType, payload);
18240
+ const allowedActions = MENTION_ACTIONS[eventType];
18241
+ if (!allowedActions || !action || !allowedActions.includes(action)) return reply.status(200).send({
18242
+ ok: true,
18243
+ event: eventType,
18244
+ handled: false
18245
+ });
18246
+ const mentionsRouted = await handleMentionDelegation(app, orgId, eventType, payload);
17864
18247
  return reply.status(200).send({
17865
18248
  ok: true,
17866
18249
  event: eventType,
17867
- handled: mentionsRouted > 0,
17868
18250
  mentionsRouted
17869
18251
  });
17870
18252
  } catch (err) {
@@ -18036,9 +18418,15 @@ async function handleMentionDelegation(app, organizationId, eventType, payload)
18036
18418
  const textMentions = extractMentions$1(extractEventText(eventType, payload));
18037
18419
  const structuralMentions = extractStructuralMentions(eventType, payload);
18038
18420
  const mentions = [...new Set([...textMentions, ...structuralMentions])];
18039
- const mentionCtx = extractEventContext(eventType, payload);
18040
- if (mentions.length > 0 && mentionCtx) return routeMentionDelegations(app, organizationId, mentions, mentionCtx);
18041
- return 0;
18421
+ if (mentions.length === 0) return 0;
18422
+ const ctx = extractEventContext(eventType, payload);
18423
+ if (!ctx) return 0;
18424
+ const entity = extractEventEntity(eventType, payload);
18425
+ if (!entity) {
18426
+ log$1.warn({ eventType }, "mention extracted but no entity resolvable; skipping fan-out");
18427
+ return 0;
18428
+ }
18429
+ return routeMentionDelegations(app, organizationId, mentions, ctx, entity, eventType === "pull_request" && ctx.repository.length > 0 ? parseFixesRefs(ctx.body, ctx.repository) : []);
18042
18430
  }
18043
18431
  /** Actions that represent new/changed content (worth scanning for @mentions).
18044
18432
  * Note: `pull_request.review_requested` doesn't carry an @mention in any
@@ -18059,124 +18447,6 @@ const MENTION_ACTIONS = {
18059
18447
  discussion_comment: ["created"],
18060
18448
  commit_comment: ["created"]
18061
18449
  };
18062
- async function handleIssuesEvent(app, organizationId, eventType, payload, reply) {
18063
- const data = parseIssuesPayload(payload);
18064
- if (MENTION_ACTIONS.issues?.includes(data.action)) await handleMentionDelegation(app, organizationId, eventType, payload);
18065
- if (![
18066
- "opened",
18067
- "edited",
18068
- "labeled"
18069
- ].includes(data.action)) return reply.status(200).send({
18070
- ok: true,
18071
- event: "issues",
18072
- action: data.action,
18073
- handled: false
18074
- });
18075
- const [senderId, targetAgentId] = await Promise.all([ensureGitHubAdapterAgent(app.db, organizationId), findTargetAgent(app.db, organizationId, data.repository.full_name)]);
18076
- if (!targetAgentId) {
18077
- log$1.warn({
18078
- repo: data.repository.full_name,
18079
- event: "issue"
18080
- }, "no target agent found for GitHub event");
18081
- return reply.status(200).send({
18082
- ok: true,
18083
- event: "issues",
18084
- action: data.action,
18085
- routed: false
18086
- });
18087
- }
18088
- const content = {
18089
- type: "github_issue",
18090
- action: data.action,
18091
- issue: {
18092
- number: data.issue.number,
18093
- title: data.issue.title,
18094
- body: data.issue.body,
18095
- url: data.issue.html_url,
18096
- labels: data.issue.labels.map((l) => l.name),
18097
- state: data.issue.state
18098
- },
18099
- repository: data.repository.full_name,
18100
- sender: data.sender.login
18101
- };
18102
- const metadata = {
18103
- source: "github",
18104
- event: "issues",
18105
- action: data.action
18106
- };
18107
- const chat = await findOrCreateDirectChat(app.db, senderId, targetAgentId);
18108
- const { message: msg, recipients } = await sendMessage(app.db, chat.id, senderId, {
18109
- format: "card",
18110
- content,
18111
- metadata
18112
- });
18113
- notifyRecipients(app.notifier, recipients, msg.id);
18114
- return reply.status(200).send({
18115
- ok: true,
18116
- event: "issues",
18117
- action: data.action,
18118
- routed: true
18119
- });
18120
- }
18121
- async function handleIssueCommentEvent(app, organizationId, eventType, payload, reply) {
18122
- const data = parseIssueCommentPayload(payload);
18123
- if (MENTION_ACTIONS.issue_comment?.includes(data.action)) await handleMentionDelegation(app, organizationId, eventType, payload);
18124
- if (data.action !== "created") return reply.status(200).send({
18125
- ok: true,
18126
- event: "issue_comment",
18127
- action: data.action,
18128
- handled: false
18129
- });
18130
- const [senderId, targetAgentId] = await Promise.all([ensureGitHubAdapterAgent(app.db, organizationId), findTargetAgent(app.db, organizationId, data.repository.full_name)]);
18131
- if (!targetAgentId) {
18132
- log$1.warn({
18133
- repo: data.repository.full_name,
18134
- event: "issue_comment"
18135
- }, "no target agent found for GitHub event");
18136
- return reply.status(200).send({
18137
- ok: true,
18138
- event: "issue_comment",
18139
- action: data.action,
18140
- routed: false
18141
- });
18142
- }
18143
- const content = {
18144
- type: "github_issue_comment",
18145
- action: data.action,
18146
- issue: {
18147
- number: data.issue.number,
18148
- title: data.issue.title,
18149
- url: data.issue.html_url,
18150
- labels: data.issue.labels.map((l) => l.name),
18151
- state: data.issue.state
18152
- },
18153
- comment: {
18154
- body: data.comment.body,
18155
- url: data.comment.html_url,
18156
- author: data.comment.user.login
18157
- },
18158
- repository: data.repository.full_name,
18159
- sender: data.sender.login
18160
- };
18161
- const metadata = {
18162
- source: "github",
18163
- event: "issue_comment",
18164
- action: data.action
18165
- };
18166
- const chat = await findOrCreateDirectChat(app.db, senderId, targetAgentId);
18167
- const { message: msg, recipients } = await sendMessage(app.db, chat.id, senderId, {
18168
- format: "card",
18169
- content,
18170
- metadata
18171
- });
18172
- notifyRecipients(app.notifier, recipients, msg.id);
18173
- return reply.status(200).send({
18174
- ok: true,
18175
- event: "issue_comment",
18176
- action: data.action,
18177
- routed: true
18178
- });
18179
- }
18180
18450
  var schema_exports = /* @__PURE__ */ __exportAll({
18181
18451
  adapterAgentMappings: () => adapterAgentMappings,
18182
18452
  adapterChatMappings: () => adapterChatMappings,
@@ -18191,6 +18461,7 @@ var schema_exports = /* @__PURE__ */ __exportAll({
18191
18461
  chatSubscriptions: () => chatSubscriptions,
18192
18462
  chats: () => chats,
18193
18463
  clients: () => clients,
18464
+ githubEntityChatMappings: () => githubEntityChatMappings,
18194
18465
  inboxEntries: () => inboxEntries,
18195
18466
  invitationRedemptions: () => invitationRedemptions,
18196
18467
  invitations: () => invitations,
@@ -20330,4 +20601,4 @@ function registerSaaSConnectCommand(program) {
20330
20601
  });
20331
20602
  }
20332
20603
  //#endregion
20333
- export { formatStaleReason as $, checkDocker as A, isServiceSupported as B, createApiNameResolver as C, checkBackgroundService as D, checkAgentConfigs as E, checkWebSocket as F, uninstallClientService as G, restartClientService as H, printResults as I, stopPostgres as J, ensurePostgres as K, reconcileAgentConfigs as L, checkServerConfig as M, checkServerHealth as N, checkClientConfig as O, checkServerReachable as P, findStaleAliases as Q, getClientServiceStatus as R, runHomeMigration as S, runMigrations as T, startClientService as U, resolveCliInvocation as V, stopClientService as W, handleClientOrgMismatch as X, ClientRuntime as Y, rotateClientIdWithBackup as Z, formatCheckReport as _, declineUpdate as a, success as at, onboardCreate as b, detectInstallMode as c, FirstTreeHubSDK as ct, startServer as d, cleanWorkspaces as dt, removeLocalAgent as et, reconcileLocalRuntimeProviders as f, probeCapabilities as ft, promptMissingFields as g, promptAddAgent as h, createExecuteUpdate as i, fail as it, checkNodeVersion as j, checkDatabase as k, fetchLatestVersion as l, SdkError as lt, isInteractive as m, configureClientLoggerForService as mt, deriveHubUrlFromToken as n, hasUser as nt, promptUpdate as o, ClientOrgMismatchError as ot, uploadClientCapabilities as p, applyClientLoggerConfig as pt, isDockerAvailable as q, registerSaaSConnectCommand as r, resolveReplyToFromEnv as rt, PACKAGE_NAME as s, ClientUserMismatchError as st, HubUrlDerivationError as t, createOwner as tt, installGlobalLatest as u, SessionRegistry as ut, loadOnboardState as v, migrateLocalAgentDirs as w, saveOnboardState as x, onboardCheck as y, installClientService as z };
20604
+ export { formatStaleReason as $, checkDocker as A, isServiceSupported as B, createApiNameResolver as C, checkBackgroundService as D, checkAgentConfigs as E, checkWebSocket as F, uninstallClientService as G, restartClientService as H, printResults as I, stopPostgres as J, ensurePostgres as K, reconcileAgentConfigs as L, checkServerConfig as M, checkServerHealth as N, checkClientConfig as O, checkServerReachable as P, findStaleAliases as Q, getClientServiceStatus as R, runHomeMigration as S, runMigrations as T, startClientService as U, resolveCliInvocation as V, stopClientService as W, handleClientOrgMismatch as X, ClientRuntime as Y, rotateClientIdWithBackup as Z, formatCheckReport as _, declineUpdate as a, fail as at, onboardCreate as b, detectInstallMode as c, ClientUserMismatchError as ct, startServer as d, SessionRegistry as dt, removeLocalAgent as et, reconcileLocalRuntimeProviders as f, cleanWorkspaces as ft, promptMissingFields as g, promptAddAgent as h, configureClientLoggerForService as ht, createExecuteUpdate as i, resolveSenderName as it, checkNodeVersion as j, checkDatabase as k, fetchLatestVersion as l, FirstTreeHubSDK as lt, isInteractive as m, applyClientLoggerConfig as mt, deriveHubUrlFromToken as n, hasUser as nt, promptUpdate as o, success as ot, uploadClientCapabilities as p, probeCapabilities as pt, isDockerAvailable as q, registerSaaSConnectCommand as r, resolveReplyToFromEnv as rt, PACKAGE_NAME as s, ClientOrgMismatchError as st, HubUrlDerivationError as t, createOwner as tt, installGlobalLatest as u, SdkError as ut, loadOnboardState as v, migrateLocalAgentDirs as w, saveOnboardState as x, onboardCheck as y, installClientService as z };