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

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.
@@ -570,13 +570,6 @@ const serverConfigSchema = defineConfig({
570
570
  refreshTokenExpiry: field(z.string().default("30d"), { env: "FIRST_TREE_HUB_AUTH_REFRESH_TOKEN_EXPIRY" }),
571
571
  connectTokenExpiry: field(z.string().default("10m"), { env: "FIRST_TREE_HUB_AUTH_CONNECT_TOKEN_EXPIRY" })
572
572
  },
573
- contextTreeSync: optional({
574
- githubToken: field(z.string(), {
575
- env: "FIRST_TREE_HUB_CONTEXT_TREE_GITHUB_TOKEN",
576
- secret: true
577
- }),
578
- githubTokenRepos: field(z.string().optional(), { env: "FIRST_TREE_HUB_CONTEXT_TREE_GITHUB_TOKEN_REPOS" })
579
- }),
580
573
  oauth: optional({ githubApp: optional({
581
574
  appId: field(z.string().min(1), { env: "FIRST_TREE_HUB_GITHUB_APP_ID" }),
582
575
  clientId: field(z.string().min(1), { env: "FIRST_TREE_HUB_GITHUB_APP_CLIENT_ID" }),
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import "../observability-BAScT_5S-BcW9HgkG.mjs";
3
- import { $ as formatStaleReason, A as checkDocker, B as isServiceSupported, C as createApiNameResolver, D as checkBackgroundService, E as checkAgentConfigs, F as checkWebSocket, H as restartClientService, I as printResults, J as stopPostgres, L as reconcileAgentConfigs, M as checkServerConfig, N as checkServerHealth, O as checkClientConfig, P as checkServerReachable, Q as findStaleAliases, R as getClientServiceStatus, S as runHomeMigration, T as runMigrations, U as startClientService, W as stopClientService, X as handleClientOrgMismatch, Y as ClientRuntime, _ as formatCheckReport, a as declineUpdate, at as fail, b as onboardCreate, c as detectInstallMode, ct as ClientUserMismatchError, d as startServer, dt as SessionRegistry, et as removeLocalAgent, f as reconcileLocalRuntimeProviders, ft as cleanWorkspaces, g as promptMissingFields, h as promptAddAgent, ht as configureClientLoggerForService, i as createExecuteUpdate, it as resolveSenderName, j as checkNodeVersion, k as checkDatabase, l as fetchLatestVersion, lt as FirstTreeHubSDK, m as isInteractive, mt as applyClientLoggerConfig, o as promptUpdate, ot as success, p as uploadClientCapabilities, pt as probeCapabilities, r as registerSaaSConnectCommand, rt as resolveReplyToFromEnv, s as PACKAGE_NAME, st as ClientOrgMismatchError, tt as createOwner, u as installGlobalLatest, ut as SdkError, v as loadOnboardState, w as migrateLocalAgentDirs, x as saveOnboardState, y as onboardCheck, z as installClientService } from "../saas-connect-DgCSZ8Yk.mjs";
3
+ import { $ as formatStaleReason, A as checkDocker, B as isServiceSupported, C as createApiNameResolver, D as checkBackgroundService, E as checkAgentConfigs, F as checkWebSocket, H as restartClientService, I as printResults, J as stopPostgres, L as reconcileAgentConfigs, M as checkServerConfig, N as checkServerHealth, O as checkClientConfig, P as checkServerReachable, Q as findStaleAliases, R as getClientServiceStatus, S as runHomeMigration, T as runMigrations, U as startClientService, W as stopClientService, X as handleClientOrgMismatch, Y as ClientRuntime, _ as formatCheckReport, a as declineUpdate, at as fail, b as onboardCreate, c as detectInstallMode, ct as ClientUserMismatchError, d as startServer, dt as SessionRegistry, et as removeLocalAgent, f as reconcileLocalRuntimeProviders, ft as cleanWorkspaces, g as promptMissingFields, h as promptAddAgent, ht as configureClientLoggerForService, i as createExecuteUpdate, it as resolveSenderName, j as checkNodeVersion, k as checkDatabase, l as fetchLatestVersion, lt as FirstTreeHubSDK, m as isInteractive, mt as applyClientLoggerConfig, o as promptUpdate, ot as success, p as uploadClientCapabilities, pt as probeCapabilities, r as registerSaaSConnectCommand, rt as resolveReplyToFromEnv, s as PACKAGE_NAME, st as ClientOrgMismatchError, tt as createOwner, u as installGlobalLatest, ut as SdkError, v as loadOnboardState, w as migrateLocalAgentDirs, x as saveOnboardState, y as onboardCheck, z as installClientService } from "../saas-connect-ChxZv2YQ.mjs";
4
4
  import "../logger-core-BTmvdflj-DjW8FM4T.mjs";
5
- import { C as resetConfigMeta, E as setConfigValue, S as resetConfig, T as serverConfigSchema, _ as getConfigValue, a as ensureFreshAdminToken, c as resolveServerUrl, d as DEFAULT_CONFIG_DIR, f as DEFAULT_DATA_DIR, h as clientConfigSchema, i as ensureFreshAccessToken, l as saveAgentConfig, m as agentConfigSchema, o as loadCredentials, p as DEFAULT_HOME_DIR, v as initConfig, w as resolveConfigReadonly, x as readConfigFile, y as loadAgents } from "../bootstrap-C15ZBOCC.mjs";
5
+ import { C as resetConfigMeta, E as setConfigValue, S as resetConfig, T as serverConfigSchema, _ as getConfigValue, a as ensureFreshAdminToken, c as resolveServerUrl, d as DEFAULT_CONFIG_DIR, f as DEFAULT_DATA_DIR, h as clientConfigSchema, i as ensureFreshAccessToken, l as saveAgentConfig, m as agentConfigSchema, o as loadCredentials, p as DEFAULT_HOME_DIR, v as initConfig, w as resolveConfigReadonly, x as readConfigFile, y as loadAgents } from "../bootstrap-CQQGgIx1.mjs";
6
6
  import { a as print, n as CLI_USER_AGENT, o as setJsonMode, r as COMMAND_VERSION, t as cliFetch } from "../cli-fetch--tiwKm5S.mjs";
7
- import "../dist-DmYxT5Kb.mjs";
8
- import { n as bindFeishuUser, t as bindFeishuBot } from "../feishu-CCWd-JE4.mjs";
7
+ import "../dist-CwsiHGX7.mjs";
8
+ import { n as bindFeishuUser, t as bindFeishuBot } from "../feishu-DHSy6WDD.mjs";
9
9
  import "../uuid-DbS_4vFh-iFghv4zA.mjs";
10
10
  import "../src-DNBS5Yjj.mjs";
11
- import "../client-CZ_VnbEc-CBF46cJd.mjs";
11
+ import "../client-BSfCc0pJ-BP_1f21y.mjs";
12
12
  import "../invitation-D_ENPHyj-5ETiae5r.mjs";
13
13
  import { join } from "node:path";
14
14
  import { existsSync, mkdirSync, readFileSync, readdirSync } from "node:fs";
@@ -255,11 +255,6 @@ function handleSdkError$1(error) {
255
255
  if (error instanceof TypeError && "cause" in error) fail("CONNECTION_ERROR", `Cannot connect to server: ${error.message}`, 6);
256
256
  fail("UNKNOWN_ERROR", error instanceof Error ? error.message : String(error), 1);
257
257
  }
258
- function parseLimit$1(value, max) {
259
- const limit = Number.parseInt(value, 10);
260
- if (Number.isNaN(limit) || limit < 1 || limit > max) fail("INVALID_LIMIT", `Limit must be between 1 and ${max}.`, 2);
261
- return limit;
262
- }
263
258
  async function resolveAgent$1(serverUrl, adminToken, agentName) {
264
259
  const res = await cliFetch(`${serverUrl}/api/v1/me/managed-agents`, {
265
260
  headers: { Authorization: `Bearer ${adminToken}` },
@@ -693,25 +688,13 @@ function registerAgentCommands(program) {
693
688
  fail("SESSION_CMD_ERROR", error instanceof Error ? error.message : String(error));
694
689
  }
695
690
  });
696
- const debugCmd = agent.command("debug", { hidden: true }).description("Low-level SDK debug commands");
697
- debugCmd.command("register").description("Register this agent and return identity info").option("--agent <name>", "Agent name on the Hub (default: first configured on this client)").action(async (options) => {
691
+ agent.command("debug", { hidden: true }).description("Low-level SDK debug commands").command("register").description("Register this agent and return identity info").option("--agent <name>", "Agent name on the Hub (default: first configured on this client)").action(async (options) => {
698
692
  try {
699
693
  success(await createSdk$1(options.agent).register());
700
694
  } catch (error) {
701
695
  handleSdkError$1(error);
702
696
  }
703
697
  });
704
- debugCmd.command("pull").description("Pull pending messages from inbox").option("-l, --limit <number>", "Maximum entries to return", "10").option("-a, --ack", "Automatically ACK entries after pulling").option("--agent <name>", "Agent name on the Hub (default: first configured on this client)").action(async (options) => {
705
- try {
706
- const sdk = createSdk$1(options.agent);
707
- const limit = parseLimit$1(options.limit, 50);
708
- const result = await sdk.pull(limit);
709
- if (options.ack && result.entries.length > 0) await Promise.all(result.entries.map((entry) => sdk.ack(entry.id)));
710
- success(result);
711
- } catch (error) {
712
- handleSdkError$1(error);
713
- }
714
- });
715
698
  }
716
699
  //#endregion
717
700
  //#region src/commands/chat.ts
@@ -1487,13 +1470,13 @@ function decodeJwtExpSeconds(token) {
1487
1470
  //#region src/commands/onboard.ts
1488
1471
  async function promptMissing(args) {
1489
1472
  if (!args.server) try {
1490
- const { resolveServerUrl } = await import("../bootstrap-C15ZBOCC.mjs").then((n) => n.r);
1473
+ const { resolveServerUrl } = await import("../bootstrap-CQQGgIx1.mjs").then((n) => n.r);
1491
1474
  resolveServerUrl();
1492
1475
  } catch {
1493
1476
  args.server = await input({ message: "Hub server URL:" });
1494
1477
  saveOnboardState(args);
1495
1478
  }
1496
- const { loadCredentials } = await import("../bootstrap-C15ZBOCC.mjs").then((n) => n.r);
1479
+ const { loadCredentials } = await import("../bootstrap-CQQGgIx1.mjs").then((n) => n.r);
1497
1480
  if (!loadCredentials()) throw new Error("No saved credentials. Run `first-tree-hub connect <token>` before onboarding.");
1498
1481
  if (!args.id) {
1499
1482
  args.id = await input({ message: "Agent ID:" });
@@ -1,10 +1,10 @@
1
1
  import { A as FIRST_TREE_HUB_ATTR, O as withSpan, f as messageAttrs, s as createLogger } from "./observability-BAScT_5S-BcW9HgkG.mjs";
2
- import { L as extractMentions, N as defaultParticipantMode, P as defaultRuntimeConfigPayload, S as clientCapabilitiesSchema, St as stripCode, Z as isReservedAgentName, a as AGENT_TYPES, b as agentTypeSchema, ct as questionAnswerMessageContentSchema, d as MENTION_REGEX, i as AGENT_STATUSES, l as GITHUB_ENTITY_TYPES, lt as questionMessageContentSchema, mt as scanMentionTokens, n as AGENT_NAME_REGEX, nt as messageSourceSchema, o as AGENT_VISIBILITY, s as CHAT_ENGAGEMENT_STATUSES } from "./dist-DmYxT5Kb.mjs";
2
+ import { L as extractMentions, N as defaultParticipantMode, P as defaultRuntimeConfigPayload, S as clientCapabilitiesSchema, St as stripCode, Z as isReservedAgentName, a as AGENT_TYPES, b as agentTypeSchema, ct as questionAnswerMessageContentSchema, d as MENTION_REGEX, i as AGENT_STATUSES, l as GITHUB_ENTITY_TYPES, lt as questionMessageContentSchema, mt as scanMentionTokens, n as AGENT_NAME_REGEX, nt as messageSourceSchema, o as AGENT_VISIBILITY, s as CHAT_ENGAGEMENT_STATUSES } from "./dist-CwsiHGX7.mjs";
3
3
  import { a as ClientUserMismatchError, c as NotFoundError, d as users, f as uuidv7, o as ConflictError, r as BadRequestError, s as ForbiddenError, t as AgentSendNonMemberError, u as organizations } from "./uuid-DbS_4vFh-iFghv4zA.mjs";
4
4
  import { randomUUID } from "node:crypto";
5
5
  import { and, asc, count, desc, eq, gt, gte, inArray, isNotNull, lt, ne, or, sql } from "drizzle-orm";
6
6
  import { bigserial, boolean, customType, index, integer, jsonb, pgTable, primaryKey, serial, text, timestamp, unique } from "drizzle-orm/pg-core";
7
- //#region ../server/dist/client-CZ_VnbEc.mjs
7
+ //#region ../server/dist/client-BSfCc0pJ.mjs
8
8
  /**
9
9
  * Client connections. A client is a single SDK process (AgentRuntime) that may
10
10
  * host multiple agents. From the unified-user-token milestone on, a client is
@@ -401,7 +401,7 @@ async function pollInboxInner(db, inboxId, limit) {
401
401
  /**
402
402
  * Shared payload assembler for already-claimed `inbox_entries` rows.
403
403
  *
404
- * Both the HTTP poll path (`pollInbox`) and the WS push path
404
+ * Both the debug `GET /inbox` path (`pollInbox`) and the WS push path
405
405
  * (`claimAndBuildForPush`) call this with rows they have just `UPDATE`d to
406
406
  * `status='delivered'`. Keeping the silent-context bundling in one place is
407
407
  * the only way to keep the two paths from drifting (proposal
@@ -473,9 +473,9 @@ const PUSH_CLAIM_BATCH_LIMIT = 8;
473
473
  * WS-push path: atomically claim every pending entry the just-fired
474
474
  * `NOTIFY (inboxId:messageId)` references and assemble their wire payloads.
475
475
  *
476
- * Returns `[]` if no row matches — benign race with HTTP poll or another
477
- * server instance that already claimed the entry. NOTIFY is fire-and-forget
478
- * (proposal §3.2).
476
+ * Returns `[]` if no row matches — benign race with another server instance
477
+ * (or the debug `GET /inbox` endpoint) that already claimed the entry.
478
+ * NOTIFY is fire-and-forget (proposal §3.2).
479
479
  *
480
480
  * Why an array, not a single row: `sendMessage` can write **two** rows for
481
481
  * the same `(inbox, messageId)` pair when the recipient is both a chat
@@ -500,9 +500,9 @@ async function claimAndBuildForPush(db, inboxId, messageId) {
500
500
  /**
501
501
  * WS-push backlog path: on agent rebind (or once an in-flight slot frees up
502
502
  * after an ack), drain up to `limit` pending `notify=true` entries oldest-
503
- * first and assemble wire payloads. Identical claim shape to the HTTP poll
504
- * path — they are intentionally interchangeable so a hot-path bug fixed in
505
- * one shows up in the other (proposal §3.3 / §3.5).
503
+ * first and assemble wire payloads. Identical claim shape to `pollInbox`
504
+ * they are intentionally interchangeable so a hot-path bug fixed in one
505
+ * shows up in the other (proposal §3.3 / §3.5).
506
506
  */
507
507
  async function claimBacklogForPush(db, inboxId, limit) {
508
508
  return withSpan("inbox.deliver.backlog", {
@@ -563,28 +563,14 @@ async function collectPrecedingContext(tx, inboxId, triggers) {
563
563
  }
564
564
  return result;
565
565
  }
566
- async function ackEntry(db, entryId, inboxId) {
567
- return withSpan("inbox.ack", {
568
- [FIRST_TREE_HUB_ATTR.INBOX_ENTRY_ID]: String(entryId),
569
- "inbox.id": inboxId
570
- }, async () => {
571
- const [entry] = await db.update(inboxEntries).set({
572
- status: "acked",
573
- ackedAt: /* @__PURE__ */ new Date()
574
- }).where(and(eq(inboxEntries.id, entryId), eq(inboxEntries.inboxId, inboxId), eq(inboxEntries.status, "delivered"))).returning();
575
- if (!entry) throw new NotFoundError("Inbox entry not found or not in delivered status");
576
- return entry;
577
- });
578
- }
579
566
  /**
580
567
  * Ack a delivered entry from the WS data plane, scoped to the inboxes the
581
568
  * connected socket has bound. Returns the acked row on success, `null` if no
582
569
  * row matches — a benign outcome the caller should ignore (the entry may
583
570
  * have already been acked, timed out, or never belonged to this socket).
584
571
  *
585
- * Distinct from {@link ackEntry} so the WS path can ack without trusting an
586
- * `inboxId` from the wire only entries whose `inboxId` is in `inboxIds`
587
- * are eligible. Empty `inboxIds` short-circuits to `null`.
572
+ * Trusts only the `inboxId` set the connected socket has bound (no `inboxId`
573
+ * on the wire), and short-circuits on an empty `inboxIds`.
588
574
  */
589
575
  async function ackEntryByIdForBoundAgents(db, entryId, inboxIds) {
590
576
  if (inboxIds.length === 0) return null;
@@ -596,11 +582,6 @@ async function ackEntryByIdForBoundAgents(db, entryId, inboxIds) {
596
582
  return entry ?? null;
597
583
  });
598
584
  }
599
- async function renewEntry(db, entryId, inboxId) {
600
- const [entry] = await db.update(inboxEntries).set({ deliveredAt: /* @__PURE__ */ new Date() }).where(and(eq(inboxEntries.id, entryId), eq(inboxEntries.inboxId, inboxId), eq(inboxEntries.status, "delivered"))).returning();
601
- if (!entry) throw new NotFoundError("Inbox entry not found or not in delivered status");
602
- return entry;
603
- }
604
585
  async function resetTimedOutEntries(db, timeoutSeconds = DEFAULT_INBOX_TIMEOUT_SECONDS, maxRetries = DEFAULT_MAX_RETRY_COUNT) {
605
586
  const reset = await db.update(inboxEntries).set({
606
587
  status: "pending",
@@ -2298,15 +2279,9 @@ function createNotifier(listenClient) {
2298
2279
  const messageId = payload.slice(sepIdx + 1);
2299
2280
  const sockets = subscriptions.get(inboxId);
2300
2281
  if (!sockets) return;
2301
- const doorbellFrame = JSON.stringify({
2302
- type: "new_message",
2303
- inboxId,
2304
- messageId
2305
- });
2306
2282
  for (const [ws, pushHandler] of sockets) {
2307
2283
  if (ws.readyState !== ws.OPEN) continue;
2308
- if (pushHandler) Promise.resolve(pushHandler(messageId)).catch(() => {});
2309
- else ws.send(doorbellFrame);
2284
+ Promise.resolve(pushHandler(messageId)).catch(() => {});
2310
2285
  }
2311
2286
  }
2312
2287
  return {
@@ -2316,7 +2291,7 @@ function createNotifier(listenClient) {
2316
2291
  map = /* @__PURE__ */ new Map();
2317
2292
  subscriptions.set(inboxId, map);
2318
2293
  }
2319
- map.set(ws, pushHandler ?? null);
2294
+ map.set(ws, pushHandler);
2320
2295
  },
2321
2296
  unsubscribe(inboxId, ws) {
2322
2297
  const map = subscriptions.get(inboxId);
@@ -4210,4 +4185,4 @@ async function cleanupStaleClients(db, staleSeconds = 60) {
4210
4185
  return result.length;
4211
4186
  }
4212
4187
  //#endregion
4213
- export { getSession as $, suspendSession as $t, createAgent as A, pollInbox as At, extractSummary as B, resetTimedOutEntries as Bt, claimAndBuildForPush as C, markMeChatRead as Ct, cleanupStalePresence as D, messages as Dt, cleanupStaleClients as E, members as Et, deriveAuthState as F, registerChatMessageDispatcher as Ft, getAgentAvatarImage as G, sendToAgent as Gt, findOrCreateDirectChat as H, resolveDefaultOrgId as Ht, disconnectClient as I, registerClient as It, getChatDetail as J, setChatEngagement as Jt, getCachedAudience as K, serverInstances as Kt, editMessage as L, removeParticipant as Lt, createMeChat as M, reactivateAgent as Mt, createNotifier as N, rebindAgent as Nt, clearAgentAvatarImage as O, notifyRecipients as Ot, deleteAgent as P, recomputeWatchersForMember as Pt, getPresence as Q, suspendAgent as Qt, ensureDefaultOrganization as R, renewEntry as Rt, checkAgentNameAvailability as S, listMyPinnedAgents as St, claimClient as T, markStaleAgents as Tt, getActivityOverview as U, retireClient as Ut, filterSessionsByParticipant as V, resolveChatTitle as Vt, getAgent as W, sendMessage as Wt, getOnlineCount as X, setRuntimeState as Xt, getClient as Y, setOffline as Yt, getOrganization as Z, submitAnswer as Zt, assertParticipant as _, listClients as _t, adapterAgentMappings as a, upsertSessionState as an, leaveChat as at, chatUserState as b, listMeChats as bt, addMeChatParticipants as c, listAgentSessions as ct, agentChatSessions as d, listAgentsManagedByUser as dt, touchAgent as en, heartbeatClient as et, agentConfigs as f, listAgentsWithRuntime as ft, assertClientOwner as g, listChatsForMember as gt, archiveSession as h, listChats as ht, ackEntryByIdForBoundAgents as i, updateOrganization as in, joinMeChat as it, createChat as j, pruneStaleSilentEntries as jt, clients as k, pendingQuestions as kt, addParticipant as l, listAgentsForAdmin as lt, agents as m, listChatParticipantsWithNames as mt, SUPPORTED_AVATAR_IMAGE_MIMES as n, updateAgent as nn, inboxEntries as nt, adapterConfigs as o, leaveMeChat as ot, agentPresence as p, listAllSessions as pt, getCallerEngagement as q, setAgentAvatarImage as qt, ackEntry as r, updateClientCapabilities as rn, joinChat as rt, addChatParticipants as s, listActiveAgentsPinnedToClient as st, MAX_AVATAR_IMAGE_BYTES as t, unbindAgent as tn, heartbeatInstance as tt, agentAvatarImageUrl as u, listAgentsForMember as ut, bindAgent as v, listClientsForOrgAdmin as vt, claimBacklogForPush as w, markMeChatUnread as wt, chats as x, listMessages as xt, chatMembership as y, listMeChatSourceCounts as yt, ensureParticipant as z, resetActivity as zt };
4188
+ export { heartbeatClient as $, unbindAgent as $t, createChat as A, pruneStaleSilentEntries as At, filterSessionsByParticipant as B, resolveDefaultOrgId as Bt, claimBacklogForPush as C, markMeChatUnread as Ct, clearAgentAvatarImage as D, notifyRecipients as Dt, cleanupStalePresence as E, messages as Et, disconnectClient as F, registerClient as Ft, getCachedAudience as G, setAgentAvatarImage as Gt, getActivityOverview as H, sendMessage as Ht, editMessage as I, removeParticipant as It, getClient as J, setRuntimeState as Jt, getCallerEngagement as K, setChatEngagement as Kt, ensureDefaultOrganization as L, resetActivity as Lt, createNotifier as M, rebindAgent as Mt, deleteAgent as N, recomputeWatchersForMember as Nt, clients as O, pendingQuestions as Ot, deriveAuthState as P, registerChatMessageDispatcher as Pt, getSession as Q, touchAgent as Qt, ensureParticipant as R, resetTimedOutEntries as Rt, claimAndBuildForPush as S, markMeChatRead as St, cleanupStaleClients as T, members as Tt, getAgent as U, sendToAgent as Ut, findOrCreateDirectChat as V, retireClient as Vt, getAgentAvatarImage as W, serverInstances as Wt, getOrganization as X, suspendAgent as Xt, getOnlineCount as Y, submitAnswer as Yt, getPresence as Z, suspendSession as Zt, bindAgent as _, listClientsForOrgAdmin as _t, adapterConfigs as a, leaveMeChat as at, chats as b, listMessages as bt, addParticipant as c, listAgentsForAdmin as ct, agentConfigs as d, listAgentsWithRuntime as dt, updateAgent as en, heartbeatInstance as et, agentPresence as f, listAllSessions as ft, assertParticipant as g, listClients as gt, assertClientOwner as h, listChatsForMember as ht, adapterAgentMappings as i, leaveChat as it, createMeChat as j, reactivateAgent as jt, createAgent as k, pollInbox as kt, agentAvatarImageUrl as l, listAgentsForMember as lt, archiveSession as m, listChats as mt, SUPPORTED_AVATAR_IMAGE_MIMES as n, updateOrganization as nn, joinChat as nt, addChatParticipants as o, listActiveAgentsPinnedToClient as ot, agents as p, listChatParticipantsWithNames as pt, getChatDetail as q, setOffline as qt, ackEntryByIdForBoundAgents as r, upsertSessionState as rn, joinMeChat as rt, addMeChatParticipants as s, listAgentSessions as st, MAX_AVATAR_IMAGE_BYTES as t, updateClientCapabilities as tn, inboxEntries as tt, agentChatSessions as u, listAgentsManagedByUser as ut, chatMembership as v, listMeChatSourceCounts as vt, claimClient as w, markStaleAgents as wt, checkAgentNameAvailability as x, listMyPinnedAgents as xt, chatUserState as y, listMeChats as yt, extractSummary as z, resolveChatTitle as zt };
@@ -1,7 +1,7 @@
1
1
  import "./observability-BAScT_5S-BcW9HgkG.mjs";
2
2
  import "./logger-core-BTmvdflj-DjW8FM4T.mjs";
3
- import "./dist-DmYxT5Kb.mjs";
3
+ import "./dist-CwsiHGX7.mjs";
4
4
  import "./uuid-DbS_4vFh-iFghv4zA.mjs";
5
5
  import "./src-DNBS5Yjj.mjs";
6
- import { St as listMyPinnedAgents } from "./client-CZ_VnbEc-CBF46cJd.mjs";
6
+ import { xt as listMyPinnedAgents } from "./client-BSfCc0pJ-BP_1f21y.mjs";
7
7
  export { listMyPinnedAgents };
@@ -803,9 +803,13 @@ z.object({
803
803
  * Optional opt-in flags the client carries on `client:register` to advertise
804
804
  * which negotiable wire-protocol features it implements. Distinct from
805
805
  * `clientCapabilitiesSchema` (per-runtime-provider availability — different
806
- * concept). Older clients omit the field; the server treats every unset flag
807
- * as `false` and falls back to the legacy path. See proposal
808
- * hub-inbox-ws-data-plane §3.6.
806
+ * concept).
807
+ *
808
+ * 0.10.4 ~ 0.14.2 clients still send this block (with `wsInboxDeliver: true`
809
+ * hard-coded). The 0.14.3+ runtime omits it. The schema is retained so that
810
+ * middle-version `client:register` frames still parse, even though the
811
+ * server no longer reads any of these fields — the WS inbox data plane is
812
+ * mandatory on this server build.
809
813
  */
810
814
  const clientWireCapabilitiesSchema = z.object({ wsInboxDeliver: z.boolean().default(false) }).partial();
811
815
  const clientRegisterSchema = z.object({
@@ -1196,14 +1200,11 @@ z.object({
1196
1200
  }).extend({ message: clientMessageSchema });
1197
1201
  const inboxPollQuerySchema = z.object({ limit: z.coerce.number().int().min(1).max(50).default(10) });
1198
1202
  /**
1199
- * server → client: a single inbox entry pushed over the active WS connection,
1200
- * replacing the legacy `new_message` doorbell + HTTP `/inbox` poll round-trip.
1203
+ * server → client: a single inbox entry pushed over the active WS connection.
1201
1204
  *
1202
1205
  * `entryId` is the server-side `inbox_entries.id` the client must echo back
1203
- * in `inbox:ack`. `message` is exactly what the legacy poll path returned —
1204
- * `clientMessageSchema` already carries `precedingMessages`, so the client-
1205
- * side dispatch logic is reused verbatim (see proposal
1206
- * hub-inbox-ws-data-plane §3.1).
1206
+ * in `inbox:ack`. `clientMessageSchema` carries `precedingMessages`, so the
1207
+ * client-side dispatch logic handles the silent-context bundle uniformly.
1207
1208
  *
1208
1209
  * `.passthrough()` so a forward-rolling server may extend the frame without
1209
1210
  * breaking older clients that validate strictly. Older clients drop unknown
@@ -1217,8 +1218,7 @@ const inboxDeliverFrameSchema = z.object({
1217
1218
  message: clientMessageSchema
1218
1219
  }).passthrough();
1219
1220
  /**
1220
- * client → server: ack for an `inbox:deliver` frame. Replaces the legacy
1221
- * `POST /inbox/:id/ack` HTTP endpoint when the WS data plane is active.
1221
+ * client → server: ack for an `inbox:deliver` frame.
1222
1222
  */
1223
1223
  const inboxAckFrameSchema = z.object({
1224
1224
  type: z.literal("inbox:ack"),
@@ -1994,8 +1994,13 @@ const WS_AUTH_FRAME_TIMEOUT_MS = 5e3;
1994
1994
  /**
1995
1995
  * Negotiable wire-protocol features the server advertises in its `welcome`
1996
1996
  * frame. Older clients drop the `capabilities` field silently because the
1997
- * frame is `.passthrough()`. New clients gate optional code paths on it —
1998
- * absent ⇒ feature off, never assumed.
1997
+ * frame is `.passthrough()`.
1998
+ *
1999
+ * Required by clients in the 0.10.4 ~ 0.14.2 range: those builds read
2000
+ * `wsInboxDeliver` here to decide whether to skip the local HTTP poll loop
2001
+ * and rely on `inbox:deliver` push frames. The 0.14.3+ runtime ignores the
2002
+ * field (push is the only path) but the server still emits it so middle-
2003
+ * version clients keep working.
1999
2004
  */
2000
2005
  const serverCapabilitiesSchema = z.object({ wsInboxDeliver: z.boolean().default(false) }).partial();
2001
2006
  z.object({
@@ -1,6 +1,6 @@
1
1
  import { r as __exportAll } from "./chunk-BSw8zbkd.mjs";
2
2
  import { t as cliFetch } from "./cli-fetch--tiwKm5S.mjs";
3
- import { r as AGENT_SELECTOR_HEADER } from "./dist-DmYxT5Kb.mjs";
3
+ import { r as AGENT_SELECTOR_HEADER } from "./dist-CwsiHGX7.mjs";
4
4
  //#region src/core/feishu.ts
5
5
  var feishu_exports = /* @__PURE__ */ __exportAll({
6
6
  bindFeishuBot: () => bindFeishuBot,
package/dist/index.mjs CHANGED
@@ -1,12 +1,12 @@
1
1
  import "./observability-BAScT_5S-BcW9HgkG.mjs";
2
- import { A as checkDocker, B as isServiceSupported, E as checkAgentConfigs, F as checkWebSocket, G as uninstallClientService, H as restartClientService, I as printResults, J as stopPostgres, K as ensurePostgres, M as checkServerConfig, N as checkServerHealth, O as checkClientConfig, P as checkServerReachable, R as getClientServiceStatus, S as runHomeMigration, T as runMigrations, U as startClientService, V as resolveCliInvocation, W as stopClientService, X as handleClientOrgMismatch, Y as ClientRuntime, Z as rotateClientIdWithBackup, _ as formatCheckReport, b as onboardCreate, d as startServer, g as promptMissingFields, h as promptAddAgent, j as checkNodeVersion, k as checkDatabase, lt as FirstTreeHubSDK, m as isInteractive, n as deriveHubUrlFromToken, nt as hasUser, q as isDockerAvailable, t as HubUrlDerivationError, tt as createOwner, ut as SdkError, y as onboardCheck, z as installClientService } from "./saas-connect-DgCSZ8Yk.mjs";
2
+ import { A as checkDocker, B as isServiceSupported, E as checkAgentConfigs, F as checkWebSocket, G as uninstallClientService, H as restartClientService, I as printResults, J as stopPostgres, K as ensurePostgres, M as checkServerConfig, N as checkServerHealth, O as checkClientConfig, P as checkServerReachable, R as getClientServiceStatus, S as runHomeMigration, T as runMigrations, U as startClientService, V as resolveCliInvocation, W as stopClientService, X as handleClientOrgMismatch, Y as ClientRuntime, Z as rotateClientIdWithBackup, _ as formatCheckReport, b as onboardCreate, d as startServer, g as promptMissingFields, h as promptAddAgent, j as checkNodeVersion, k as checkDatabase, lt as FirstTreeHubSDK, m as isInteractive, n as deriveHubUrlFromToken, nt as hasUser, q as isDockerAvailable, t as HubUrlDerivationError, tt as createOwner, ut as SdkError, y as onboardCheck, z as installClientService } from "./saas-connect-ChxZv2YQ.mjs";
3
3
  import "./logger-core-BTmvdflj-DjW8FM4T.mjs";
4
- import { a as ensureFreshAdminToken, c as resolveServerUrl, i as ensureFreshAccessToken, n as AuthRefreshRateLimitedError, s as resolveAccessToken, t as AuthRefreshFailedError } from "./bootstrap-C15ZBOCC.mjs";
4
+ import { a as ensureFreshAdminToken, c as resolveServerUrl, i as ensureFreshAccessToken, n as AuthRefreshRateLimitedError, s as resolveAccessToken, t as AuthRefreshFailedError } from "./bootstrap-CQQGgIx1.mjs";
5
5
  import { i as blank, s as status } from "./cli-fetch--tiwKm5S.mjs";
6
- import "./dist-DmYxT5Kb.mjs";
7
- import { n as bindFeishuUser, t as bindFeishuBot } from "./feishu-CCWd-JE4.mjs";
6
+ import "./dist-CwsiHGX7.mjs";
7
+ import { n as bindFeishuUser, t as bindFeishuBot } from "./feishu-DHSy6WDD.mjs";
8
8
  import "./uuid-DbS_4vFh-iFghv4zA.mjs";
9
9
  import "./src-DNBS5Yjj.mjs";
10
- import "./client-CZ_VnbEc-CBF46cJd.mjs";
10
+ import "./client-BSfCc0pJ-BP_1f21y.mjs";
11
11
  import "./invitation-D_ENPHyj-5ETiae5r.mjs";
12
12
  export { AuthRefreshFailedError, AuthRefreshRateLimitedError, ClientRuntime, FirstTreeHubSDK, HubUrlDerivationError, SdkError, bindFeishuBot, bindFeishuUser, blank, checkAgentConfigs, checkClientConfig, checkDatabase, checkDocker, checkNodeVersion, checkServerConfig, checkServerHealth, checkServerReachable, checkWebSocket, createOwner, deriveHubUrlFromToken, ensureFreshAccessToken, ensureFreshAdminToken, ensurePostgres, formatCheckReport, getClientServiceStatus, handleClientOrgMismatch, hasUser, installClientService, isDockerAvailable, isInteractive, isServiceSupported, onboardCheck, onboardCreate, printResults, promptAddAgent, promptMissingFields, resolveAccessToken, resolveCliInvocation, resolveServerUrl, restartClientService, rotateClientIdWithBackup, runHomeMigration, runMigrations, startClientService, startServer, status, stopClientService, stopPostgres, uninstallClientService };
@@ -1,4 +1,4 @@
1
- import "./dist-DmYxT5Kb.mjs";
1
+ import "./dist-CwsiHGX7.mjs";
2
2
  import "./uuid-DbS_4vFh-iFghv4zA.mjs";
3
3
  import { s as previewInvitation } from "./invitation-D_ENPHyj-5ETiae5r.mjs";
4
4
  export { previewInvitation };
@@ -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-CwsiHGX7.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 heartbeatClient, $t as unbindAgent, A as createChat, At as pruneStaleSilentEntries, B as filterSessionsByParticipant, Bt as resolveDefaultOrgId$1, C as claimBacklogForPush, Ct as markMeChatUnread, D as clearAgentAvatarImage, Dt as notifyRecipients, E as cleanupStalePresence, Et as messages, F as disconnectClient, Ft as registerClient, G as getCachedAudience, Gt as setAgentAvatarImage, H as getActivityOverview, Ht as sendMessage, I as editMessage, It as removeParticipant, J as getClient, Jt as setRuntimeState, K as getCallerEngagement, Kt as setChatEngagement, L as ensureDefaultOrganization, Lt as resetActivity, M as createNotifier, Mt as rebindAgent, N as deleteAgent, Nt as recomputeWatchersForMember, O as clients, Ot as pendingQuestions, P as deriveAuthState, Pt as registerChatMessageDispatcher, Q as getSession, Qt as touchAgent, R as ensureParticipant, Rt as resetTimedOutEntries, S as claimAndBuildForPush, St as markMeChatRead, T as cleanupStaleClients, Tt as members, U as getAgent, Ut as sendToAgent$1, V as findOrCreateDirectChat, Vt as retireClient, W as getAgentAvatarImage, Wt as serverInstances, X as getOrganization, Xt as suspendAgent, Y as getOnlineCount, Yt as submitAnswer, Z as getPresence, Zt as suspendSession, _ as bindAgent, _t as listClientsForOrgAdmin, a as adapterConfigs, at as leaveMeChat, b as chats, bt as listMessages, c as addParticipant, ct as listAgentsForAdmin, d as agentConfigs, dt as listAgentsWithRuntime, en as updateAgent, et as heartbeatInstance, f as agentPresence, ft as listAllSessions, g as assertParticipant, gt as listClients, h as assertClientOwner, ht as listChatsForMember, i as adapterAgentMappings, it as leaveChat, j as createMeChat, jt as reactivateAgent, k as createAgent, kt as pollInbox, l as agentAvatarImageUrl, lt as listAgentsForMember, m as archiveSession, mt as listChats, n as SUPPORTED_AVATAR_IMAGE_MIMES, nn as updateOrganization, nt as joinChat, o as addChatParticipants, ot as listActiveAgentsPinnedToClient, p as agents, pt as listChatParticipantsWithNames, q as getChatDetail, qt as setOffline, r as ackEntryByIdForBoundAgents, rn as upsertSessionState, rt as joinMeChat, s as addMeChatParticipants, st as listAgentSessions, t as MAX_AVATAR_IMAGE_BYTES, tn as updateClientCapabilities, tt as inboxEntries, u as agentChatSessions, ut as listAgentsManagedByUser, v as chatMembership, vt as listMeChatSourceCounts, w as claimClient, wt as markStaleAgents, x as checkAgentNameAvailability, y as chatUserState, yt as listMeChats, z as extractSummary, zt as resolveChatTitle } from "./client-BSfCc0pJ-BP_1f21y.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";
@@ -887,9 +887,13 @@ z.object({
887
887
  * Optional opt-in flags the client carries on `client:register` to advertise
888
888
  * which negotiable wire-protocol features it implements. Distinct from
889
889
  * `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.
890
+ * concept).
891
+ *
892
+ * 0.10.4 ~ 0.14.2 clients still send this block (with `wsInboxDeliver: true`
893
+ * hard-coded). The 0.14.3+ runtime omits it. The schema is retained so that
894
+ * middle-version `client:register` frames still parse, even though the
895
+ * server no longer reads any of these fields — the WS inbox data plane is
896
+ * mandatory on this server build.
893
897
  */
894
898
  const clientWireCapabilitiesSchema = z.object({ wsInboxDeliver: z.boolean().default(false) }).partial();
895
899
  z.object({
@@ -1114,8 +1118,8 @@ z.object({
1114
1118
  });
1115
1119
  /**
1116
1120
  * 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.
1121
+ * message. Pushed before the corresponding `inbox:deliver` frame so the
1122
+ * client has the file on disk by the time it renders the message.
1119
1123
  *
1120
1124
  * Best-effort: if the target client WS lives on a different server
1121
1125
  * instance (or is offline), the frame is lost and the reference message
@@ -1276,14 +1280,11 @@ z.object({
1276
1280
  }).extend({ message: clientMessageSchema });
1277
1281
  z.object({ limit: z.coerce.number().int().min(1).max(50).default(10) });
1278
1282
  /**
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.
1283
+ * server → client: a single inbox entry pushed over the active WS connection.
1281
1284
  *
1282
1285
  * `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).
1286
+ * in `inbox:ack`. `clientMessageSchema` carries `precedingMessages`, so the
1287
+ * client-side dispatch logic handles the silent-context bundle uniformly.
1287
1288
  *
1288
1289
  * `.passthrough()` so a forward-rolling server may extend the frame without
1289
1290
  * breaking older clients that validate strictly. Older clients drop unknown
@@ -2015,8 +2016,13 @@ z.object({
2015
2016
  /**
2016
2017
  * Negotiable wire-protocol features the server advertises in its `welcome`
2017
2018
  * 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.
2019
+ * frame is `.passthrough()`.
2020
+ *
2021
+ * Required by clients in the 0.10.4 ~ 0.14.2 range: those builds read
2022
+ * `wsInboxDeliver` here to decide whether to skip the local HTTP poll loop
2023
+ * and rely on `inbox:deliver` push frames. The 0.14.3+ runtime ignores the
2024
+ * field (push is the only path) but the server still emits it so middle-
2025
+ * version clients keep working.
2020
2026
  */
2021
2027
  const serverCapabilitiesSchema = z.object({ wsInboxDeliver: z.boolean().default(false) }).partial();
2022
2028
  /**
@@ -2145,13 +2151,6 @@ defineConfig({
2145
2151
  refreshTokenExpiry: field(z.string().default("30d"), { env: "FIRST_TREE_HUB_AUTH_REFRESH_TOKEN_EXPIRY" }),
2146
2152
  connectTokenExpiry: field(z.string().default("10m"), { env: "FIRST_TREE_HUB_AUTH_CONNECT_TOKEN_EXPIRY" })
2147
2153
  },
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
2154
  oauth: optional({ githubApp: optional({
2156
2155
  appId: field(z.string().min(1), { env: "FIRST_TREE_HUB_GITHUB_APP_ID" }),
2157
2156
  clientId: field(z.string().min(1), { env: "FIRST_TREE_HUB_GITHUB_APP_CLIENT_ID" }),
@@ -2407,15 +2406,6 @@ var FirstTreeHubSDK = class {
2407
2406
  return false;
2408
2407
  }
2409
2408
  }
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
2409
  async sendMessage(chatId, data) {
2420
2410
  return this.requestJson(`/api/v1/agent/chats/${chatId}/messages`, {
2421
2411
  method: "POST",
@@ -2583,17 +2573,6 @@ const RECONNECT_MAX_MS = 3e4;
2583
2573
  const WS_CONNECT_TIMEOUT_MS = 1e4;
2584
2574
  const HEARTBEAT_INTERVAL_MS = 3e4;
2585
2575
  /**
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
2576
  * Unified-user-token C5: reconnect PROACTIVELY this many ms before the JWT's
2598
2577
  * `exp` claim so the client rotates to a fresh JWT without ever hitting the
2599
2578
  * server-side `auth:expired` push. The provider's next `getAccessToken()` call
@@ -2653,15 +2632,6 @@ var ClientConnection = class extends EventEmitter {
2653
2632
  /** Count of `server:welcome` frames received; drives `isReconnect` flag. */
2654
2633
  welcomeFramesReceived = 0;
2655
2634
  /**
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
2635
  * Last handshake error, stashed for the `close` handler to surface a typed
2666
2636
  * reason (e.g. {@link ClientOrgMismatchError}) instead of a generic
2667
2637
  * "closed before ready" when `connect()` is pending.
@@ -2674,11 +2644,11 @@ var ClientConnection = class extends EventEmitter {
2674
2644
  desiredBindings = /* @__PURE__ */ new Map();
2675
2645
  pendingBinds = /* @__PURE__ */ new Map();
2676
2646
  /**
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.
2647
+ * In-flight image writes from recent `image_payload` frames. `image_payload`
2648
+ * arrives on the WS just before `inbox:deliver` for the same message, but
2649
+ * the EventEmitter dispatch is sync so without gating, the deliver
2650
+ * handler can fire before the image bytes hit disk. Block `inbox:deliver`
2651
+ * emission until these settle.
2682
2652
  */
2683
2653
  pendingImageWrites = /* @__PURE__ */ new Set();
2684
2654
  constructor(config) {
@@ -2699,24 +2669,21 @@ var ClientConnection = class extends EventEmitter {
2699
2669
  return this.boundAgents;
2700
2670
  }
2701
2671
  /**
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.
2672
+ * Ack a delivered inbox entry over the WS data plane. Safe to call when the
2673
+ * WS is closed the frame is dropped (logged) and the entry will time out
2674
+ * server-side and re-deliver on reconnect. The handler has by then already
2675
+ * started processing, so reaper-driven redelivery surfaces as a duplicate
2676
+ * dispatch on the next connect; SessionManager's dedupe key
2677
+ * `(chatId, messageId)` collapses it.
2717
2678
  */
2718
2679
  sendInboxAck(entryId) {
2719
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
2680
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
2681
+ this.wsLogger.warn({
2682
+ entryId,
2683
+ readyState: this.ws?.readyState
2684
+ }, "inbox:ack dropped — socket not OPEN");
2685
+ return;
2686
+ }
2720
2687
  this.ws.send(JSON.stringify({
2721
2688
  type: "inbox:ack",
2722
2689
  entryId
@@ -2873,7 +2840,6 @@ var ClientConnection = class extends EventEmitter {
2873
2840
  this.clearAuthRefreshTimer();
2874
2841
  const wasRegistered = this.registered;
2875
2842
  this.registered = false;
2876
- this.wsInboxDeliverActive = false;
2877
2843
  this.rejectAllPendingBinds("WebSocket closed");
2878
2844
  if (!settled) {
2879
2845
  this.wsLogger.warn({ code }, "closed before ready");
@@ -2909,8 +2875,7 @@ var ClientConnection = class extends EventEmitter {
2909
2875
  clientId: this.clientId,
2910
2876
  hostname: hostname(),
2911
2877
  os: platform(),
2912
- sdkVersion: this.sdkVersion,
2913
- wireCapabilities: { wsInboxDeliver: WS_INBOX_DELIVER_OPT_IN }
2878
+ sdkVersion: this.sdkVersion
2914
2879
  }));
2915
2880
  return;
2916
2881
  }
@@ -2920,7 +2885,6 @@ var ClientConnection = class extends EventEmitter {
2920
2885
  this.wsLogger.warn({ issues: parsed.error.issues.map((i) => i.message) }, "ignoring malformed server:welcome frame");
2921
2886
  return;
2922
2887
  }
2923
- this.wsInboxDeliverActive = parsed.data.capabilities?.wsInboxDeliver === true && WS_INBOX_DELIVER_OPT_IN;
2924
2888
  const isReconnect = this.welcomeFramesReceived > 0;
2925
2889
  this.welcomeFramesReceived++;
2926
2890
  this.emit("server:welcome", {
@@ -3058,15 +3022,6 @@ var ClientConnection = class extends EventEmitter {
3058
3022
  write.finally(() => this.pendingImageWrites.delete(write));
3059
3023
  return;
3060
3024
  }
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
3025
  if (type === "inbox:deliver") {
3071
3026
  const parsed = inboxDeliverFrameSchema.safeParse(msg);
3072
3027
  if (!parsed.success) {
@@ -6812,18 +6767,13 @@ var SessionManager = class {
6812
6767
  this.config.onStateChange(chatId, state);
6813
6768
  }
6814
6769
  /**
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).
6770
+ * ACK an inbox entry — delayed until handler starts processing. Routes
6771
+ * through `config.ackEntry`, which is wired to the WS data plane.
6821
6772
  */
6822
6773
  async ackEntry(entryId, chatId) {
6823
6774
  if (entryId === void 0) return;
6824
6775
  try {
6825
- if (this.config.ackEntry) await this.config.ackEntry(entryId);
6826
- else await this.config.sdk.ack(entryId);
6776
+ await this.config.ackEntry(entryId);
6827
6777
  } catch {
6828
6778
  this.config.log.warn({
6829
6779
  chatId,
@@ -6976,9 +6926,7 @@ var AgentSlot = class {
6976
6926
  sessionManager = null;
6977
6927
  config;
6978
6928
  logger;
6979
- sdk = null;
6980
6929
  agentConfigCache = null;
6981
- pollingTimer = null;
6982
6930
  reconcileTimer = null;
6983
6931
  listeners = [];
6984
6932
  /**
@@ -7016,7 +6964,6 @@ var AgentSlot = class {
7016
6964
  }
7017
6965
  async start(contextTreeBinding) {
7018
6966
  const sdk = (await this.clientConnection.bindAgent(this.config.agentId, this.config.runtimeType ?? this.config.type, this.config.runtimeVersion)).sdk;
7019
- this.sdk = sdk;
7020
6967
  const agent = await sdk.register();
7021
6968
  this.logger.info({ displayName: agent.displayName }, "agent bound");
7022
6969
  if (agent.type === "human") {
@@ -7036,9 +6983,6 @@ var AgentSlot = class {
7036
6983
  throw new Error(`Hub unreachable while loading agent config: ${msg}`);
7037
6984
  }
7038
6985
  this.inboxId = agent.inboxId;
7039
- const onMessage = (agentId) => {
7040
- if (agentId === this.config.agentId) this.pullAndDispatch();
7041
- };
7042
6986
  const onInboxDeliver = (inboxId, frame) => {
7043
6987
  if (inboxId !== this.inboxId) return;
7044
6988
  this.dispatchPushedFrame(frame).catch((err) => {
@@ -7057,14 +7001,10 @@ var AgentSlot = class {
7057
7001
  const onReconcileResult = (result) => {
7058
7002
  if (result.agentId === this.config.agentId && this.sessionManager) this.sessionManager.applyStaleChatIds(result.staleChatIds);
7059
7003
  };
7060
- this.clientConnection.on("agent:message", onMessage);
7061
7004
  this.clientConnection.on("inbox:deliver", onInboxDeliver);
7062
7005
  this.clientConnection.on("agent:bound", onBound);
7063
7006
  this.clientConnection.on("session:reconcile:result", onReconcileResult);
7064
7007
  this.listeners.push({
7065
- event: "agent:message",
7066
- fn: onMessage
7067
- }, {
7068
7008
  event: "inbox:deliver",
7069
7009
  fn: onInboxDeliver
7070
7010
  }, {
@@ -7082,10 +7022,10 @@ var AgentSlot = class {
7082
7022
  agentId: this.config.agentId
7083
7023
  })
7084
7024
  });
7085
- const ackEntry = this.clientConnection.supportsWsInboxDeliver ? (entryId) => {
7025
+ const ackEntry = (entryId) => {
7086
7026
  this.clientConnection.sendInboxAck(entryId);
7087
7027
  return Promise.resolve();
7088
- } : void 0;
7028
+ };
7089
7029
  this.sessionManager = new SessionManager({
7090
7030
  session: this.config.session,
7091
7031
  concurrency: this.config.concurrency,
@@ -7128,24 +7068,15 @@ var AgentSlot = class {
7128
7068
  event: "session:command",
7129
7069
  fn: onCommand
7130
7070
  });
7131
- this.startPolling();
7132
7071
  this.startReconcileLoop();
7133
7072
  return agent;
7134
7073
  }
7135
7074
  async stop() {
7136
- if (this.pollingTimer) {
7137
- clearInterval(this.pollingTimer);
7138
- this.pollingTimer = null;
7139
- }
7140
7075
  if (this.reconcileTimer) {
7141
7076
  clearInterval(this.reconcileTimer);
7142
7077
  this.reconcileTimer = null;
7143
7078
  }
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);
7079
+ for (const entry of this.listeners) this.clientConnection.off(entry.event, entry.fn);
7149
7080
  this.listeners = [];
7150
7081
  await this.clientConnection.unbindAgent(this.config.agentId);
7151
7082
  await this.sessionManager?.shutdown();
@@ -7166,31 +7097,18 @@ var AgentSlot = class {
7166
7097
  const runtimeState = this.sessionManager.getAggregateRuntimeState();
7167
7098
  if (runtimeState) this.clientConnection.reportRuntimeState(this.config.agentId, runtimeState);
7168
7099
  }
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
7100
  /**
7180
7101
  * Translate an `inbox:deliver` push frame into the {@link InboxEntryWithMessage}
7181
7102
  * shape `SessionManager.dispatch` expects, then dispatch.
7182
7103
  *
7183
7104
  * 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.
7105
+ * construction time — `clientConnection.sendInboxAck`. Sending an additional
7106
+ * ack here would double-ack: a WS frame the server cannot match against any
7107
+ * `delivered` row, which leaks the server's per-agent in-flight counter and
7108
+ * stalls push after `inboxMaxInFlightPerAgent` messages.
7190
7109
  *
7191
7110
  * 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).
7111
+ * and the 300s timeout reaper rolls it back to `pending` for replay.
7194
7112
  */
7195
7113
  async dispatchPushedFrame(frame) {
7196
7114
  if (!this.sessionManager) return;
@@ -7218,15 +7136,6 @@ var AgentSlot = class {
7218
7136
  if (chatIds.length === 0) return;
7219
7137
  this.clientConnection.sendSessionReconcile(this.config.agentId, chatIds);
7220
7138
  }
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
7139
  };
7231
7140
  /**
7232
7141
  * Top-level marker file Claude Code writes after a successful OAuth login.
@@ -9853,7 +9762,7 @@ async function onboardCreate(args) {
9853
9762
  }
9854
9763
  const runtimeAgent = args.type === "human" ? args.assistant : args.id;
9855
9764
  if (args.feishuBotAppId && args.feishuBotAppSecret) {
9856
- const { bindFeishuBot } = await import("./feishu-CCWd-JE4.mjs").then((n) => n.r);
9765
+ const { bindFeishuBot } = await import("./feishu-DHSy6WDD.mjs").then((n) => n.r);
9857
9766
  const targetAgentUuid = args.type === "human" ? assistantUuid : primary.uuid;
9858
9767
  if (!targetAgentUuid) print.line(`Warning: Cannot bind Feishu bot — no runtime agent available for "${args.id}".\n`);
9859
9768
  else {
@@ -11066,7 +10975,7 @@ function createFeedbackHandler(config) {
11066
10975
  return { handle };
11067
10976
  }
11068
10977
  //#endregion
11069
- //#region ../server/dist/app-Cv337jed.mjs
10978
+ //#region ../server/dist/app-z2-jRXbZ.mjs
11070
10979
  var import_fastify_opentelemetry = /* @__PURE__ */ __toESM(require_fastify_opentelemetry(), 1);
11071
10980
  init_esm();
11072
10981
  var __defProp = Object.defineProperty;
@@ -11844,18 +11753,6 @@ async function agentInboxRoutes(app) {
11844
11753
  const query = inboxPollQuerySchema.parse(request.query);
11845
11754
  return await pollInbox(app.db, identity.inboxId, query.limit);
11846
11755
  });
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
11756
  }
11860
11757
  async function agentMeRoutes(app) {
11861
11758
  app.get("/me", async (request) => {
@@ -11887,11 +11784,11 @@ async function agentMeRoutes(app) {
11887
11784
  * {imageId, mimeType, filename, size}
11888
11785
  *
11889
11786
  * 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.
11787
+ * send buffer synchronously, which is the only ordering guarantee we need
11788
+ * the subsequent `inbox:deliver` frame is driven by a PG NOTIFY round trip,
11789
+ * so the image lands first on the wire. Awaiting the TCP flush here would
11790
+ * put a slow subscriber's backpressure on the sender's HTTP response for a
11791
+ * feature that is already best-effort.
11895
11792
  *
11896
11793
  * Non-image messages are returned unchanged. Missing-subscriber / wrong-
11897
11794
  * instance cases are acceptable loss per the image-out-of-messages design
@@ -12588,8 +12485,7 @@ async function summarizeContextTreeUsage(db, organizationId, windowDays) {
12588
12485
  /**
12589
12486
  * Default per-agent in-flight cap when `server.inbox.maxInFlightPerAgent` is
12590
12487
  * 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.
12488
+ * `inbox` block still gets reasonable backpressure.
12593
12489
  */
12594
12490
  const DEFAULT_INBOX_MAX_IN_FLIGHT_PER_AGENT = 32;
12595
12491
  /**
@@ -12634,23 +12530,12 @@ function clientWsRoutes(notifier, instanceId) {
12634
12530
  let authExpiryTimer = null;
12635
12531
  const boundAgents = /* @__PURE__ */ new Map();
12636
12532
  /**
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
12533
  * Per-agent in-flight `inbox:deliver` counter for backpressure. Lives on
12646
12534
  * the socket — when the WS closes it goes with it; that's intentional,
12647
12535
  * because re-counting on a fresh connection would bias the cap against
12648
- * a healthy reconnect (proposal §3.5).
12536
+ * a healthy reconnect.
12649
12537
  */
12650
12538
  const inboxInFlight = /* @__PURE__ */ new Map();
12651
- function pushUseWsDataPlane() {
12652
- return clientWantsWsInboxDeliver;
12653
- }
12654
12539
  /**
12655
12540
  * Returns `false` when the socket has already moved out of `OPEN` —
12656
12541
  * the only failure mode the caller can observe synchronously.
@@ -12893,7 +12778,6 @@ function clientWsRoutes(notifier, instanceId) {
12893
12778
  try {
12894
12779
  if (type === "client:register") {
12895
12780
  const data = clientRegisterSchema.parse(msg);
12896
- clientWantsWsInboxDeliver = data.wireCapabilities?.wsInboxDeliver === true;
12897
12781
  let placeholderOrgId = jwtDefaultOrgId;
12898
12782
  if (!placeholderOrgId) {
12899
12783
  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 +12915,7 @@ function clientWsRoutes(notifier, instanceId) {
13031
12915
  inboxId: agent.inboxId,
13032
12916
  organizationId: agent.organizationId
13033
12917
  });
13034
- const wsPushActive = pushUseWsDataPlane();
13035
- if (wsPushActive) notifier.subscribe(agent.inboxId, socket, makeInboxPushHandler(agent.id, agent.inboxId));
13036
- else notifier.subscribe(agent.inboxId, socket);
12918
+ notifier.subscribe(agent.inboxId, socket, makeInboxPushHandler(agent.id, agent.inboxId));
13037
12919
  socket.send(JSON.stringify({
13038
12920
  type: "agent:bound",
13039
12921
  ref,
@@ -13041,7 +12923,7 @@ function clientWsRoutes(notifier, instanceId) {
13041
12923
  displayName: agent.displayName,
13042
12924
  agentType: agent.type
13043
12925
  }));
13044
- if (wsPushActive) drainBacklogForAgent(agent.id, agent.inboxId).catch((err) => {
12926
+ drainBacklogForAgent(agent.id, agent.inboxId).catch((err) => {
13045
12927
  app.log.error({
13046
12928
  err,
13047
12929
  agentId: agent.id
@@ -13162,6 +13044,14 @@ function clientWsRoutes(notifier, instanceId) {
13162
13044
  } else if (type === "inbox:ack") {
13163
13045
  const payloadResult = inboxAckFrameSchema.safeParse(msg);
13164
13046
  if (!payloadResult.success) {
13047
+ app.log.warn({
13048
+ clientId,
13049
+ issues: payloadResult.error.issues.map((i) => ({
13050
+ path: i.path.join("."),
13051
+ code: i.code,
13052
+ message: i.message
13053
+ }))
13054
+ }, "malformed inbox:ack frame — replying error");
13165
13055
  socket.send(JSON.stringify({
13166
13056
  type: "error",
13167
13057
  message: "Malformed inbox:ack frame"
@@ -13171,7 +13061,14 @@ function clientWsRoutes(notifier, instanceId) {
13171
13061
  const { entryId } = payloadResult.data;
13172
13062
  try {
13173
13063
  const ackedEntry = await ackEntryByIdForBoundAgents(app.db, entryId, [...boundAgents.values()].map((a) => a.inboxId));
13174
- if (!ackedEntry) return;
13064
+ if (!ackedEntry) {
13065
+ app.log.debug({
13066
+ clientId,
13067
+ entryId,
13068
+ boundInboxes: boundAgents.size
13069
+ }, "inbox:ack matched no row — stale ack or reaper race");
13070
+ return;
13071
+ }
13175
13072
  const owner = [...boundAgents.values()].find((a) => a.inboxId === ackedEntry.inboxId);
13176
13073
  if (owner) {
13177
13074
  inboxInFlight.set(owner.agentId, Math.max(0, (inboxInFlight.get(owner.agentId) ?? 1) - 1));
@@ -13859,6 +13756,7 @@ const APP_JWT_EXPIRY = "9m";
13859
13756
  * caller's side; the docs recommend 60 seconds. We mirror that.
13860
13757
  */
13861
13758
  const APP_JWT_IAT_SKEW_SECONDS = 60;
13759
+ const APP_INSTALLATION_TOKEN_URL = (id) => `https://api.github.com/app/installations/${id}/access_tokens`;
13862
13760
  const APP_INSTALLATION_URL = (id) => `https://api.github.com/app/installations/${id}`;
13863
13761
  const OAUTH_TOKEN_URL = "https://github.com/login/oauth/access_token";
13864
13762
  const OAUTH_AUTHORIZE_URL = "https://github.com/login/oauth/authorize";
@@ -13963,6 +13861,39 @@ async function listUserAccessibleInstallationIds(userAccessToken, opts = {}) {
13963
13861
  return out;
13964
13862
  }
13965
13863
  /**
13864
+ * Mint a per-installation token (server-to-server). The token is cheap
13865
+ * (one signature + one HTTP round-trip) and the upstream TTL is ~1h, so
13866
+ * the recommended caller pattern is "mint per request" rather than caching
13867
+ * — caching forces the caller to also track expiry, suspended state, and
13868
+ * GitHub-side permission churn, which the design explicitly punts to Phase
13869
+ * 4. We give callers a typed result and let them cache if profiling shows
13870
+ * the round-trip is hot.
13871
+ *
13872
+ * Throws `GithubAppApiError` on non-2xx. 401 means the App JWT is bad or
13873
+ * the App's key has been rotated upstream; 404 means the installation
13874
+ * was uninstalled. Callers SHOULD persist the suspension state when 403
13875
+ * comes back with `suspended` (the design tracks this as `suspended_at`
13876
+ * on `github_app_installations`).
13877
+ */
13878
+ async function mintInstallationToken(appJwt, installationId, opts = {}) {
13879
+ const res = await (opts.fetcher ?? fetch)(APP_INSTALLATION_TOKEN_URL(installationId), {
13880
+ method: "POST",
13881
+ headers: {
13882
+ Authorization: `Bearer ${appJwt}`,
13883
+ Accept: "application/vnd.github+json",
13884
+ "X-GitHub-Api-Version": "2022-11-28"
13885
+ }
13886
+ });
13887
+ if (!res.ok) throw new GithubAppApiError(res.status, `GitHub App installation-token request failed (${res.status})`);
13888
+ const body = await res.json();
13889
+ return {
13890
+ token: body.token,
13891
+ expiresAt: body.expires_at,
13892
+ permissions: body.permissions ?? {},
13893
+ repositorySelection: body.repository_selection ?? "all"
13894
+ };
13895
+ }
13896
+ /**
13966
13897
  * Trade an expiring user-to-server access token for a fresh pair using
13967
13898
  * its refresh token. Thrown on:
13968
13899
  * - Network / 5xx — `GithubAppApiError(status, …)`
@@ -15695,6 +15626,26 @@ function normalizeRemoteRepoUrl(value) {
15695
15626
  if (/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+(?:\.git)?$/.test(value)) return `https://github.com/${value}`;
15696
15627
  return value;
15697
15628
  }
15629
+ /**
15630
+ * Whether this binding actually drives a GitHub-hosted remote fetch — the
15631
+ * only case where minting a GitHub App installation token is meaningful.
15632
+ *
15633
+ * Returns false when:
15634
+ * - `localPath` is set (sync code short-circuits to the local checkout
15635
+ * before ever looking at `repo`)
15636
+ * - `repo` is missing
15637
+ * - `repo` is a file:// URL, a non-GitHub HTTPS URL, or otherwise
15638
+ * unparseable
15639
+ *
15640
+ * Used by the snapshot routes to gate the "install the GitHub App"
15641
+ * guidance — without this gate, every unavailable snapshot (missing repo,
15642
+ * bad branch, …) gets a misleading App-install hint appended.
15643
+ */
15644
+ function isGithubRemoteBinding(binding) {
15645
+ if (binding.localPath && binding.localPath.trim().length > 0) return false;
15646
+ if (!binding.repo) return false;
15647
+ return isGithubHttpsRepo(normalizeRemoteRepoUrl(binding.repo));
15648
+ }
15698
15649
  function managedContextTreeCacheRoot() {
15699
15650
  return join(DEFAULT_DATA_DIR$1, "context-tree-repos");
15700
15651
  }
@@ -15868,7 +15819,7 @@ function errorMessage(error) {
15868
15819
  return redactSecret(error.message.trim().split("\n")[0] ?? "");
15869
15820
  }
15870
15821
  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]");
15822
+ 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
15823
  }
15873
15824
  function unavailableSnapshot(repo, branch, detail) {
15874
15825
  return {
@@ -16448,6 +16399,80 @@ function ghostNodeId(path) {
16448
16399
  function toPosix(path) {
16449
16400
  return sep === "/" ? path : path.split(sep).join("/");
16450
16401
  }
16402
+ /**
16403
+ * Mint a short-lived GitHub App installation token for the given installation.
16404
+ * Returns `ok: false` (with a precise reason) when the org has no App
16405
+ * configured, no installation row, the installation is suspended, or GitHub
16406
+ * rejects the mint — callers fall back to unauthenticated git fetch (public
16407
+ * repos still resolve; private repos surface as an unavailable snapshot
16408
+ * with a remediation message).
16409
+ *
16410
+ * Takes the `installation` row directly so the helper has no DB dependency
16411
+ * — route handlers do the `findInstallationByOrg` lookup themselves. Keeps
16412
+ * this module a pure transform that's trivial to unit-test.
16413
+ *
16414
+ * Credentials use the narrow `GithubAppCredentials` shape so the helper
16415
+ * isn't coupled to the broader OAuth config surface; callers pass
16416
+ * `config.oauth?.githubApp`, which structurally satisfies it.
16417
+ */
16418
+ async function mintContextTreeInstallationToken(installation, appCredentials, options = {}) {
16419
+ if (!appCredentials) return {
16420
+ ok: false,
16421
+ reason: "no-app-config"
16422
+ };
16423
+ if (!installation) return {
16424
+ ok: false,
16425
+ reason: "no-installation"
16426
+ };
16427
+ if (installation.suspendedAt) return {
16428
+ ok: false,
16429
+ reason: "suspended"
16430
+ };
16431
+ try {
16432
+ return {
16433
+ ok: true,
16434
+ token: (await mintInstallationToken(await createAppJwt({
16435
+ appId: appCredentials.appId,
16436
+ privateKeyPem: appCredentials.privateKeyPem
16437
+ }), installation.installationId, { fetcher: options.fetcher })).token
16438
+ };
16439
+ } catch (error) {
16440
+ return {
16441
+ ok: false,
16442
+ reason: "mint-failed",
16443
+ detail: error instanceof GithubAppApiError ? `GitHub returned ${error.status} when minting an installation token.` : "Hub could not mint a GitHub App installation token."
16444
+ };
16445
+ }
16446
+ }
16447
+ /**
16448
+ * Append a remediation hint to an unavailable snapshot's `contextStatus.detail`
16449
+ * when the underlying cause is a missing / suspended / failed GitHub App token
16450
+ * mint. Public-repo snapshots (mint reason `no-app-config`) are left untouched
16451
+ * — the deployment may legitimately have no App configured.
16452
+ *
16453
+ * Gated on `isGithubRemoteBinding(binding)` so unrelated unavailable
16454
+ * reasons (no repo configured, localPath missing, illegal branch name,
16455
+ * public-repo fetch error) don't get a misleading "install the GitHub
16456
+ * App" hint appended.
16457
+ *
16458
+ * Lives next to `mintContextTreeInstallationToken` so the two routes that
16459
+ * call mint share one shaping function; the snapshot service itself stays
16460
+ * token-agnostic.
16461
+ */
16462
+ function decorateSnapshotWithMintGuidance(snapshot, binding, mintResult) {
16463
+ if (mintResult.ok) return snapshot;
16464
+ if (snapshot.snapshotStatus !== "unavailable") return snapshot;
16465
+ if (mintResult.reason === "no-app-config") return snapshot;
16466
+ if (!isGithubRemoteBinding(binding)) return snapshot;
16467
+ 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}` : ""}`;
16468
+ return {
16469
+ ...snapshot,
16470
+ contextStatus: {
16471
+ ...snapshot.contextStatus,
16472
+ detail: `${snapshot.contextStatus.detail} ${guidance}`
16473
+ }
16474
+ };
16475
+ }
16451
16476
  const querySchema$1 = z.object({ window: z.enum([
16452
16477
  "1d",
16453
16478
  "7d",
@@ -16463,12 +16488,15 @@ async function contextTreeSnapshotRoutes(app) {
16463
16488
  const { userId } = requireUser(request);
16464
16489
  const orgId = await resolveUserPrimaryOrgId(app.db, userId);
16465
16490
  const binding = orgId ? await getOrgContextTree(app.db, orgId) : {};
16466
- const githubToken = contextTreeGithubTokenForRepo(binding.repo, app.config.contextTreeSync);
16491
+ let mintResult = null;
16492
+ if (orgId && isGithubRemoteBinding(binding)) mintResult = await mintContextTreeInstallationToken(await findInstallationByOrg(app.db, orgId), app.config.oauth?.githubApp);
16493
+ const githubToken = mintResult?.ok ? mintResult.token : void 0;
16467
16494
  const window = query.window ?? "7d";
16468
- const snapshot = await getContextTreeSnapshot({
16495
+ const rawSnapshot = await getContextTreeSnapshot({
16469
16496
  ...binding,
16470
16497
  githubToken
16471
16498
  }, window);
16499
+ const snapshot = mintResult ? decorateSnapshotWithMintGuidance(rawSnapshot, binding, mintResult) : rawSnapshot;
16472
16500
  const usage = orgId ? await summarizeContextTreeUsage(app.db, orgId, contextTreeSnapshotWindowDays(window)) : snapshot.usage;
16473
16501
  return contextTreeSnapshotSchema.parse({
16474
16502
  ...snapshot,
@@ -16476,31 +16504,6 @@ async function contextTreeSnapshotRoutes(app) {
16476
16504
  });
16477
16505
  });
16478
16506
  }
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
16507
  /**
16505
16508
  * Resolve the client IP for rate-limit attribution.
16506
16509
  *
@@ -16602,7 +16605,7 @@ async function healthzRoutes(app) {
16602
16605
  * `api/orgs/invitations.ts` (Class B, admin-gated).
16603
16606
  */
16604
16607
  async function publicInvitationRoutes(app) {
16605
- const { previewInvitation } = await import("./invitation-C9m2gQx4-CkwWteA3.mjs");
16608
+ const { previewInvitation } = await import("./invitation-C9m2gQx4-BSErdb8x.mjs");
16606
16609
  app.get("/:token/preview", async (request, reply) => {
16607
16610
  if (!request.params.token) throw new UnauthorizedError("Token required");
16608
16611
  const preview = await previewInvitation(app.db, request.params.token);
@@ -16891,7 +16894,7 @@ async function meRoutes(app) {
16891
16894
  */
16892
16895
  app.get("/me/pinned-agents", async (request) => {
16893
16896
  const { userId } = requireUser(request);
16894
- const { listMyPinnedAgents } = await import("./client-CDw0f-kN-BPzOVd8L.mjs");
16897
+ const { listMyPinnedAgents } = await import("./client-q1EYQD1n-ypjoumIO.mjs");
16895
16898
  return listMyPinnedAgents(app.db, { userId });
16896
16899
  });
16897
16900
  /**
@@ -17467,12 +17470,15 @@ async function orgContextTreeSnapshotRoutes(app) {
17467
17470
  const query = querySchema.parse(request.query);
17468
17471
  const scope = await requireOrgMembership(request, app.db);
17469
17472
  const binding = await getOrgContextTree(app.db, scope.organizationId);
17470
- const githubToken = contextTreeGithubTokenForRepo(binding.repo, app.config.contextTreeSync);
17473
+ let mintResult = null;
17474
+ if (isGithubRemoteBinding(binding)) mintResult = await mintContextTreeInstallationToken(await findInstallationByOrg(app.db, scope.organizationId), app.config.oauth?.githubApp);
17475
+ const githubToken = mintResult?.ok ? mintResult.token : void 0;
17471
17476
  const window = query.window ?? "7d";
17472
- const snapshot = await getContextTreeSnapshot({
17477
+ const rawSnapshot = await getContextTreeSnapshot({
17473
17478
  ...binding,
17474
17479
  githubToken
17475
17480
  }, window);
17481
+ const snapshot = mintResult ? decorateSnapshotWithMintGuidance(rawSnapshot, binding, mintResult) : rawSnapshot;
17476
17482
  const usage = await summarizeContextTreeUsage(app.db, scope.organizationId, contextTreeSnapshotWindowDays(window));
17477
17483
  return contextTreeSnapshotSchema.parse({
17478
17484
  ...snapshot,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-team-foundation/first-tree-hub",
3
- "version": "0.14.2",
3
+ "version": "0.14.3",
4
4
  "type": "module",
5
5
  "description": "First Tree Hub — unified CLI for server, client, and agent management",
6
6
  "exports": {