@agent-team-foundation/first-tree-hub 0.14.2 → 0.14.4

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-C15ZBOCC.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-CQQGgIx1.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 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, Q as joinByInvitationSchema, R as getMeDocResponseSchema, T as contextTreeSnapshotSchema, Tt as updateAgentRuntimeConfigSchema, U as githubDevCallbackQuerySchema, V as githubAppInstallationPermissionsSchema$1, W as githubStartQuerySchema, X as isRedactedEnvValue, Y as isOrgSettingNamespace, _ as agentBindRequestSchema, _t as sendToAgentSchema, at as paginationQuerySchema, b as agentTypeSchema$1, bt as sessionReconcileRequestSchema, 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, m as WS_AUTH_FRAME_TIMEOUT_MS, 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-DmYxT5Kb.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, Q as joinByInvitationSchema, R as getMeDocResponseSchema, T as contextTreeSnapshotSchema, Tt as updateAgentRuntimeConfigSchema, U as githubDevCallbackQuerySchema, V as githubAppInstallationPermissionsSchema$1, W as githubStartQuerySchema, X as isRedactedEnvValue, Y as isOrgSettingNamespace, _ as agentBindRequestSchema, _t as sendToAgentSchema, at as paginationQuerySchema, b as agentTypeSchema$1, bt as sessionReconcileRequestSchema, 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, m as WS_AUTH_FRAME_TIMEOUT_MS, 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-CrdnqZjv.mjs";
6
6
  import { a as ClientUserMismatchError$1, c as NotFoundError, d as users, f as uuidv7, i as ClientOrgMismatchError$1, l as UnauthorizedError, n as AppError, o as ConflictError, r as BadRequestError, s as ForbiddenError, u as organizations } from "./uuid-DbS_4vFh-iFghv4zA.mjs";
7
7
  import { n as init_esm, r as trace, t as esm_exports } from "./esm-iadMkGbV.mjs";
8
- import { $ as getSession, $t as suspendSession, A as createAgent, At as pollInbox, B as extractSummary, Bt as resetTimedOutEntries, C as claimAndBuildForPush, Ct as markMeChatRead, D as cleanupStalePresence, Dt as messages, E as cleanupStaleClients, Et as members, F as deriveAuthState, Ft as registerChatMessageDispatcher, G as getAgentAvatarImage, Gt as sendToAgent$1, H as findOrCreateDirectChat, Ht as resolveDefaultOrgId$1, I as disconnectClient, It as registerClient, J as getChatDetail, Jt as setChatEngagement, K as getCachedAudience, Kt as serverInstances, L as editMessage, Lt as removeParticipant, M as createMeChat, Mt as reactivateAgent, N as createNotifier, Nt as rebindAgent, O as clearAgentAvatarImage, Ot as notifyRecipients, P as deleteAgent, Pt as recomputeWatchersForMember, Q as getPresence, Qt as suspendAgent, R as ensureDefaultOrganization, Rt as renewEntry, S as checkAgentNameAvailability, T as claimClient, Tt as markStaleAgents, U as getActivityOverview, Ut as retireClient, V as filterSessionsByParticipant, Vt as resolveChatTitle, W as getAgent, Wt as sendMessage, X as getOnlineCount, Xt as setRuntimeState, Y as getClient, Yt as setOffline, Z as getOrganization, Zt as submitAnswer, _ as assertParticipant, _t as listClients, a as adapterAgentMappings, an as upsertSessionState, at as leaveChat, b as chatUserState, bt as listMeChats, c as addMeChatParticipants, ct as listAgentSessions, d as agentChatSessions, dt as listAgentsManagedByUser, en as touchAgent, et as heartbeatClient, f as agentConfigs, ft as listAgentsWithRuntime, g as assertClientOwner, gt as listChatsForMember, h as archiveSession, ht as listChats, i as ackEntryByIdForBoundAgents, in as updateOrganization, it as joinMeChat, j as createChat, jt as pruneStaleSilentEntries, k as clients, kt as pendingQuestions, l as addParticipant, lt as listAgentsForAdmin, m as agents, mt as listChatParticipantsWithNames, n as SUPPORTED_AVATAR_IMAGE_MIMES, nn as updateAgent, nt as inboxEntries, o as adapterConfigs, ot as leaveMeChat, p as agentPresence, pt as listAllSessions, q as getCallerEngagement, qt as setAgentAvatarImage, r as ackEntry$2, rn as updateClientCapabilities, rt as joinChat, s as addChatParticipants, st as listActiveAgentsPinnedToClient, t as MAX_AVATAR_IMAGE_BYTES, tn as unbindAgent, tt as heartbeatInstance, u as agentAvatarImageUrl, ut as listAgentsForMember, v as bindAgent, vt as listClientsForOrgAdmin, w as claimBacklogForPush, wt as markMeChatUnread, x as chats, xt as listMessages, y as chatMembership, yt as listMeChatSourceCounts, z as ensureParticipant, zt as resetActivity } from "./client-CZ_VnbEc-CBF46cJd.mjs";
8
+ import { $ as getSession, $t as suspendSession, A as createChat, At as pollInbox, B as fetchUserAvatarForHumanAgent, Bt as resolveAvatarImageUrl, C as claimBacklogForPush, Ct as markMeChatRead, D as clearAgentAvatarImage, Dt as messages, E as cleanupStalePresence, Et as members, F as disconnectClient, Ft as registerChatMessageDispatcher, G as getAgentAvatarImage, Gt as sendToAgent$1, H as findOrCreateDirectChat, Ht as resolveDefaultOrgId$1, I as editMessage, It as registerClient, J as getChatDetail, Jt as setChatEngagement, K as getCachedAudience, Kt as serverInstances, L as ensureDefaultOrganization, Lt as removeParticipant, M as createNotifier, Mt as reactivateAgent, N as deleteAgent, Nt as rebindAgent, O as clients, Ot as notifyRecipients, P as deriveAuthState, Pt as recomputeWatchersForMember, Q as getPresence, Qt as suspendAgent, R as ensureParticipant, Rt as resetActivity, S as claimAndBuildForPush, T as cleanupStaleClients, Tt as markStaleAgents, U as getActivityOverview, Ut as retireClient, V as filterSessionsByParticipant, Vt as resolveChatTitle, W as getAgent, Wt as sendMessage, X as getOnlineCount, Xt as setRuntimeState, Y as getClient, Yt as setOffline, Z as getOrganization, Zt as submitAnswer, _ as bindAgent, _t as listClients, a as adapterConfigs, an as upsertSessionState, at as leaveChat, b as chats, bt as listMeChats, c as addParticipant, ct as listAgentSessions, d as agentConfigs, dt as listAgentsManagedByUser, en as touchAgent, et as heartbeatClient, f as agentPresence, ft as listAgentsWithRuntime, g as assertParticipant, gt as listChatsForMember, h as assertClientOwner, ht as listChats, i as adapterAgentMappings, in as updateOrganization, it as joinMeChat, j as createMeChat, jt as pruneStaleSilentEntries, k as createAgent, kt as pendingQuestions, l as agentAvatarImageUrl, lt as listAgentsForAdmin, m as archiveSession, mt as listChatParticipantsWithNames, n as SUPPORTED_AVATAR_IMAGE_MIMES, nn as updateAgent, nt as inboxEntries, o as addChatParticipants, ot as leaveMeChat, p as agents, pt as listAllSessions, q as getCallerEngagement, qt as setAgentAvatarImage, r as ackEntryByIdForBoundAgents, rn as updateClientCapabilities, rt as joinChat, s as addMeChatParticipants, st as listActiveAgentsPinnedToClient, t as MAX_AVATAR_IMAGE_BYTES, tn as unbindAgent, tt as heartbeatInstance, u as agentChatSessions, ut as listAgentsForMember, v as chatMembership, vt as listClientsForOrgAdmin, w as claimClient, wt as markMeChatUnread, x as checkAgentNameAvailability, xt as listMessages, y as chatUserState, yt as listMeChatSourceCounts, z as extractSummary, zt as resetTimedOutEntries } from "./client-BPRIfrOT-CoV_2o7e.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 } from "./invitation-D_ENPHyj-5ETiae5r.mjs";
10
10
  import { createRequire } from "node:module";
11
11
  import { ZodError, z } from "zod";
@@ -850,7 +850,8 @@ z.object({
850
850
  participants: z.array(chatParticipantDetailSchema),
851
851
  title: z.string(),
852
852
  firstMessagePreview: z.string().nullable(),
853
- engagementStatus: chatEngagementStatusSchema
853
+ engagementStatus: chatEngagementStatusSchema,
854
+ viewerMembershipKind: z.enum(["participant", "watching"]).nullable()
854
855
  });
855
856
  z.object({ topic: z.string().trim().max(500).nullable() });
856
857
  z.object({ agentId: z.string().min(1) });
@@ -887,9 +888,13 @@ z.object({
887
888
  * Optional opt-in flags the client carries on `client:register` to advertise
888
889
  * which negotiable wire-protocol features it implements. Distinct from
889
890
  * `clientCapabilitiesSchema` (per-runtime-provider availability — different
890
- * concept). Older clients omit the field; the server treats every unset flag
891
- * as `false` and falls back to the legacy path. See proposal
892
- * hub-inbox-ws-data-plane §3.6.
891
+ * concept).
892
+ *
893
+ * 0.10.4 ~ 0.14.2 clients still send this block (with `wsInboxDeliver: true`
894
+ * hard-coded). The 0.14.3+ runtime omits it. The schema is retained so that
895
+ * middle-version `client:register` frames still parse, even though the
896
+ * server no longer reads any of these fields — the WS inbox data plane is
897
+ * mandatory on this server build.
893
898
  */
894
899
  const clientWireCapabilitiesSchema = z.object({ wsInboxDeliver: z.boolean().default(false) }).partial();
895
900
  z.object({
@@ -1114,8 +1119,8 @@ z.object({
1114
1119
  });
1115
1120
  /**
1116
1121
  * Server → client WS frame carrying the full image bytes for an image
1117
- * message. Pushed before the corresponding `new_message` notification so
1118
- * the client has the file on disk by the time it polls the message.
1122
+ * message. Pushed before the corresponding `inbox:deliver` frame so the
1123
+ * client has the file on disk by the time it renders the message.
1119
1124
  *
1120
1125
  * Best-effort: if the target client WS lives on a different server
1121
1126
  * instance (or is offline), the frame is lost and the reference message
@@ -1276,14 +1281,11 @@ z.object({
1276
1281
  }).extend({ message: clientMessageSchema });
1277
1282
  z.object({ limit: z.coerce.number().int().min(1).max(50).default(10) });
1278
1283
  /**
1279
- * server → client: a single inbox entry pushed over the active WS connection,
1280
- * replacing the legacy `new_message` doorbell + HTTP `/inbox` poll round-trip.
1284
+ * server → client: a single inbox entry pushed over the active WS connection.
1281
1285
  *
1282
1286
  * `entryId` is the server-side `inbox_entries.id` the client must echo back
1283
- * in `inbox:ack`. `message` is exactly what the legacy poll path returned —
1284
- * `clientMessageSchema` already carries `precedingMessages`, so the client-
1285
- * side dispatch logic is reused verbatim (see proposal
1286
- * hub-inbox-ws-data-plane §3.1).
1287
+ * in `inbox:ack`. `clientMessageSchema` carries `precedingMessages`, so the
1288
+ * client-side dispatch logic handles the silent-context bundle uniformly.
1287
1289
  *
1288
1290
  * `.passthrough()` so a forward-rolling server may extend the frame without
1289
1291
  * breaking older clients that validate strictly. Older clients drop unknown
@@ -2015,8 +2017,13 @@ z.object({
2015
2017
  /**
2016
2018
  * Negotiable wire-protocol features the server advertises in its `welcome`
2017
2019
  * frame. Older clients drop the `capabilities` field silently because the
2018
- * frame is `.passthrough()`. New clients gate optional code paths on it —
2019
- * absent ⇒ feature off, never assumed.
2020
+ * frame is `.passthrough()`.
2021
+ *
2022
+ * Required by clients in the 0.10.4 ~ 0.14.2 range: those builds read
2023
+ * `wsInboxDeliver` here to decide whether to skip the local HTTP poll loop
2024
+ * and rely on `inbox:deliver` push frames. The 0.14.3+ runtime ignores the
2025
+ * field (push is the only path) but the server still emits it so middle-
2026
+ * version clients keep working.
2020
2027
  */
2021
2028
  const serverCapabilitiesSchema = z.object({ wsInboxDeliver: z.boolean().default(false) }).partial();
2022
2029
  /**
@@ -2145,13 +2152,6 @@ defineConfig({
2145
2152
  refreshTokenExpiry: field(z.string().default("30d"), { env: "FIRST_TREE_HUB_AUTH_REFRESH_TOKEN_EXPIRY" }),
2146
2153
  connectTokenExpiry: field(z.string().default("10m"), { env: "FIRST_TREE_HUB_AUTH_CONNECT_TOKEN_EXPIRY" })
2147
2154
  },
2148
- contextTreeSync: optional({
2149
- githubToken: field(z.string(), {
2150
- env: "FIRST_TREE_HUB_CONTEXT_TREE_GITHUB_TOKEN",
2151
- secret: true
2152
- }),
2153
- githubTokenRepos: field(z.string().optional(), { env: "FIRST_TREE_HUB_CONTEXT_TREE_GITHUB_TOKEN_REPOS" })
2154
- }),
2155
2155
  oauth: optional({ githubApp: optional({
2156
2156
  appId: field(z.string().min(1), { env: "FIRST_TREE_HUB_GITHUB_APP_ID" }),
2157
2157
  clientId: field(z.string().min(1), { env: "FIRST_TREE_HUB_GITHUB_APP_CLIENT_ID" }),
@@ -2407,15 +2407,6 @@ var FirstTreeHubSDK = class {
2407
2407
  return false;
2408
2408
  }
2409
2409
  }
2410
- async pull(limit = 10) {
2411
- return { entries: await this.requestJson(`/api/v1/agent/inbox?limit=${limit}`) };
2412
- }
2413
- async ack(entryId) {
2414
- await this.requestVoid(`/api/v1/agent/inbox/${entryId}/ack`, { method: "POST" });
2415
- }
2416
- async renew(entryId) {
2417
- await this.requestVoid(`/api/v1/agent/inbox/${entryId}/renew`, { method: "POST" });
2418
- }
2419
2410
  async sendMessage(chatId, data) {
2420
2411
  return this.requestJson(`/api/v1/agent/chats/${chatId}/messages`, {
2421
2412
  method: "POST",
@@ -2583,17 +2574,6 @@ const RECONNECT_MAX_MS = 3e4;
2583
2574
  const WS_CONNECT_TIMEOUT_MS = 1e4;
2584
2575
  const HEARTBEAT_INTERVAL_MS = 3e4;
2585
2576
  /**
2586
- * Client-side opt-in for the WS inbox data plane. Gates BOTH the
2587
- * `wireCapabilities.wsInboxDeliver` flag we declare on `client:register`
2588
- * AND how we interpret the server's welcome capability — without this AND,
2589
- * a future client kill-switch could land in a half-state where we tell the
2590
- * server "no thanks" but still treat welcome's `wsInboxDeliver:true` as
2591
- * authoritative and stop the 5s HTTP poll, leaving messages stuck if a
2592
- * NOTIFY ever drops. Hard-coded `true` for now; flip to a config knob if
2593
- * you need a runtime kill-switch.
2594
- */
2595
- const WS_INBOX_DELIVER_OPT_IN = true;
2596
- /**
2597
2577
  * Unified-user-token C5: reconnect PROACTIVELY this many ms before the JWT's
2598
2578
  * `exp` claim so the client rotates to a fresh JWT without ever hitting the
2599
2579
  * server-side `auth:expired` push. The provider's next `getAccessToken()` call
@@ -2653,15 +2633,6 @@ var ClientConnection = class extends EventEmitter {
2653
2633
  /** Count of `server:welcome` frames received; drives `isReconnect` flag. */
2654
2634
  welcomeFramesReceived = 0;
2655
2635
  /**
2656
- * Whether the most recent `server:welcome` frame advertised
2657
- * `capabilities.wsInboxDeliver`. The runtime (AgentSlot) reads this
2658
- * (via {@link supportsWsInboxDeliver}) to decide whether to keep the
2659
- * legacy 5s HTTP poll or rely entirely on `inbox:deliver` push frames.
2660
- * Re-evaluated on every reconnect — the welcome frame is the source of
2661
- * truth, never assumed sticky across connections.
2662
- */
2663
- wsInboxDeliverActive = false;
2664
- /**
2665
2636
  * Last handshake error, stashed for the `close` handler to surface a typed
2666
2637
  * reason (e.g. {@link ClientOrgMismatchError}) instead of a generic
2667
2638
  * "closed before ready" when `connect()` is pending.
@@ -2674,11 +2645,11 @@ var ClientConnection = class extends EventEmitter {
2674
2645
  desiredBindings = /* @__PURE__ */ new Map();
2675
2646
  pendingBinds = /* @__PURE__ */ new Map();
2676
2647
  /**
2677
- * In-flight image writes from recent `image_payload` frames. The server
2678
- * pushes `image_payload` immediately before firing the `new_message`
2679
- * notification, but WS message handlers run through EventEmitter (sync
2680
- * dispatch, no await), so the disk write can still race the HTTP poll
2681
- * that follows. Defer `new_message` emission until these settle.
2648
+ * In-flight image writes from recent `image_payload` frames. `image_payload`
2649
+ * arrives on the WS just before `inbox:deliver` for the same message, but
2650
+ * the EventEmitter dispatch is sync so without gating, the deliver
2651
+ * handler can fire before the image bytes hit disk. Block `inbox:deliver`
2652
+ * emission until these settle.
2682
2653
  */
2683
2654
  pendingImageWrites = /* @__PURE__ */ new Set();
2684
2655
  constructor(config) {
@@ -2699,24 +2670,21 @@ var ClientConnection = class extends EventEmitter {
2699
2670
  return this.boundAgents;
2700
2671
  }
2701
2672
  /**
2702
- * True when the current connection's `server:welcome` advertised
2703
- * `capabilities.wsInboxDeliver`meaning the server will push
2704
- * `inbox:deliver` frames and accept `inbox:ack` frames over this WS.
2705
- * Resets to false on every reconnect until the new welcome arrives.
2706
- */
2707
- get supportsWsInboxDeliver() {
2708
- return this.wsInboxDeliverActive;
2709
- }
2710
- /**
2711
- * Ack a delivered inbox entry over the WS data plane. Replaces the legacy
2712
- * `sdk.ack()` HTTP call when the connection has negotiated
2713
- * `wsInboxDeliver`. Safe to call when the WS is closed — the frame is
2714
- * dropped silently and the entry will time out and re-deliver on
2715
- * reconnect, mirroring how the legacy timeout reaper handles HTTP
2716
- * ack-loss.
2673
+ * Ack a delivered inbox entry over the WS data plane. Safe to call when the
2674
+ * WS is closed the frame is dropped (logged) and the entry will time out
2675
+ * server-side and re-deliver on reconnect. The handler has by then already
2676
+ * started processing, so reaper-driven redelivery surfaces as a duplicate
2677
+ * dispatch on the next connect; SessionManager's dedupe key
2678
+ * `(chatId, messageId)` collapses it.
2717
2679
  */
2718
2680
  sendInboxAck(entryId) {
2719
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
2681
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
2682
+ this.wsLogger.warn({
2683
+ entryId,
2684
+ readyState: this.ws?.readyState
2685
+ }, "inbox:ack dropped — socket not OPEN");
2686
+ return;
2687
+ }
2720
2688
  this.ws.send(JSON.stringify({
2721
2689
  type: "inbox:ack",
2722
2690
  entryId
@@ -2873,7 +2841,6 @@ var ClientConnection = class extends EventEmitter {
2873
2841
  this.clearAuthRefreshTimer();
2874
2842
  const wasRegistered = this.registered;
2875
2843
  this.registered = false;
2876
- this.wsInboxDeliverActive = false;
2877
2844
  this.rejectAllPendingBinds("WebSocket closed");
2878
2845
  if (!settled) {
2879
2846
  this.wsLogger.warn({ code }, "closed before ready");
@@ -2909,8 +2876,7 @@ var ClientConnection = class extends EventEmitter {
2909
2876
  clientId: this.clientId,
2910
2877
  hostname: hostname(),
2911
2878
  os: platform(),
2912
- sdkVersion: this.sdkVersion,
2913
- wireCapabilities: { wsInboxDeliver: WS_INBOX_DELIVER_OPT_IN }
2879
+ sdkVersion: this.sdkVersion
2914
2880
  }));
2915
2881
  return;
2916
2882
  }
@@ -2920,7 +2886,6 @@ var ClientConnection = class extends EventEmitter {
2920
2886
  this.wsLogger.warn({ issues: parsed.error.issues.map((i) => i.message) }, "ignoring malformed server:welcome frame");
2921
2887
  return;
2922
2888
  }
2923
- this.wsInboxDeliverActive = parsed.data.capabilities?.wsInboxDeliver === true && WS_INBOX_DELIVER_OPT_IN;
2924
2889
  const isReconnect = this.welcomeFramesReceived > 0;
2925
2890
  this.welcomeFramesReceived++;
2926
2891
  this.emit("server:welcome", {
@@ -3058,15 +3023,6 @@ var ClientConnection = class extends EventEmitter {
3058
3023
  write.finally(() => this.pendingImageWrites.delete(write));
3059
3024
  return;
3060
3025
  }
3061
- if (type === "new_message") {
3062
- const inboxId = msg.inboxId;
3063
- if (!inboxId) return;
3064
- if (this.pendingImageWrites.size > 0) Promise.all([...this.pendingImageWrites]).finally(() => {
3065
- this.emit("agent:message", inboxId, msg);
3066
- });
3067
- else this.emit("agent:message", inboxId, msg);
3068
- return;
3069
- }
3070
3026
  if (type === "inbox:deliver") {
3071
3027
  const parsed = inboxDeliverFrameSchema.safeParse(msg);
3072
3028
  if (!parsed.success) {
@@ -6812,18 +6768,13 @@ var SessionManager = class {
6812
6768
  this.config.onStateChange(chatId, state);
6813
6769
  }
6814
6770
  /**
6815
- * ACK an inbox entry — delayed until handler starts processing.
6816
- *
6817
- * Routes through `config.ackEntry` when set (WS push path) or falls back to
6818
- * `sdk.ack` (HTTP poll path). One ack per entry, one channel per slot —
6819
- * mixing channels in one slot would leak the server's per-agent in-flight
6820
- * counter (proposal hub-inbox-ws-data-plane §3.5).
6771
+ * ACK an inbox entry — delayed until handler starts processing. Routes
6772
+ * through `config.ackEntry`, which is wired to the WS data plane.
6821
6773
  */
6822
6774
  async ackEntry(entryId, chatId) {
6823
6775
  if (entryId === void 0) return;
6824
6776
  try {
6825
- if (this.config.ackEntry) await this.config.ackEntry(entryId);
6826
- else await this.config.sdk.ack(entryId);
6777
+ await this.config.ackEntry(entryId);
6827
6778
  } catch {
6828
6779
  this.config.log.warn({
6829
6780
  chatId,
@@ -6976,9 +6927,7 @@ var AgentSlot = class {
6976
6927
  sessionManager = null;
6977
6928
  config;
6978
6929
  logger;
6979
- sdk = null;
6980
6930
  agentConfigCache = null;
6981
- pollingTimer = null;
6982
6931
  reconcileTimer = null;
6983
6932
  listeners = [];
6984
6933
  /**
@@ -7016,7 +6965,6 @@ var AgentSlot = class {
7016
6965
  }
7017
6966
  async start(contextTreeBinding) {
7018
6967
  const sdk = (await this.clientConnection.bindAgent(this.config.agentId, this.config.runtimeType ?? this.config.type, this.config.runtimeVersion)).sdk;
7019
- this.sdk = sdk;
7020
6968
  const agent = await sdk.register();
7021
6969
  this.logger.info({ displayName: agent.displayName }, "agent bound");
7022
6970
  if (agent.type === "human") {
@@ -7036,9 +6984,6 @@ var AgentSlot = class {
7036
6984
  throw new Error(`Hub unreachable while loading agent config: ${msg}`);
7037
6985
  }
7038
6986
  this.inboxId = agent.inboxId;
7039
- const onMessage = (agentId) => {
7040
- if (agentId === this.config.agentId) this.pullAndDispatch();
7041
- };
7042
6987
  const onInboxDeliver = (inboxId, frame) => {
7043
6988
  if (inboxId !== this.inboxId) return;
7044
6989
  this.dispatchPushedFrame(frame).catch((err) => {
@@ -7057,14 +7002,10 @@ var AgentSlot = class {
7057
7002
  const onReconcileResult = (result) => {
7058
7003
  if (result.agentId === this.config.agentId && this.sessionManager) this.sessionManager.applyStaleChatIds(result.staleChatIds);
7059
7004
  };
7060
- this.clientConnection.on("agent:message", onMessage);
7061
7005
  this.clientConnection.on("inbox:deliver", onInboxDeliver);
7062
7006
  this.clientConnection.on("agent:bound", onBound);
7063
7007
  this.clientConnection.on("session:reconcile:result", onReconcileResult);
7064
7008
  this.listeners.push({
7065
- event: "agent:message",
7066
- fn: onMessage
7067
- }, {
7068
7009
  event: "inbox:deliver",
7069
7010
  fn: onInboxDeliver
7070
7011
  }, {
@@ -7082,10 +7023,10 @@ var AgentSlot = class {
7082
7023
  agentId: this.config.agentId
7083
7024
  })
7084
7025
  });
7085
- const ackEntry = this.clientConnection.supportsWsInboxDeliver ? (entryId) => {
7026
+ const ackEntry = (entryId) => {
7086
7027
  this.clientConnection.sendInboxAck(entryId);
7087
7028
  return Promise.resolve();
7088
- } : void 0;
7029
+ };
7089
7030
  this.sessionManager = new SessionManager({
7090
7031
  session: this.config.session,
7091
7032
  concurrency: this.config.concurrency,
@@ -7128,24 +7069,15 @@ var AgentSlot = class {
7128
7069
  event: "session:command",
7129
7070
  fn: onCommand
7130
7071
  });
7131
- this.startPolling();
7132
7072
  this.startReconcileLoop();
7133
7073
  return agent;
7134
7074
  }
7135
7075
  async stop() {
7136
- if (this.pollingTimer) {
7137
- clearInterval(this.pollingTimer);
7138
- this.pollingTimer = null;
7139
- }
7140
7076
  if (this.reconcileTimer) {
7141
7077
  clearInterval(this.reconcileTimer);
7142
7078
  this.reconcileTimer = null;
7143
7079
  }
7144
- for (const entry of this.listeners) if (entry.event === "agent:message") this.clientConnection.off(entry.event, entry.fn);
7145
- else if (entry.event === "inbox:deliver") this.clientConnection.off(entry.event, entry.fn);
7146
- else if (entry.event === "agent:bound") this.clientConnection.off(entry.event, entry.fn);
7147
- else if (entry.event === "session:reconcile:result") this.clientConnection.off(entry.event, entry.fn);
7148
- else this.clientConnection.off(entry.event, entry.fn);
7080
+ for (const entry of this.listeners) this.clientConnection.off(entry.event, entry.fn);
7149
7081
  this.listeners = [];
7150
7082
  await this.clientConnection.unbindAgent(this.config.agentId);
7151
7083
  await this.sessionManager?.shutdown();
@@ -7166,31 +7098,18 @@ var AgentSlot = class {
7166
7098
  const runtimeState = this.sessionManager.getAggregateRuntimeState();
7167
7099
  if (runtimeState) this.clientConnection.reportRuntimeState(this.config.agentId, runtimeState);
7168
7100
  }
7169
- startPolling() {
7170
- if (this.clientConnection.supportsWsInboxDeliver) {
7171
- this.logger.info("WS inbox data plane active — skipping 5s HTTP poll");
7172
- return;
7173
- }
7174
- this.pollingTimer = setInterval(() => {
7175
- this.pullAndDispatch();
7176
- }, 5e3);
7177
- this.pullAndDispatch();
7178
- }
7179
7101
  /**
7180
7102
  * Translate an `inbox:deliver` push frame into the {@link InboxEntryWithMessage}
7181
7103
  * shape `SessionManager.dispatch` expects, then dispatch.
7182
7104
  *
7183
7105
  * Ack happens INSIDE `dispatch` via the `ackEntry` callback we pinned at
7184
- * construction time — for push slots that's `clientConnection.sendInboxAck`,
7185
- * for poll slots it stays the legacy `sdk.ack`. Sending an additional ack
7186
- * here would double-ack: HTTP first (`delivered acked`) followed by a
7187
- * WS frame the server can no longer match against any `delivered` row,
7188
- * which leaks the server's per-agent in-flight counter and stalls push
7189
- * after `inboxMaxInFlightPerAgent` messages.
7106
+ * construction time — `clientConnection.sendInboxAck`. Sending an additional
7107
+ * ack here would double-ack: a WS frame the server cannot match against any
7108
+ * `delivered` row, which leaks the server's per-agent in-flight counter and
7109
+ * stalls push after `inboxMaxInFlightPerAgent` messages.
7190
7110
  *
7191
7111
  * Dispatch errors propagate up; the entry stays `delivered` server-side
7192
- * and the 300s timeout reaper rolls it back to `pending` for replay
7193
- * (proposal §3.7).
7112
+ * and the 300s timeout reaper rolls it back to `pending` for replay.
7194
7113
  */
7195
7114
  async dispatchPushedFrame(frame) {
7196
7115
  if (!this.sessionManager) return;
@@ -7218,15 +7137,6 @@ var AgentSlot = class {
7218
7137
  if (chatIds.length === 0) return;
7219
7138
  this.clientConnection.sendSessionReconcile(this.config.agentId, chatIds);
7220
7139
  }
7221
- async pullAndDispatch() {
7222
- if (!this.sdk || !this.sessionManager) return;
7223
- try {
7224
- const { entries } = await this.sdk.pull(10);
7225
- for (const entry of entries) await this.sessionManager.dispatch(entry);
7226
- } catch (err) {
7227
- this.logger.warn({ err }, "poll error");
7228
- }
7229
- }
7230
7140
  };
7231
7141
  /**
7232
7142
  * Top-level marker file Claude Code writes after a successful OAuth login.
@@ -9853,7 +9763,7 @@ async function onboardCreate(args) {
9853
9763
  }
9854
9764
  const runtimeAgent = args.type === "human" ? args.assistant : args.id;
9855
9765
  if (args.feishuBotAppId && args.feishuBotAppSecret) {
9856
- const { bindFeishuBot } = await import("./feishu-CCWd-JE4.mjs").then((n) => n.r);
9766
+ const { bindFeishuBot } = await import("./feishu-DNoBroKK.mjs").then((n) => n.r);
9857
9767
  const targetAgentUuid = args.type === "human" ? assistantUuid : primary.uuid;
9858
9768
  if (!targetAgentUuid) print.line(`Warning: Cannot bind Feishu bot — no runtime agent available for "${args.id}".\n`);
9859
9769
  else {
@@ -11066,7 +10976,7 @@ function createFeedbackHandler(config) {
11066
10976
  return { handle };
11067
10977
  }
11068
10978
  //#endregion
11069
- //#region ../server/dist/app-Cv337jed.mjs
10979
+ //#region ../server/dist/app-D4mz6WSP.mjs
11070
10980
  var import_fastify_opentelemetry = /* @__PURE__ */ __toESM(require_fastify_opentelemetry(), 1);
11071
10981
  init_esm();
11072
10982
  var __defProp = Object.defineProperty;
@@ -11844,18 +11754,6 @@ async function agentInboxRoutes(app) {
11844
11754
  const query = inboxPollQuerySchema.parse(request.query);
11845
11755
  return await pollInbox(app.db, identity.inboxId, query.limit);
11846
11756
  });
11847
- app.post("/:entryId/ack", async (request, reply) => {
11848
- const identity = requireAgent(request);
11849
- const entryId = Number(request.params.entryId);
11850
- await ackEntry$2(app.db, entryId, identity.inboxId);
11851
- return reply.status(204).send();
11852
- });
11853
- app.post("/:entryId/renew", async (request, reply) => {
11854
- const identity = requireAgent(request);
11855
- const entryId = Number(request.params.entryId);
11856
- await renewEntry(app.db, entryId, identity.inboxId);
11857
- return reply.status(204).send();
11858
- });
11859
11757
  }
11860
11758
  async function agentMeRoutes(app) {
11861
11759
  app.get("/me", async (request) => {
@@ -11887,11 +11785,11 @@ async function agentMeRoutes(app) {
11887
11785
  * {imageId, mimeType, filename, size}
11888
11786
  *
11889
11787
  * The push is fire-and-forget: `ws.send()` queues the frame into the socket's
11890
- * send buffer synchronously, which is the only ordering guarantee we need
11891
- * the subsequent `new_message` notification travels a strictly slower PG
11892
- * NOTIFY round trip, so the image lands first on the wire. Awaiting the TCP
11893
- * flush here would put a slow subscriber's backpressure on the sender's
11894
- * HTTP response for a feature that is already best-effort.
11788
+ * send buffer synchronously, which is the only ordering guarantee we need
11789
+ * the subsequent `inbox:deliver` frame is driven by a PG NOTIFY round trip,
11790
+ * so the image lands first on the wire. Awaiting the TCP flush here would
11791
+ * put a slow subscriber's backpressure on the sender's HTTP response for a
11792
+ * feature that is already best-effort.
11895
11793
  *
11896
11794
  * Non-image messages are returned unchanged. Missing-subscriber / wrong-
11897
11795
  * instance cases are acceptable loss per the image-out-of-messages design
@@ -12588,8 +12486,7 @@ async function summarizeContextTreeUsage(db, organizationId, windowDays) {
12588
12486
  /**
12589
12487
  * Default per-agent in-flight cap when `server.inbox.maxInFlightPerAgent` is
12590
12488
  * unset. Mirrors the schema default so a hub running without an explicit
12591
- * `inbox` block still gets reasonable backpressure once `wsDataPlane` is
12592
- * flipped on. See proposal hub-inbox-ws-data-plane §3.5.
12489
+ * `inbox` block still gets reasonable backpressure.
12593
12490
  */
12594
12491
  const DEFAULT_INBOX_MAX_IN_FLIGHT_PER_AGENT = 32;
12595
12492
  /**
@@ -12634,23 +12531,12 @@ function clientWsRoutes(notifier, instanceId) {
12634
12531
  let authExpiryTimer = null;
12635
12532
  const boundAgents = /* @__PURE__ */ new Map();
12636
12533
  /**
12637
- * Whether the connected client opted into the WS inbox data plane via
12638
- * `client:register.wireCapabilities.wsInboxDeliver`. Set per-socket
12639
- * because client SDKs are upgraded independently — an old client
12640
- * connecting to a new server must keep receiving the legacy
12641
- * `new_message` doorbell + HTTP poll path (proposal §3.6).
12642
- */
12643
- let clientWantsWsInboxDeliver = false;
12644
- /**
12645
12534
  * Per-agent in-flight `inbox:deliver` counter for backpressure. Lives on
12646
12535
  * the socket — when the WS closes it goes with it; that's intentional,
12647
12536
  * because re-counting on a fresh connection would bias the cap against
12648
- * a healthy reconnect (proposal §3.5).
12537
+ * a healthy reconnect.
12649
12538
  */
12650
12539
  const inboxInFlight = /* @__PURE__ */ new Map();
12651
- function pushUseWsDataPlane() {
12652
- return clientWantsWsInboxDeliver;
12653
- }
12654
12540
  /**
12655
12541
  * Returns `false` when the socket has already moved out of `OPEN` —
12656
12542
  * the only failure mode the caller can observe synchronously.
@@ -12893,7 +12779,6 @@ function clientWsRoutes(notifier, instanceId) {
12893
12779
  try {
12894
12780
  if (type === "client:register") {
12895
12781
  const data = clientRegisterSchema.parse(msg);
12896
- clientWantsWsInboxDeliver = data.wireCapabilities?.wsInboxDeliver === true;
12897
12782
  let placeholderOrgId = jwtDefaultOrgId;
12898
12783
  if (!placeholderOrgId) {
12899
12784
  const [m] = await app.db.select({ organizationId: members.organizationId }).from(members).where(and(eq(members.userId, session.userId), eq(members.status, "active"))).orderBy(desc(members.createdAt), desc(members.id)).limit(1);
@@ -13031,9 +12916,7 @@ function clientWsRoutes(notifier, instanceId) {
13031
12916
  inboxId: agent.inboxId,
13032
12917
  organizationId: agent.organizationId
13033
12918
  });
13034
- const wsPushActive = pushUseWsDataPlane();
13035
- if (wsPushActive) notifier.subscribe(agent.inboxId, socket, makeInboxPushHandler(agent.id, agent.inboxId));
13036
- else notifier.subscribe(agent.inboxId, socket);
12919
+ notifier.subscribe(agent.inboxId, socket, makeInboxPushHandler(agent.id, agent.inboxId));
13037
12920
  socket.send(JSON.stringify({
13038
12921
  type: "agent:bound",
13039
12922
  ref,
@@ -13041,7 +12924,7 @@ function clientWsRoutes(notifier, instanceId) {
13041
12924
  displayName: agent.displayName,
13042
12925
  agentType: agent.type
13043
12926
  }));
13044
- if (wsPushActive) drainBacklogForAgent(agent.id, agent.inboxId).catch((err) => {
12927
+ drainBacklogForAgent(agent.id, agent.inboxId).catch((err) => {
13045
12928
  app.log.error({
13046
12929
  err,
13047
12930
  agentId: agent.id
@@ -13162,6 +13045,14 @@ function clientWsRoutes(notifier, instanceId) {
13162
13045
  } else if (type === "inbox:ack") {
13163
13046
  const payloadResult = inboxAckFrameSchema.safeParse(msg);
13164
13047
  if (!payloadResult.success) {
13048
+ app.log.warn({
13049
+ clientId,
13050
+ issues: payloadResult.error.issues.map((i) => ({
13051
+ path: i.path.join("."),
13052
+ code: i.code,
13053
+ message: i.message
13054
+ }))
13055
+ }, "malformed inbox:ack frame — replying error");
13165
13056
  socket.send(JSON.stringify({
13166
13057
  type: "error",
13167
13058
  message: "Malformed inbox:ack frame"
@@ -13171,7 +13062,14 @@ function clientWsRoutes(notifier, instanceId) {
13171
13062
  const { entryId } = payloadResult.data;
13172
13063
  try {
13173
13064
  const ackedEntry = await ackEntryByIdForBoundAgents(app.db, entryId, [...boundAgents.values()].map((a) => a.inboxId));
13174
- if (!ackedEntry) return;
13065
+ if (!ackedEntry) {
13066
+ app.log.debug({
13067
+ clientId,
13068
+ entryId,
13069
+ boundInboxes: boundAgents.size
13070
+ }, "inbox:ack matched no row — stale ack or reaper race");
13071
+ return;
13072
+ }
13175
13073
  const owner = [...boundAgents.values()].find((a) => a.inboxId === ackedEntry.inboxId);
13176
13074
  if (owner) {
13177
13075
  inboxInFlight.set(owner.agentId, Math.max(0, (inboxInFlight.get(owner.agentId) ?? 1) - 1));
@@ -13237,17 +13135,23 @@ async function agentActivityRoutes(app) {
13237
13135
  /**
13238
13136
  * Project a DB agent row into its wire shape. Strips the inline image
13239
13137
  * `avatarImageData` (large bytea, only meant for the image-serve route)
13240
- * and synthesises the public `avatarImageUrl` from the upload timestamp.
13241
- * `createdAt`/`updatedAt` are coerced to ISO strings so the response is
13242
- * pure JSON.
13138
+ * and synthesises the public `avatarImageUrl` via {@link resolveAvatarImageUrl}
13139
+ * so human agents fall back to the backing user's external avatar URL
13140
+ * (e.g. GitHub) when no upload exists. `createdAt`/`updatedAt` are
13141
+ * coerced to ISO strings so the response is pure JSON.
13243
13142
  */
13244
- function serializeAgent(agent) {
13143
+ function serializeAgent(agent, userAvatarUrl) {
13245
13144
  const { avatarImageData: _data, avatarImageMime: _mime, avatarImageUpdatedAt, createdAt, updatedAt, ...rest } = agent;
13246
13145
  return {
13247
13146
  ...rest,
13248
13147
  createdAt: createdAt.toISOString(),
13249
13148
  updatedAt: updatedAt.toISOString(),
13250
- avatarImageUrl: agentAvatarImageUrl(agent.uuid, avatarImageUpdatedAt ?? null)
13149
+ avatarImageUrl: resolveAvatarImageUrl({
13150
+ uuid: agent.uuid,
13151
+ type: agent.type,
13152
+ avatarImageUpdatedAt,
13153
+ userAvatarUrl
13154
+ })
13251
13155
  };
13252
13156
  }
13253
13157
  /**
@@ -13279,7 +13183,7 @@ async function agentRoutes(app) {
13279
13183
  }
13280
13184
  app.get("/:uuid", async (request) => {
13281
13185
  const { agent } = await requireAgentAccess(request, app.db, "visible");
13282
- return serializeAgent(agent);
13186
+ return serializeAgent(agent, await fetchUserAvatarForHumanAgent(app.db, agent));
13283
13187
  });
13284
13188
  app.patch("/:uuid", { config: { otelRecordBody: true } }, async (request) => {
13285
13189
  const { scope } = await requireAgentAccess(request, app.db, "manage");
@@ -13288,14 +13192,14 @@ async function agentRoutes(app) {
13288
13192
  const before = body.clientId !== void 0 ? await getAgent(app.db, request.params.uuid) : null;
13289
13193
  const agent = await updateAgent(app.db, request.params.uuid, body);
13290
13194
  if (before && before.clientId === null && agent.clientId !== null) notifyClientAgentPinned(agent);
13291
- return serializeAgent(agent);
13195
+ return serializeAgent(agent, await fetchUserAvatarForHumanAgent(app.db, agent));
13292
13196
  });
13293
13197
  app.patch("/:uuid/rebind", { config: { otelRecordBody: true } }, async (request) => {
13294
13198
  await requireAgentAccess(request, app.db, "manage");
13295
13199
  const body = rebindAgentSchema.parse(request.body);
13296
13200
  const agent = await rebindAgent(app.db, request.params.uuid, body);
13297
13201
  notifyClientAgentPinned(agent);
13298
- return serializeAgent(agent);
13202
+ return serializeAgent(agent, await fetchUserAvatarForHumanAgent(app.db, agent));
13299
13203
  });
13300
13204
  app.post("/:uuid/disconnect", async (request, reply) => {
13301
13205
  await requireAgentAccess(request, app.db, "manage");
@@ -13305,11 +13209,13 @@ async function agentRoutes(app) {
13305
13209
  });
13306
13210
  app.post("/:uuid/suspend", async (request) => {
13307
13211
  await requireAgentAccess(request, app.db, "manage");
13308
- return serializeAgent(await suspendAgent(app.db, request.params.uuid));
13212
+ const agent = await suspendAgent(app.db, request.params.uuid);
13213
+ return serializeAgent(agent, await fetchUserAvatarForHumanAgent(app.db, agent));
13309
13214
  });
13310
13215
  app.post("/:uuid/reactivate", async (request) => {
13311
13216
  await requireAgentAccess(request, app.db, "manage");
13312
- return serializeAgent(await reactivateAgent(app.db, request.params.uuid));
13217
+ const agent = await reactivateAgent(app.db, request.params.uuid);
13218
+ return serializeAgent(agent, await fetchUserAvatarForHumanAgent(app.db, agent));
13313
13219
  });
13314
13220
  app.delete("/:uuid", async (request, reply) => {
13315
13221
  await requireAgentAccess(request, app.db, "manage");
@@ -13859,6 +13765,7 @@ const APP_JWT_EXPIRY = "9m";
13859
13765
  * caller's side; the docs recommend 60 seconds. We mirror that.
13860
13766
  */
13861
13767
  const APP_JWT_IAT_SKEW_SECONDS = 60;
13768
+ const APP_INSTALLATION_TOKEN_URL = (id) => `https://api.github.com/app/installations/${id}/access_tokens`;
13862
13769
  const APP_INSTALLATION_URL = (id) => `https://api.github.com/app/installations/${id}`;
13863
13770
  const OAUTH_TOKEN_URL = "https://github.com/login/oauth/access_token";
13864
13771
  const OAUTH_AUTHORIZE_URL = "https://github.com/login/oauth/authorize";
@@ -13963,6 +13870,39 @@ async function listUserAccessibleInstallationIds(userAccessToken, opts = {}) {
13963
13870
  return out;
13964
13871
  }
13965
13872
  /**
13873
+ * Mint a per-installation token (server-to-server). The token is cheap
13874
+ * (one signature + one HTTP round-trip) and the upstream TTL is ~1h, so
13875
+ * the recommended caller pattern is "mint per request" rather than caching
13876
+ * — caching forces the caller to also track expiry, suspended state, and
13877
+ * GitHub-side permission churn, which the design explicitly punts to Phase
13878
+ * 4. We give callers a typed result and let them cache if profiling shows
13879
+ * the round-trip is hot.
13880
+ *
13881
+ * Throws `GithubAppApiError` on non-2xx. 401 means the App JWT is bad or
13882
+ * the App's key has been rotated upstream; 404 means the installation
13883
+ * was uninstalled. Callers SHOULD persist the suspension state when 403
13884
+ * comes back with `suspended` (the design tracks this as `suspended_at`
13885
+ * on `github_app_installations`).
13886
+ */
13887
+ async function mintInstallationToken(appJwt, installationId, opts = {}) {
13888
+ const res = await (opts.fetcher ?? fetch)(APP_INSTALLATION_TOKEN_URL(installationId), {
13889
+ method: "POST",
13890
+ headers: {
13891
+ Authorization: `Bearer ${appJwt}`,
13892
+ Accept: "application/vnd.github+json",
13893
+ "X-GitHub-Api-Version": "2022-11-28"
13894
+ }
13895
+ });
13896
+ if (!res.ok) throw new GithubAppApiError(res.status, `GitHub App installation-token request failed (${res.status})`);
13897
+ const body = await res.json();
13898
+ return {
13899
+ token: body.token,
13900
+ expiresAt: body.expires_at,
13901
+ permissions: body.permissions ?? {},
13902
+ repositorySelection: body.repository_selection ?? "all"
13903
+ };
13904
+ }
13905
+ /**
13966
13906
  * Trade an expiring user-to-server access token for a fresh pair using
13967
13907
  * its refresh token. Thrown on:
13968
13908
  * - Network / 5xx — `GithubAppApiError(status, …)`
@@ -15068,11 +15008,14 @@ async function chatRoutes(app) {
15068
15008
  }));
15069
15009
  const title = resolveChatTitle(chat.topic, firstMessagePreview, participantsForTitle, scope.humanAgentId);
15070
15010
  const engagementStatus = await getCallerEngagement(app.db, chat.id, scope.humanAgentId);
15011
+ const [callerMembership] = await app.db.select({ accessMode: chatMembership.accessMode }).from(chatMembership).where(and(eq(chatMembership.chatId, chat.id), eq(chatMembership.agentId, scope.humanAgentId))).limit(1);
15012
+ const viewerMembershipKind = callerMembership ? callerMembership.accessMode === "speaker" ? "participant" : "watching" : null;
15071
15013
  return {
15072
15014
  ...chat,
15073
15015
  title,
15074
15016
  firstMessagePreview,
15075
15017
  engagementStatus,
15018
+ viewerMembershipKind,
15076
15019
  createdAt: chat.createdAt.toISOString(),
15077
15020
  updatedAt: chat.updatedAt.toISOString(),
15078
15021
  participants: participants.map((p) => ({
@@ -15695,6 +15638,26 @@ function normalizeRemoteRepoUrl(value) {
15695
15638
  if (/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+(?:\.git)?$/.test(value)) return `https://github.com/${value}`;
15696
15639
  return value;
15697
15640
  }
15641
+ /**
15642
+ * Whether this binding actually drives a GitHub-hosted remote fetch — the
15643
+ * only case where minting a GitHub App installation token is meaningful.
15644
+ *
15645
+ * Returns false when:
15646
+ * - `localPath` is set (sync code short-circuits to the local checkout
15647
+ * before ever looking at `repo`)
15648
+ * - `repo` is missing
15649
+ * - `repo` is a file:// URL, a non-GitHub HTTPS URL, or otherwise
15650
+ * unparseable
15651
+ *
15652
+ * Used by the snapshot routes to gate the "install the GitHub App"
15653
+ * guidance — without this gate, every unavailable snapshot (missing repo,
15654
+ * bad branch, …) gets a misleading App-install hint appended.
15655
+ */
15656
+ function isGithubRemoteBinding(binding) {
15657
+ if (binding.localPath && binding.localPath.trim().length > 0) return false;
15658
+ if (!binding.repo) return false;
15659
+ return isGithubHttpsRepo(normalizeRemoteRepoUrl(binding.repo));
15660
+ }
15698
15661
  function managedContextTreeCacheRoot() {
15699
15662
  return join(DEFAULT_DATA_DIR$1, "context-tree-repos");
15700
15663
  }
@@ -15868,7 +15831,7 @@ function errorMessage(error) {
15868
15831
  return redactSecret(error.message.trim().split("\n")[0] ?? "");
15869
15832
  }
15870
15833
  function redactSecret(message) {
15871
- return message.replace(/(https?:\/\/)[^/@\s]+@/g, "$1[redacted]@").replace(/\bghp_[A-Za-z0-9_]+/g, "[redacted]").replace(/\bgithub_pat_[A-Za-z0-9_]+/g, "[redacted]");
15834
+ return message.replace(/(https?:\/\/)[^/@\s]+@/g, "$1[redacted]@").replace(/\b(?:ghp|ghs|ghu|gho|ghr)_[A-Za-z0-9_]+/g, "[redacted]").replace(/\bgithub_pat_[A-Za-z0-9_]+/g, "[redacted]");
15872
15835
  }
15873
15836
  function unavailableSnapshot(repo, branch, detail) {
15874
15837
  return {
@@ -16448,6 +16411,80 @@ function ghostNodeId(path) {
16448
16411
  function toPosix(path) {
16449
16412
  return sep === "/" ? path : path.split(sep).join("/");
16450
16413
  }
16414
+ /**
16415
+ * Mint a short-lived GitHub App installation token for the given installation.
16416
+ * Returns `ok: false` (with a precise reason) when the org has no App
16417
+ * configured, no installation row, the installation is suspended, or GitHub
16418
+ * rejects the mint — callers fall back to unauthenticated git fetch (public
16419
+ * repos still resolve; private repos surface as an unavailable snapshot
16420
+ * with a remediation message).
16421
+ *
16422
+ * Takes the `installation` row directly so the helper has no DB dependency
16423
+ * — route handlers do the `findInstallationByOrg` lookup themselves. Keeps
16424
+ * this module a pure transform that's trivial to unit-test.
16425
+ *
16426
+ * Credentials use the narrow `GithubAppCredentials` shape so the helper
16427
+ * isn't coupled to the broader OAuth config surface; callers pass
16428
+ * `config.oauth?.githubApp`, which structurally satisfies it.
16429
+ */
16430
+ async function mintContextTreeInstallationToken(installation, appCredentials, options = {}) {
16431
+ if (!appCredentials) return {
16432
+ ok: false,
16433
+ reason: "no-app-config"
16434
+ };
16435
+ if (!installation) return {
16436
+ ok: false,
16437
+ reason: "no-installation"
16438
+ };
16439
+ if (installation.suspendedAt) return {
16440
+ ok: false,
16441
+ reason: "suspended"
16442
+ };
16443
+ try {
16444
+ return {
16445
+ ok: true,
16446
+ token: (await mintInstallationToken(await createAppJwt({
16447
+ appId: appCredentials.appId,
16448
+ privateKeyPem: appCredentials.privateKeyPem
16449
+ }), installation.installationId, { fetcher: options.fetcher })).token
16450
+ };
16451
+ } catch (error) {
16452
+ return {
16453
+ ok: false,
16454
+ reason: "mint-failed",
16455
+ detail: error instanceof GithubAppApiError ? `GitHub returned ${error.status} when minting an installation token.` : "Hub could not mint a GitHub App installation token."
16456
+ };
16457
+ }
16458
+ }
16459
+ /**
16460
+ * Append a remediation hint to an unavailable snapshot's `contextStatus.detail`
16461
+ * when the underlying cause is a missing / suspended / failed GitHub App token
16462
+ * mint. Public-repo snapshots (mint reason `no-app-config`) are left untouched
16463
+ * — the deployment may legitimately have no App configured.
16464
+ *
16465
+ * Gated on `isGithubRemoteBinding(binding)` so unrelated unavailable
16466
+ * reasons (no repo configured, localPath missing, illegal branch name,
16467
+ * public-repo fetch error) don't get a misleading "install the GitHub
16468
+ * App" hint appended.
16469
+ *
16470
+ * Lives next to `mintContextTreeInstallationToken` so the two routes that
16471
+ * call mint share one shaping function; the snapshot service itself stays
16472
+ * token-agnostic.
16473
+ */
16474
+ function decorateSnapshotWithMintGuidance(snapshot, binding, mintResult) {
16475
+ if (mintResult.ok) return snapshot;
16476
+ if (snapshot.snapshotStatus !== "unavailable") return snapshot;
16477
+ if (mintResult.reason === "no-app-config") return snapshot;
16478
+ if (!isGithubRemoteBinding(binding)) return snapshot;
16479
+ const guidance = mintResult.reason === "no-installation" ? "Install the First Tree GitHub App from Team Settings and grant it access to this repo." : mintResult.reason === "suspended" ? "The GitHub App installation is suspended — unsuspend it from your GitHub account settings." : `Hub could not mint a GitHub App installation token.${mintResult.detail ? ` ${mintResult.detail}` : ""}`;
16480
+ return {
16481
+ ...snapshot,
16482
+ contextStatus: {
16483
+ ...snapshot.contextStatus,
16484
+ detail: `${snapshot.contextStatus.detail} ${guidance}`
16485
+ }
16486
+ };
16487
+ }
16451
16488
  const querySchema$1 = z.object({ window: z.enum([
16452
16489
  "1d",
16453
16490
  "7d",
@@ -16463,12 +16500,15 @@ async function contextTreeSnapshotRoutes(app) {
16463
16500
  const { userId } = requireUser(request);
16464
16501
  const orgId = await resolveUserPrimaryOrgId(app.db, userId);
16465
16502
  const binding = orgId ? await getOrgContextTree(app.db, orgId) : {};
16466
- const githubToken = contextTreeGithubTokenForRepo(binding.repo, app.config.contextTreeSync);
16503
+ let mintResult = null;
16504
+ if (orgId && isGithubRemoteBinding(binding)) mintResult = await mintContextTreeInstallationToken(await findInstallationByOrg(app.db, orgId), app.config.oauth?.githubApp);
16505
+ const githubToken = mintResult?.ok ? mintResult.token : void 0;
16467
16506
  const window = query.window ?? "7d";
16468
- const snapshot = await getContextTreeSnapshot({
16507
+ const rawSnapshot = await getContextTreeSnapshot({
16469
16508
  ...binding,
16470
16509
  githubToken
16471
16510
  }, window);
16511
+ const snapshot = mintResult ? decorateSnapshotWithMintGuidance(rawSnapshot, binding, mintResult) : rawSnapshot;
16472
16512
  const usage = orgId ? await summarizeContextTreeUsage(app.db, orgId, contextTreeSnapshotWindowDays(window)) : snapshot.usage;
16473
16513
  return contextTreeSnapshotSchema.parse({
16474
16514
  ...snapshot,
@@ -16476,31 +16516,6 @@ async function contextTreeSnapshotRoutes(app) {
16476
16516
  });
16477
16517
  });
16478
16518
  }
16479
- function contextTreeGithubTokenForRepo(repo, syncConfig) {
16480
- if (!repo || !syncConfig?.githubToken) return void 0;
16481
- const repoKey = githubRepoKey(repo);
16482
- if (!repoKey) return void 0;
16483
- return new Set((syncConfig.githubTokenRepos ?? "").split(",").map((entry) => normalizeGithubRepoKey(entry)).filter((entry) => entry !== null)).has(repoKey) ? syncConfig.githubToken : void 0;
16484
- }
16485
- function githubRepoKey(value) {
16486
- const shorthand = normalizeGithubRepoKey(value);
16487
- if (shorthand) return shorthand;
16488
- let url;
16489
- try {
16490
- url = new URL(value);
16491
- } catch {
16492
- return null;
16493
- }
16494
- if (url.protocol !== "https:" || url.hostname.toLowerCase() !== "github.com") return null;
16495
- if (url.username || url.password) return null;
16496
- return normalizeGithubRepoKey(url.pathname.replace(/^\/+/, ""));
16497
- }
16498
- function normalizeGithubRepoKey(value) {
16499
- const trimmed = value.trim().replace(/^\/+/, "").replace(/\.git$/i, "");
16500
- const match = /^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/.exec(trimmed);
16501
- if (!match) return null;
16502
- return `${match[1]?.toLowerCase()}/${match[2]?.toLowerCase()}`;
16503
- }
16504
16519
  /**
16505
16520
  * Resolve the client IP for rate-limit attribution.
16506
16521
  *
@@ -16602,7 +16617,7 @@ async function healthzRoutes(app) {
16602
16617
  * `api/orgs/invitations.ts` (Class B, admin-gated).
16603
16618
  */
16604
16619
  async function publicInvitationRoutes(app) {
16605
- const { previewInvitation } = await import("./invitation-C9m2gQx4-CkwWteA3.mjs");
16620
+ const { previewInvitation } = await import("./invitation-C9m2gQx4-C_4f5VTs.mjs");
16606
16621
  app.get("/:token/preview", async (request, reply) => {
16607
16622
  if (!request.params.token) throw new UnauthorizedError("Token required");
16608
16623
  const preview = await previewInvitation(app.db, request.params.token);
@@ -16881,7 +16896,13 @@ async function meRoutes(app) {
16881
16896
  inboxId: r.inboxId,
16882
16897
  visibility: r.visibility,
16883
16898
  runtimeProvider: r.runtimeProvider,
16884
- clientId: r.clientId
16899
+ clientId: r.clientId,
16900
+ avatarImageUrl: resolveAvatarImageUrl({
16901
+ uuid: r.uuid,
16902
+ type: r.type,
16903
+ avatarImageUpdatedAt: r.avatarImageUpdatedAt,
16904
+ userAvatarUrl: r.userAvatarUrl
16905
+ })
16885
16906
  }));
16886
16907
  });
16887
16908
  /**
@@ -16891,7 +16912,7 @@ async function meRoutes(app) {
16891
16912
  */
16892
16913
  app.get("/me/pinned-agents", async (request) => {
16893
16914
  const { userId } = requireUser(request);
16894
- const { listMyPinnedAgents } = await import("./client-CDw0f-kN-BPzOVd8L.mjs");
16915
+ const { listMyPinnedAgents } = await import("./client-CEdYVnoj-BGiGcJbH.mjs");
16895
16916
  return listMyPinnedAgents(app.db, { userId });
16896
16917
  });
16897
16918
  /**
@@ -17275,7 +17296,7 @@ async function orgAgentRoutes(app) {
17275
17296
  const { type } = listAgentsFilterSchema.parse(request.query);
17276
17297
  const result = await listAgentsForMember(app.db, scope, query.limit, query.cursor, type);
17277
17298
  return {
17278
- items: result.items.map(({ avatarImageUpdatedAt, ...a }) => ({
17299
+ items: result.items.map(({ avatarImageUpdatedAt, userAvatarUrl, ...a }) => ({
17279
17300
  ...a,
17280
17301
  managerId: a.managerId ?? null,
17281
17302
  presenceStatus: a.presenceStatus ?? "offline",
@@ -17285,7 +17306,12 @@ async function orgAgentRoutes(app) {
17285
17306
  runtimeType: a.runtimeType ?? null,
17286
17307
  runtimeState: a.runtimeState ?? null,
17287
17308
  activeSessions: a.activeSessions ?? null,
17288
- avatarImageUrl: agentAvatarImageUrl(a.uuid, avatarImageUpdatedAt)
17309
+ avatarImageUrl: resolveAvatarImageUrl({
17310
+ uuid: a.uuid,
17311
+ type: a.type,
17312
+ avatarImageUpdatedAt,
17313
+ userAvatarUrl
17314
+ })
17289
17315
  })),
17290
17316
  nextCursor: result.nextCursor
17291
17317
  };
@@ -17301,7 +17327,7 @@ async function orgAgentRoutes(app) {
17301
17327
  const query = paginationQuerySchema.parse(request.query);
17302
17328
  const result = await listAgentsForAdmin(app.db, scope, query.limit, query.cursor);
17303
17329
  return {
17304
- items: result.items.map(({ avatarImageUpdatedAt, ...a }) => ({
17330
+ items: result.items.map(({ avatarImageUpdatedAt, userAvatarUrl, ...a }) => ({
17305
17331
  ...a,
17306
17332
  managerId: a.managerId ?? null,
17307
17333
  presenceStatus: a.presenceStatus ?? "offline",
@@ -17311,7 +17337,12 @@ async function orgAgentRoutes(app) {
17311
17337
  runtimeType: a.runtimeType ?? null,
17312
17338
  runtimeState: a.runtimeState ?? null,
17313
17339
  activeSessions: a.activeSessions ?? null,
17314
- avatarImageUrl: agentAvatarImageUrl(a.uuid, avatarImageUpdatedAt)
17340
+ avatarImageUrl: resolveAvatarImageUrl({
17341
+ uuid: a.uuid,
17342
+ type: a.type,
17343
+ avatarImageUpdatedAt,
17344
+ userAvatarUrl
17345
+ })
17315
17346
  })),
17316
17347
  nextCursor: result.nextCursor
17317
17348
  };
@@ -17467,12 +17498,15 @@ async function orgContextTreeSnapshotRoutes(app) {
17467
17498
  const query = querySchema.parse(request.query);
17468
17499
  const scope = await requireOrgMembership(request, app.db);
17469
17500
  const binding = await getOrgContextTree(app.db, scope.organizationId);
17470
- const githubToken = contextTreeGithubTokenForRepo(binding.repo, app.config.contextTreeSync);
17501
+ let mintResult = null;
17502
+ if (isGithubRemoteBinding(binding)) mintResult = await mintContextTreeInstallationToken(await findInstallationByOrg(app.db, scope.organizationId), app.config.oauth?.githubApp);
17503
+ const githubToken = mintResult?.ok ? mintResult.token : void 0;
17471
17504
  const window = query.window ?? "7d";
17472
- const snapshot = await getContextTreeSnapshot({
17505
+ const rawSnapshot = await getContextTreeSnapshot({
17473
17506
  ...binding,
17474
17507
  githubToken
17475
17508
  }, window);
17509
+ const snapshot = mintResult ? decorateSnapshotWithMintGuidance(rawSnapshot, binding, mintResult) : rawSnapshot;
17476
17510
  const usage = await summarizeContextTreeUsage(app.db, scope.organizationId, contextTreeSnapshotWindowDays(window));
17477
17511
  return contextTreeSnapshotSchema.parse({
17478
17512
  ...snapshot,
@@ -21226,7 +21260,7 @@ function detectInstallMode(argv1 = process.argv[1] ?? "") {
21226
21260
  resolvedArgv1 = argv1;
21227
21261
  }
21228
21262
  const start = dirname(resolve(resolvedArgv1));
21229
- {
21263
+ if (!/(?:^|[\\/])node_modules[\\/]/.test(resolvedArgv1)) {
21230
21264
  let dir = start;
21231
21265
  for (let i = 0; i < 10; i++) {
21232
21266
  if (existsSync(resolve(dir, ".git"))) return "source";