@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.
@@ -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-Da55XxRX.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-CrdnqZjv.mjs";
8
+ import { n as bindFeishuUser, t as bindFeishuBot } from "../feishu-DNoBroKK.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-BPRIfrOT-CoV_2o7e.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-CrdnqZjv.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-BPRIfrOT.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",
@@ -744,8 +725,10 @@ async function listAgentsManagedByUser(db, userId) {
744
725
  inboxId: agents.inboxId,
745
726
  visibility: agents.visibility,
746
727
  runtimeProvider: agents.runtimeProvider,
747
- clientId: agents.clientId
748
- }).from(agents).innerJoin(members, eq(agents.managerId, members.id)).where(and(eq(members.userId, userId), eq(members.status, "active"), ne(agents.status, AGENT_STATUSES.DELETED)));
728
+ clientId: agents.clientId,
729
+ avatarImageUpdatedAt: agents.avatarImageUpdatedAt,
730
+ userAvatarUrl: users.avatarUrl
731
+ }).from(agents).innerJoin(members, eq(agents.managerId, members.id)).leftJoin(users, eq(users.id, members.userId)).where(and(eq(members.userId, userId), eq(members.status, "active"), ne(agents.status, AGENT_STATUSES.DELETED)));
749
732
  }
750
733
  /**
751
734
  * Resolve the UUID of the "default" organization. Internal use only —
@@ -1185,6 +1168,36 @@ function agentAvatarImageUrl(uuid, updatedAt) {
1185
1168
  return `/api/v1/agents/${uuid}/avatar?v=${updatedAt.getTime()}`;
1186
1169
  }
1187
1170
  /**
1171
+ * Resolve the public avatar image URL for an agent, considering both the
1172
+ * manager-uploaded image and — for human agents — the user's external
1173
+ * avatar URL (e.g. GitHub `users.avatar_url` injected by OAuth). Returns
1174
+ * `null` when neither source is available; the renderer then falls back
1175
+ * to color + initial.
1176
+ *
1177
+ * Priority: uploaded image > human user's avatar > null. The "upload
1178
+ * wins" rule gives users explicit control: once they upload a custom
1179
+ * avatar for their human agent it always shows, regardless of any later
1180
+ * GitHub avatar change.
1181
+ */
1182
+ function resolveAvatarImageUrl(args) {
1183
+ const uploaded = agentAvatarImageUrl(args.uuid, args.avatarImageUpdatedAt);
1184
+ if (uploaded) return uploaded;
1185
+ if (args.type === AGENT_TYPES.HUMAN && args.userAvatarUrl) return args.userAvatarUrl;
1186
+ return null;
1187
+ }
1188
+ /**
1189
+ * Look up the external user-avatar URL backing a human agent via the
1190
+ * `members.agent_id → members.user_id → users.avatar_url` path. Returns
1191
+ * `null` for non-human agents or when the user has no avatar URL
1192
+ * captured (e.g. signed in without GitHub OAuth). Used by single-agent
1193
+ * API responses; list endpoints inline the join in their SELECT.
1194
+ */
1195
+ async function fetchUserAvatarForHumanAgent(db, agent) {
1196
+ if (agent.type !== AGENT_TYPES.HUMAN) return null;
1197
+ const [row] = await db.select({ avatarUrl: users.avatarUrl }).from(members).innerJoin(users, eq(members.userId, users.id)).where(eq(members.agentId, agent.uuid)).limit(1);
1198
+ return row?.avatarUrl ?? null;
1199
+ }
1200
+ /**
1188
1201
  * True iff `clients.metadata.capabilities` is a non-empty object — i.e. the
1189
1202
  * client has reported at least one runtime probe result. Used to distinguish
1190
1203
  * "we don't know what's installed yet" (empty / never reported) from
@@ -1445,13 +1458,14 @@ async function listAgentsForAdmin(db, scope, limit, cursor) {
1445
1458
  runtimeProvider: agents.runtimeProvider,
1446
1459
  avatarColorToken: agents.avatarColorToken,
1447
1460
  avatarImageUpdatedAt: agents.avatarImageUpdatedAt,
1461
+ userAvatarUrl: users.avatarUrl,
1448
1462
  createdAt: agents.createdAt,
1449
1463
  updatedAt: agents.updatedAt,
1450
1464
  presenceStatus: agentPresence.status,
1451
1465
  runtimeType: agentPresence.runtimeType,
1452
1466
  runtimeState: agentPresence.runtimeState,
1453
1467
  activeSessions: agentPresence.activeSessions
1454
- }).from(agents).leftJoin(agentPresence, eq(agents.uuid, agentPresence.agentId)).where(where).orderBy(desc(agents.createdAt)).limit(limit + 1);
1468
+ }).from(agents).leftJoin(agentPresence, eq(agents.uuid, agentPresence.agentId)).leftJoin(members, eq(members.agentId, agents.uuid)).leftJoin(users, eq(users.id, members.userId)).where(where).orderBy(desc(agents.createdAt)).limit(limit + 1);
1455
1469
  const hasMore = rows.length > limit;
1456
1470
  const items = hasMore ? rows.slice(0, limit) : rows;
1457
1471
  const last = items[items.length - 1];
@@ -1485,13 +1499,14 @@ async function listAgentsForMember(db, scope, limit, cursor, type) {
1485
1499
  runtimeProvider: agents.runtimeProvider,
1486
1500
  avatarColorToken: agents.avatarColorToken,
1487
1501
  avatarImageUpdatedAt: agents.avatarImageUpdatedAt,
1502
+ userAvatarUrl: users.avatarUrl,
1488
1503
  createdAt: agents.createdAt,
1489
1504
  updatedAt: agents.updatedAt,
1490
1505
  presenceStatus: agentPresence.status,
1491
1506
  runtimeType: agentPresence.runtimeType,
1492
1507
  runtimeState: agentPresence.runtimeState,
1493
1508
  activeSessions: agentPresence.activeSessions
1494
- }).from(agents).leftJoin(agentPresence, eq(agents.uuid, agentPresence.agentId)).where(where).orderBy(desc(agents.createdAt)).limit(limit + 1);
1509
+ }).from(agents).leftJoin(agentPresence, eq(agents.uuid, agentPresence.agentId)).leftJoin(members, eq(members.agentId, agents.uuid)).leftJoin(users, eq(users.id, members.userId)).where(where).orderBy(desc(agents.createdAt)).limit(limit + 1);
1495
1510
  const hasMore = rows.length > limit;
1496
1511
  const items = hasMore ? rows.slice(0, limit) : rows;
1497
1512
  const last = items[items.length - 1];
@@ -2298,15 +2313,9 @@ function createNotifier(listenClient) {
2298
2313
  const messageId = payload.slice(sepIdx + 1);
2299
2314
  const sockets = subscriptions.get(inboxId);
2300
2315
  if (!sockets) return;
2301
- const doorbellFrame = JSON.stringify({
2302
- type: "new_message",
2303
- inboxId,
2304
- messageId
2305
- });
2306
2316
  for (const [ws, pushHandler] of sockets) {
2307
2317
  if (ws.readyState !== ws.OPEN) continue;
2308
- if (pushHandler) Promise.resolve(pushHandler(messageId)).catch(() => {});
2309
- else ws.send(doorbellFrame);
2318
+ Promise.resolve(pushHandler(messageId)).catch(() => {});
2310
2319
  }
2311
2320
  }
2312
2321
  return {
@@ -2316,7 +2325,7 @@ function createNotifier(listenClient) {
2316
2325
  map = /* @__PURE__ */ new Map();
2317
2326
  subscriptions.set(inboxId, map);
2318
2327
  }
2319
- map.set(ws, pushHandler ?? null);
2328
+ map.set(ws, pushHandler);
2320
2329
  },
2321
2330
  unsubscribe(inboxId, ws) {
2322
2331
  const map = subscriptions.get(inboxId);
@@ -3516,13 +3525,21 @@ async function getChatDetail(db, chatId, selfAgentId = null) {
3516
3525
  displayName: p.displayName,
3517
3526
  type: p.type
3518
3527
  }));
3528
+ const viewerMembershipKind = await resolveViewerMembershipKind(db, chatId, selfAgentId);
3519
3529
  return {
3520
3530
  ...chat,
3521
3531
  participants,
3522
3532
  title,
3523
- firstMessagePreview
3533
+ firstMessagePreview,
3534
+ viewerMembershipKind
3524
3535
  };
3525
3536
  }
3537
+ async function resolveViewerMembershipKind(db, chatId, viewerAgentId) {
3538
+ if (!viewerAgentId) return null;
3539
+ const [row] = await db.select({ accessMode: chatMembership.accessMode }).from(chatMembership).where(and(eq(chatMembership.chatId, chatId), eq(chatMembership.agentId, viewerAgentId))).limit(1);
3540
+ if (!row) return null;
3541
+ return row.accessMode === "speaker" ? "participant" : "watching";
3542
+ }
3526
3543
  async function listChats(db, agentId, limit, cursor) {
3527
3544
  const chatIds = (await db.select({ chatId: chatMembership.chatId }).from(chatMembership).where(and(eq(chatMembership.agentId, agentId), eq(chatMembership.accessMode, "speaker")))).map((r) => r.chatId);
3528
3545
  if (chatIds.length === 0) return {
@@ -4210,4 +4227,4 @@ async function cleanupStaleClients(db, staleSeconds = 60) {
4210
4227
  return result.length;
4211
4228
  }
4212
4229
  //#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 };
4230
+ export { getSession as $, suspendSession as $t, createChat as A, pollInbox as At, fetchUserAvatarForHumanAgent as B, resolveAvatarImageUrl as Bt, claimBacklogForPush as C, markMeChatRead as Ct, clearAgentAvatarImage as D, messages as Dt, cleanupStalePresence as E, members as Et, disconnectClient as F, registerChatMessageDispatcher as Ft, getAgentAvatarImage as G, sendToAgent as Gt, findOrCreateDirectChat as H, resolveDefaultOrgId as Ht, editMessage as I, registerClient as It, getChatDetail as J, setChatEngagement as Jt, getCachedAudience as K, serverInstances as Kt, ensureDefaultOrganization as L, removeParticipant as Lt, createNotifier as M, reactivateAgent as Mt, deleteAgent as N, rebindAgent as Nt, clients as O, notifyRecipients as Ot, deriveAuthState as P, recomputeWatchersForMember as Pt, getPresence as Q, suspendAgent as Qt, ensureParticipant as R, resetActivity as Rt, claimAndBuildForPush as S, listMyPinnedAgents as St, cleanupStaleClients 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, bindAgent as _, listClients as _t, adapterConfigs as a, upsertSessionState as an, leaveChat as at, chats as b, listMeChats as bt, addParticipant as c, listAgentSessions as ct, agentConfigs as d, listAgentsManagedByUser as dt, touchAgent as en, heartbeatClient as et, agentPresence as f, listAgentsWithRuntime as ft, assertParticipant as g, listChatsForMember as gt, assertClientOwner as h, listChats as ht, adapterAgentMappings as i, updateOrganization as in, joinMeChat as it, createMeChat as j, pruneStaleSilentEntries as jt, createAgent as k, pendingQuestions as kt, agentAvatarImageUrl as l, listAgentsForAdmin as lt, archiveSession as m, listChatParticipantsWithNames as mt, SUPPORTED_AVATAR_IMAGE_MIMES as n, updateAgent as nn, inboxEntries as nt, addChatParticipants as o, leaveMeChat as ot, agents as p, listAllSessions as pt, getCallerEngagement as q, setAgentAvatarImage as qt, ackEntryByIdForBoundAgents as r, updateClientCapabilities as rn, joinChat as rt, addMeChatParticipants as s, listActiveAgentsPinnedToClient as st, MAX_AVATAR_IMAGE_BYTES as t, unbindAgent as tn, heartbeatInstance as tt, agentChatSessions as u, listAgentsForMember as ut, chatMembership as v, listClientsForOrgAdmin as vt, claimClient as w, markMeChatUnread as wt, checkAgentNameAvailability as x, listMessages as xt, chatUserState as y, listMeChatSourceCounts as yt, extractSummary as z, resetTimedOutEntries 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-CrdnqZjv.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 { St as listMyPinnedAgents } from "./client-BPRIfrOT-CoV_2o7e.mjs";
7
7
  export { listMyPinnedAgents };
@@ -756,7 +756,8 @@ z.object({
756
756
  participants: z.array(chatParticipantDetailSchema),
757
757
  title: z.string(),
758
758
  firstMessagePreview: z.string().nullable(),
759
- engagementStatus: chatEngagementStatusSchema
759
+ engagementStatus: chatEngagementStatusSchema,
760
+ viewerMembershipKind: z.enum(["participant", "watching"]).nullable()
760
761
  });
761
762
  const updateChatSchema = z.object({ topic: z.string().trim().max(500).nullable() });
762
763
  /**
@@ -803,9 +804,13 @@ z.object({
803
804
  * Optional opt-in flags the client carries on `client:register` to advertise
804
805
  * which negotiable wire-protocol features it implements. Distinct from
805
806
  * `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.
807
+ * concept).
808
+ *
809
+ * 0.10.4 ~ 0.14.2 clients still send this block (with `wsInboxDeliver: true`
810
+ * hard-coded). The 0.14.3+ runtime omits it. The schema is retained so that
811
+ * middle-version `client:register` frames still parse, even though the
812
+ * server no longer reads any of these fields — the WS inbox data plane is
813
+ * mandatory on this server build.
809
814
  */
810
815
  const clientWireCapabilitiesSchema = z.object({ wsInboxDeliver: z.boolean().default(false) }).partial();
811
816
  const clientRegisterSchema = z.object({
@@ -1196,14 +1201,11 @@ z.object({
1196
1201
  }).extend({ message: clientMessageSchema });
1197
1202
  const inboxPollQuerySchema = z.object({ limit: z.coerce.number().int().min(1).max(50).default(10) });
1198
1203
  /**
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.
1204
+ * server → client: a single inbox entry pushed over the active WS connection.
1201
1205
  *
1202
1206
  * `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).
1207
+ * in `inbox:ack`. `clientMessageSchema` carries `precedingMessages`, so the
1208
+ * client-side dispatch logic handles the silent-context bundle uniformly.
1207
1209
  *
1208
1210
  * `.passthrough()` so a forward-rolling server may extend the frame without
1209
1211
  * breaking older clients that validate strictly. Older clients drop unknown
@@ -1217,8 +1219,7 @@ const inboxDeliverFrameSchema = z.object({
1217
1219
  message: clientMessageSchema
1218
1220
  }).passthrough();
1219
1221
  /**
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.
1222
+ * client → server: ack for an `inbox:deliver` frame.
1222
1223
  */
1223
1224
  const inboxAckFrameSchema = z.object({
1224
1225
  type: z.literal("inbox:ack"),
@@ -1994,8 +1995,13 @@ const WS_AUTH_FRAME_TIMEOUT_MS = 5e3;
1994
1995
  /**
1995
1996
  * Negotiable wire-protocol features the server advertises in its `welcome`
1996
1997
  * 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.
1998
+ * frame is `.passthrough()`.
1999
+ *
2000
+ * Required by clients in the 0.10.4 ~ 0.14.2 range: those builds read
2001
+ * `wsInboxDeliver` here to decide whether to skip the local HTTP poll loop
2002
+ * and rely on `inbox:deliver` push frames. The 0.14.3+ runtime ignores the
2003
+ * field (push is the only path) but the server still emits it so middle-
2004
+ * version clients keep working.
1999
2005
  */
2000
2006
  const serverCapabilitiesSchema = z.object({ wsInboxDeliver: z.boolean().default(false) }).partial();
2001
2007
  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-CrdnqZjv.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-Da55XxRX.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-CrdnqZjv.mjs";
7
+ import { n as bindFeishuUser, t as bindFeishuBot } from "./feishu-DNoBroKK.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-BPRIfrOT-CoV_2o7e.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-CrdnqZjv.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 };