@agent-team-foundation/first-tree-hub 0.9.11 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  import { m as __toESM } from "./esm-CYu4tXXn.mjs";
2
2
  import { _ as withSpan, a as endWsConnectionSpan, b as require_pino, c as messageAttrs, d as rootLogger$1, g as startWsConnectionSpan, i as currentTraceId, n as applyLoggerConfig, o as getFastifyOtelPlugin, p as setWsConnectionAttrs, r as createLogger$1, t as adapterAttrs, u as observabilityPlugin, v as withWsMessageSpan, y as FIRST_TREE_HUB_ATTR } from "./observability-DV_fQKqV-oxfXX6Z2.mjs";
3
3
  import { C as serverConfigSchema, S as resolveConfigReadonly, _ as loadAgents, d as DEFAULT_HOME_DIR$1, f as agentConfigSchema, g as initConfig, i as loadCredentials, l as DEFAULT_CONFIG_DIR, m as collectMissingPrompts, n as ensureFreshAccessToken, o as resolveServerUrl, p as clientConfigSchema, s as saveAgentConfig, u as DEFAULT_DATA_DIR$1, v as migrateLegacyHome, w as setConfigValue } from "./bootstrap-CtVqQA8a.mjs";
4
- import { $ as sessionEventSchema$1, A as createChatSchema, B as isReservedAgentName$1, C as agentRuntimeConfigPayloadSchema$1, D as createAdapterConfigSchema, E as connectTokenExchangeSchema, F as dryRunAgentRuntimeConfigSchema, G as paginationQuerySchema, H as loginSchema, I as extractMentions, J as selfServiceFeishuBotSchema, K as refreshTokenSchema, L as imageInlineContentSchema, M as createOrganizationSchema, N as createTaskSchema, O as createAdapterMappingSchema, P as delegateFeishuUserSchema, Q as sessionEventMessageSchema, R as inboxPollQuerySchema, S as agentPinnedMessageSchema$1, T as clientRegisterSchema, U as messageSourceSchema$1, V as linkTaskChatSchema, W as notificationQuerySchema, X as sendToAgentSchema, Y as sendMessageSchema, Z as sessionCompletionMessageSchema, _ as WS_AUTH_FRAME_TIMEOUT_MS, a as AGENT_NAME_REGEX$1, at as updateAgentSchema, b as adminUpdateTaskSchema, c as AGENT_STATUSES, ct as updateOrganizationSchema, d as DEFAULT_AGENT_RUNTIME_CONFIG_PAYLOAD, dt as wsAuthFrameSchema, et as sessionReconcileRequestSchema, f as SYSTEM_CONFIG_DEFAULTS, g as TASK_TERMINAL_STATUSES, h as TASK_STATUSES, i as AGENT_BIND_REJECT_REASONS, it as updateAgentRuntimeConfigSchema, j as createMemberSchema, k as createAgentSchema, l as AGENT_TYPES, lt as updateSystemConfigSchema, m as TASK_HEALTH_SIGNALS, nt as taskListQuerySchema, o as AGENT_SELECTOR_HEADER$1, ot as updateChatSchema, p as TASK_CREATOR_TYPES, q as runtimeStateMessageSchema, rt as updateAdapterConfigSchema, s as AGENT_SOURCES, st as updateMemberSchema, tt as sessionStateMessageSchema, u as AGENT_VISIBILITY, ut as updateTaskStatusSchema, v as addParticipantSchema, w as agentTypeSchema$1, x as agentBindRequestSchema, y as adminCreateTaskSchema, z as isRedactedEnvValue } from "./feishu-B2sjp6Z6.mjs";
4
+ import { $ as sessionEventMessageSchema, A as createChatSchema, B as isReservedAgentName$1, C as agentRuntimeConfigPayloadSchema$1, D as createAdapterConfigSchema, E as connectTokenExchangeSchema, F as dryRunAgentRuntimeConfigSchema, G as paginationQuerySchema, H as loginSchema, I as extractMentions, J as scanMentionTokens, K as refreshTokenSchema, L as imageInlineContentSchema, M as createOrganizationSchema, N as createTaskSchema, O as createAdapterMappingSchema, P as delegateFeishuUserSchema, Q as sessionCompletionMessageSchema, R as inboxPollQuerySchema, S as agentPinnedMessageSchema$1, T as clientRegisterSchema, U as messageSourceSchema$1, V as linkTaskChatSchema, W as notificationQuerySchema, X as sendMessageSchema, Y as selfServiceFeishuBotSchema, Z as sendToAgentSchema, _ as WS_AUTH_FRAME_TIMEOUT_MS, a as AGENT_NAME_REGEX$1, at as updateAgentRuntimeConfigSchema, b as adminUpdateTaskSchema, c as AGENT_STATUSES, ct as updateMemberSchema, d as DEFAULT_AGENT_RUNTIME_CONFIG_PAYLOAD, dt as updateTaskStatusSchema, et as sessionEventSchema$1, f as SYSTEM_CONFIG_DEFAULTS, ft as wsAuthFrameSchema, g as TASK_TERMINAL_STATUSES, h as TASK_STATUSES, i as AGENT_BIND_REJECT_REASONS, it as updateAdapterConfigSchema, j as createMemberSchema, k as createAgentSchema, l as AGENT_TYPES, lt as updateOrganizationSchema, m as TASK_HEALTH_SIGNALS, nt as sessionStateMessageSchema, o as AGENT_SELECTOR_HEADER$1, ot as updateAgentSchema, p as TASK_CREATOR_TYPES, q as runtimeStateMessageSchema, rt as taskListQuerySchema, s as AGENT_SOURCES, st as updateChatSchema, tt as sessionReconcileRequestSchema, u as AGENT_VISIBILITY, ut as updateSystemConfigSchema, v as addParticipantSchema, w as agentTypeSchema$1, x as agentBindRequestSchema, y as adminCreateTaskSchema, z as isRedactedEnvValue } from "./feishu-DEmwoNn_.mjs";
5
5
  import { createRequire } from "node:module";
6
6
  import { ZodError, z } from "zod";
7
7
  import { delimiter, dirname, isAbsolute, join, resolve } from "node:path";
@@ -454,7 +454,7 @@ function isReservedAgentName(name) {
454
454
  z.object({
455
455
  name: z.string().min(1).max(64).regex(AGENT_NAME_REGEX, "Must start with a letter or digit and contain only lowercase letters, digits, hyphens (-), and underscores (_). Max 64 chars.").refine((n) => !isReservedAgentName(n), { message: "That agent name is reserved — pick a different one." }).optional(),
456
456
  type: agentTypeSchema,
457
- displayName: z.string().max(200).optional(),
457
+ displayName: z.string().min(1).max(200).optional(),
458
458
  delegateMention: z.string().max(100).optional(),
459
459
  organizationId: z.string().max(100).optional(),
460
460
  source: agentSourceSchema.optional(),
@@ -465,7 +465,7 @@ z.object({
465
465
  });
466
466
  z.object({
467
467
  type: agentTypeSchema.optional(),
468
- displayName: z.string().max(200).nullable().optional(),
468
+ displayName: z.string().min(1).max(200).optional(),
469
469
  delegateMention: z.string().max(100).nullable().optional(),
470
470
  visibility: agentVisibilitySchema.optional(),
471
471
  metadata: z.record(z.string(), z.unknown()).optional(),
@@ -477,7 +477,7 @@ z.object({
477
477
  name: z.string().nullable(),
478
478
  organizationId: z.string(),
479
479
  type: agentTypeSchema,
480
- displayName: z.string().nullable(),
480
+ displayName: z.string(),
481
481
  delegateMention: z.string().nullable(),
482
482
  inboxId: z.string(),
483
483
  status: z.string(),
@@ -504,7 +504,7 @@ const agentPinnedMessageSchema = z.object({
504
504
  type: z.literal("agent:pinned"),
505
505
  agentId: z.string(),
506
506
  name: z.string().nullable(),
507
- displayName: z.string().nullable(),
507
+ displayName: z.string(),
508
508
  agentType: agentTypeSchema
509
509
  });
510
510
  /**
@@ -682,7 +682,7 @@ const chatParticipantSchema = z.object({
682
682
  });
683
683
  chatParticipantSchema.extend({
684
684
  name: z.string().nullable(),
685
- displayName: z.string().nullable(),
685
+ displayName: z.string(),
686
686
  type: z.string()
687
687
  });
688
688
  z.object({
@@ -835,6 +835,23 @@ const inReplyToSnapshotSchema = z.object({
835
835
  /** Per-chat participation mode exposed to the recipient runtime. */
836
836
  const participantModeSchema = z.enum(["full", "mention_only"]);
837
837
  /**
838
+ * Lightweight snapshot of an earlier message in the same chat that the
839
+ * recipient missed (because it was `mention_only` + not @mentioned). Server
840
+ * attaches a list of these to the next active delivery in the chat so the
841
+ * agent's prompt carries enough context to reply meaningfully.
842
+ *
843
+ * Smaller than `messageSchema` on purpose — drops fields that don't help the
844
+ * LLM (replyTo envelopes, source) and aren't safe to leak across recipients.
845
+ */
846
+ const precedingMessageSchema = z.object({
847
+ id: z.string(),
848
+ senderId: z.string(),
849
+ format: z.string(),
850
+ content: z.unknown(),
851
+ metadata: z.record(z.string(), z.unknown()).default({}),
852
+ createdAt: z.string()
853
+ });
854
+ /**
838
855
  * Wire format for messages routed FROM the Hub TO a client runtime.
839
856
  *
840
857
  * Adds `configVersion` so the client can compare against its locally cached
@@ -850,11 +867,17 @@ const participantModeSchema = z.enum(["full", "mention_only"]);
850
867
  *
851
868
  * `inReplyToSnapshot` is populated when `inReplyTo` resolves to an existing
852
869
  * message; runtime uses it to suppress self-reply echo on direct chats.
870
+ *
871
+ * `precedingMessages` is a (possibly empty) list of older messages in the
872
+ * same chat that this recipient did not previously receive (silent inbox
873
+ * context). The runtime renders them as "earlier in chat" before the
874
+ * triggering message — see proposals/group-chat-ux-improvements §1.
853
875
  */
854
876
  const clientMessageSchema = messageSchema.extend({
855
877
  configVersion: z.number().int().positive(),
856
878
  recipientMode: participantModeSchema.default("full"),
857
- inReplyToSnapshot: inReplyToSnapshotSchema.default(null)
879
+ inReplyToSnapshot: inReplyToSnapshotSchema.default(null),
880
+ precedingMessages: z.array(precedingMessageSchema).default([])
858
881
  });
859
882
  z.enum([
860
883
  "pending",
@@ -1896,7 +1919,7 @@ var ClientConnection = class extends EventEmitter {
1896
1919
  });
1897
1920
  const agent = {
1898
1921
  agentId,
1899
- displayName: msg.displayName ?? null,
1922
+ displayName: msg.displayName ?? agentId,
1900
1923
  agentType: msg.agentType ?? "personal_assistant",
1901
1924
  sdk
1902
1925
  };
@@ -3680,13 +3703,32 @@ function resolveSenderLabel(senderId, participants) {
3680
3703
  * content is serialised to JSON — handlers that want to feed structured
3681
3704
  * content some other way should opt out and format themselves.
3682
3705
  *
3706
+ * If the server attached `precedingMessages` (silent group-chat history the
3707
+ * recipient missed because it was `mention_only` and not @mentioned), prepend
3708
+ * them under an `[Earlier in chat]` block so the LLM sees what came before
3709
+ * the @mention that woke this turn — see proposals/group-chat-ux-improvements §1.
3710
+ *
3683
3711
  * Async because the participant list may need a server round-trip on first
3684
3712
  * use; subsequent messages in the same session hit the cache.
3685
3713
  */
3686
3714
  async function formatInboundContent(message, participants) {
3687
3715
  const rawContent = typeof message.content === "string" ? message.content : JSON.stringify(message.content);
3688
- if (!message.senderId) return rawContent;
3689
- return `[From: ${resolveSenderLabel(message.senderId, await participants.get())}]\n\n${rawContent}`;
3716
+ const preceding = message.precedingMessages ?? [];
3717
+ let header = "";
3718
+ if (preceding.length > 0) {
3719
+ const ps = await participants.get();
3720
+ const lines = ["[Earlier in chat — context you missed]"];
3721
+ for (const p of preceding) {
3722
+ const label = resolveSenderLabel(p.senderId, ps);
3723
+ const text = typeof p.content === "string" ? p.content : JSON.stringify(p.content);
3724
+ lines.push(`[From: ${label}] ${text}`);
3725
+ }
3726
+ lines.push("", "[Now — message that woke you]");
3727
+ header = `${lines.join("\n")}\n\n`;
3728
+ }
3729
+ if (!message.senderId) return `${header}${rawContent}`;
3730
+ const label = resolveSenderLabel(message.senderId, await participants.get());
3731
+ return `${header}[From: ${label}]\n\n${rawContent}`;
3690
3732
  }
3691
3733
  /**
3692
3734
  * Deduplicator — bounded set of recently seen IDs.
@@ -4316,7 +4358,8 @@ var SessionManager = class {
4316
4358
  senderId: msg.senderId,
4317
4359
  format: msg.format,
4318
4360
  content: msg.content,
4319
- metadata: msg.metadata
4361
+ metadata: msg.metadata,
4362
+ precedingMessages: msg.precedingMessages ?? []
4320
4363
  };
4321
4364
  }
4322
4365
  loadPersistedSessions() {
@@ -4407,7 +4450,7 @@ var AgentSlot = class {
4407
4450
  const sdk = (await this.clientConnection.bindAgent(this.config.agentId, this.config.runtimeType ?? this.config.type, this.config.runtimeVersion)).sdk;
4408
4451
  this.sdk = sdk;
4409
4452
  const agent = await sdk.register();
4410
- this.logger.info({ displayName: agent.displayName ?? agent.agentId }, "agent bound");
4453
+ this.logger.info({ displayName: agent.displayName }, "agent bound");
4411
4454
  if (agent.type === "human") {
4412
4455
  this.logger.info("server reports type=human — message processing disabled");
4413
4456
  return agent;
@@ -6109,6 +6152,168 @@ async function runMigrations(databaseUrl) {
6109
6152
  }
6110
6153
  }
6111
6154
  //#endregion
6155
+ //#region src/core/migrate-agent-dirs.ts
6156
+ function createApiNameResolver(serverUrl, getAccessToken) {
6157
+ let cache = null;
6158
+ const PAGE_SIZE = 100;
6159
+ const MAX_PAGES = 50;
6160
+ async function ensureCache() {
6161
+ if (cache) return cache;
6162
+ const token = await getAccessToken();
6163
+ const map = /* @__PURE__ */ new Map();
6164
+ let cursor = null;
6165
+ for (let page = 0; page < MAX_PAGES; page++) {
6166
+ const qs = new URLSearchParams({ limit: String(PAGE_SIZE) });
6167
+ if (cursor) qs.set("cursor", cursor);
6168
+ const res = await fetch(`${serverUrl}/api/v1/admin/agents?${qs.toString()}`, {
6169
+ method: "GET",
6170
+ headers: { Authorization: `Bearer ${token}` },
6171
+ signal: AbortSignal.timeout(1e4)
6172
+ });
6173
+ if (!res.ok) throw new Error(`admin agents list returned HTTP ${res.status}`);
6174
+ const body = await res.json();
6175
+ for (const row of body.items) map.set(row.uuid, row.name);
6176
+ if (!body.nextCursor) break;
6177
+ cursor = body.nextCursor;
6178
+ }
6179
+ cache = map;
6180
+ return map;
6181
+ }
6182
+ return { async resolveName(agentId) {
6183
+ return (await ensureCache()).get(agentId) ?? null;
6184
+ } };
6185
+ }
6186
+ /**
6187
+ * Read the `agentId` field out of a single `agent.yaml` with minimal
6188
+ * parsing. Unlike `loadAgents`, which Zod-validates every entry and
6189
+ * throws on the first malformed file (aborting migration for *every*
6190
+ * other agent below it), this helper scopes failures per-dir: a broken
6191
+ * file returns `null` and the caller logs + skips that dir only.
6192
+ */
6193
+ function readAgentId(agentYamlPath) {
6194
+ try {
6195
+ const parsed = parse(readFileSync(agentYamlPath, "utf-8"));
6196
+ if (parsed && typeof parsed === "object" && "agentId" in parsed) {
6197
+ const id = parsed.agentId;
6198
+ if (typeof id === "string" && id.length > 0) return id;
6199
+ }
6200
+ return null;
6201
+ } catch {
6202
+ return null;
6203
+ }
6204
+ }
6205
+ /**
6206
+ * Walk `agentsDir`, for each local agent compare the dir name to the
6207
+ * server's canonical `name`, and rename the dir + workspaces/sessions
6208
+ * entries when they differ. Returns a summary so the caller can decide
6209
+ * whether to print additional context.
6210
+ */
6211
+ async function migrateLocalAgentDirs(opts) {
6212
+ const { agentsDir, workspacesDir, sessionsDir, resolver } = opts;
6213
+ const result = {
6214
+ scanned: 0,
6215
+ renamed: 0,
6216
+ skipped: 0,
6217
+ errors: 0
6218
+ };
6219
+ if (!existsSync(agentsDir)) return result;
6220
+ let dirNames;
6221
+ try {
6222
+ dirNames = readdirSync(agentsDir).filter((name) => {
6223
+ try {
6224
+ return statSync(join(agentsDir, name)).isDirectory();
6225
+ } catch {
6226
+ return false;
6227
+ }
6228
+ });
6229
+ } catch (err) {
6230
+ const msg = err instanceof Error ? err.message : String(err);
6231
+ print.status("⚠️", `agent-dir migration: unable to enumerate ${agentsDir}: ${msg}`);
6232
+ return {
6233
+ ...result,
6234
+ errors: result.errors + 1
6235
+ };
6236
+ }
6237
+ const finalDirNames = new Set(dirNames);
6238
+ for (const dirName of dirNames) {
6239
+ const agentYamlPath = join(agentsDir, dirName, "agent.yaml");
6240
+ const agentId = readAgentId(agentYamlPath);
6241
+ if (!agentId) {
6242
+ if (existsSync(agentYamlPath)) {
6243
+ print.status("⚠️", `agent-dir migration: unreadable ${agentYamlPath}; skipping this dir.`);
6244
+ result.errors += 1;
6245
+ }
6246
+ continue;
6247
+ }
6248
+ result.scanned += 1;
6249
+ let serverName;
6250
+ try {
6251
+ serverName = await resolver.resolveName(agentId);
6252
+ } catch (err) {
6253
+ const msg = err instanceof Error ? err.message : String(err);
6254
+ const hint = msg.includes("403") ? " (likely a non-admin account — migration skipped)" : "";
6255
+ print.status("⚠️", `agent-dir migration: failed to resolve "${dirName}" (${agentId}): ${msg}${hint}`);
6256
+ result.errors += 1;
6257
+ return result;
6258
+ }
6259
+ if (!serverName) {
6260
+ result.skipped += 1;
6261
+ continue;
6262
+ }
6263
+ if (serverName === dirName) continue;
6264
+ const oldDir = join(agentsDir, dirName);
6265
+ const newDir = join(agentsDir, serverName);
6266
+ if (existsSync(newDir)) {
6267
+ print.status("⚠️", `agent-dir migration: cannot rename "${dirName}" → "${serverName}" — target already exists. Skipping.`);
6268
+ result.skipped += 1;
6269
+ continue;
6270
+ }
6271
+ try {
6272
+ renameSync(oldDir, newDir);
6273
+ finalDirNames.delete(dirName);
6274
+ finalDirNames.add(serverName);
6275
+ } catch (err) {
6276
+ const msg = err instanceof Error ? err.message : String(err);
6277
+ print.status("⚠️", `agent-dir migration: config dir rename failed for "${dirName}": ${msg}`);
6278
+ result.errors += 1;
6279
+ continue;
6280
+ }
6281
+ const oldWorkspace = join(workspacesDir, dirName);
6282
+ const newWorkspace = join(workspacesDir, serverName);
6283
+ if (existsSync(oldWorkspace)) try {
6284
+ if (existsSync(newWorkspace)) print.status("⚠️", `agent-dir migration: workspace target "${serverName}" already exists; leaving old "${dirName}" in place for manual cleanup.`);
6285
+ else if (statSync(oldWorkspace).isDirectory()) renameSync(oldWorkspace, newWorkspace);
6286
+ } catch (err) {
6287
+ const msg = err instanceof Error ? err.message : String(err);
6288
+ print.status("⚠️", `agent-dir migration: workspace rename failed for "${dirName}": ${msg}`);
6289
+ result.errors += 1;
6290
+ }
6291
+ const oldSessions = join(sessionsDir, `${dirName}.json`);
6292
+ const newSessions = join(sessionsDir, `${serverName}.json`);
6293
+ if (existsSync(oldSessions)) try {
6294
+ if (existsSync(newSessions)) print.status("⚠️", `agent-dir migration: sessions target "${serverName}.json" already exists; leaving old "${dirName}.json" in place for manual cleanup.`);
6295
+ else renameSync(oldSessions, newSessions);
6296
+ } catch (err) {
6297
+ const msg = err instanceof Error ? err.message : String(err);
6298
+ print.status("⚠️", `agent-dir migration: sessions rename failed for "${dirName}": ${msg}`);
6299
+ result.errors += 1;
6300
+ }
6301
+ print.status("", `agent "${dirName}" renamed to "${serverName}" to match hub`);
6302
+ result.renamed += 1;
6303
+ }
6304
+ try {
6305
+ const orphanWs = existsSync(workspacesDir) ? readdirSync(workspacesDir).filter((d) => !finalDirNames.has(d)) : [];
6306
+ const orphanSessions = existsSync(sessionsDir) ? readdirSync(sessionsDir).filter((f) => f.endsWith(".json") && !finalDirNames.has(f.slice(0, -5))) : [];
6307
+ if (orphanWs.length > 0 || orphanSessions.length > 0) {
6308
+ const parts = [];
6309
+ if (orphanWs.length > 0) parts.push(`workspaces: ${orphanWs.join(", ")}`);
6310
+ if (orphanSessions.length > 0) parts.push(`sessions: ${orphanSessions.join(", ")}`);
6311
+ print.status("", `orphaned local state detected (${parts.join("; ")}). Run \`first-tree-hub agent workspace clean\` to reclaim disk.`);
6312
+ }
6313
+ } catch {}
6314
+ return result;
6315
+ }
6316
+ //#endregion
6112
6317
  //#region src/core/migrate-home.ts
6113
6318
  /**
6114
6319
  * Run the one-shot legacy home migration at CLI startup and, if it succeeds,
@@ -6297,8 +6502,9 @@ async function onboardCreate(args) {
6297
6502
  metadata: Object.keys(metadata).length > 0 ? metadata : void 0,
6298
6503
  clientId: args.type === "human" ? void 0 : args.clientId
6299
6504
  });
6300
- print.line(`Agent "${args.id}" created (uuid ${primary.uuid}).\n`);
6301
- if (args.type !== "human") saveAgentConfig(args.id, primary.uuid, "claude-code");
6505
+ const primaryLocalName = primary.name ?? args.id;
6506
+ print.line(`Agent "${primaryLocalName}" created (uuid ${primary.uuid}).\n`);
6507
+ if (args.type !== "human") saveAgentConfig(primaryLocalName, primary.uuid, "claude-code");
6302
6508
  let assistantUuid = null;
6303
6509
  if (args.assistant) {
6304
6510
  print.line(`Creating assistant "${args.assistant}"...\n`);
@@ -6314,8 +6520,9 @@ async function onboardCreate(args) {
6314
6520
  clientId: args.clientId
6315
6521
  });
6316
6522
  assistantUuid = assistant.uuid;
6317
- saveAgentConfig(args.assistant, assistant.uuid, "claude-code");
6318
- print.line(`Assistant "${args.assistant}" ready.\n`);
6523
+ const assistantLocalName = assistant.name ?? args.assistant;
6524
+ saveAgentConfig(assistantLocalName, assistant.uuid, "claude-code");
6525
+ print.line(`Assistant "${assistantLocalName}" ready.\n`);
6319
6526
  } catch (err) {
6320
6527
  const msg = err instanceof Error ? err.message : String(err);
6321
6528
  print.line(`Warning: Failed to create assistant "${args.assistant}": ${msg}\n`);
@@ -6323,7 +6530,7 @@ async function onboardCreate(args) {
6323
6530
  }
6324
6531
  const runtimeAgent = args.type === "human" ? args.assistant : args.id;
6325
6532
  if (args.feishuBotAppId && args.feishuBotAppSecret) {
6326
- const { bindFeishuBot } = await import("./feishu-B2sjp6Z6.mjs").then((n) => n.r);
6533
+ const { bindFeishuBot } = await import("./feishu-DEmwoNn_.mjs").then((n) => n.r);
6327
6534
  const targetAgentUuid = args.type === "human" ? assistantUuid : primary.uuid;
6328
6535
  if (!targetAgentUuid) print.line(`Warning: Cannot bind Feishu bot — no runtime agent available for "${args.id}".\n`);
6329
6536
  else {
@@ -6399,18 +6606,40 @@ async function promptMissingFields(options) {
6399
6606
  return results;
6400
6607
  }
6401
6608
  /**
6402
- * Interactive add agent simple two-field prompt.
6403
- */
6404
- async function promptAddAgent() {
6609
+ * Interactive / scripted "add this agent to the local client".
6610
+ *
6611
+ * Phase 3 of the agent-naming refactor removed the free-form local
6612
+ * alias — the local config dir is keyed by the server-authoritative
6613
+ * `agent.name` slug. This helper only asks the user for the agent UUID
6614
+ * (or takes it via `opts.agentId`), then fetches the canonical name
6615
+ * from the Hub. A `name` comes back null only if the agent was
6616
+ * tombstoned server-side, in which case the caller must refuse the
6617
+ * add (there's nothing sensible to key the local dir on).
6618
+ */
6619
+ async function promptAddAgent(opts = {}) {
6620
+ if (loadCredentials() === null) throw new Error("Not connected. Run `first-tree-hub client connect <server-url>` first.");
6621
+ let serverUrl;
6622
+ try {
6623
+ serverUrl = resolveServerUrl(process.env.FIRST_TREE_HUB_SERVER_URL);
6624
+ } catch (err) {
6625
+ const msg = err instanceof Error ? err.message : String(err);
6626
+ throw new Error(`${msg} Run \`first-tree-hub client connect\` or set FIRST_TREE_HUB_SERVER_URL.`);
6627
+ }
6628
+ const agentId = opts.agentId ?? await input({
6629
+ message: "Agent UUID on the Hub:",
6630
+ validate: (v) => v.length > 0 ? true : "Agent UUID is required"
6631
+ });
6632
+ const token = await ensureFreshAccessToken();
6633
+ const res = await fetch(`${serverUrl}/api/v1/admin/agents/${encodeURIComponent(agentId)}`, {
6634
+ headers: { Authorization: `Bearer ${token}` },
6635
+ signal: AbortSignal.timeout(1e4)
6636
+ });
6637
+ if (!res.ok) throw new Error(`Failed to look up agent ${agentId}: HTTP ${res.status}`);
6638
+ const body = await res.json();
6639
+ if (!body.name) throw new Error(`Agent ${agentId} has no hub name (tombstoned or never named). Cannot add a local config without a name.`);
6405
6640
  return {
6406
- name: await input({
6407
- message: "Local alias:",
6408
- validate: (v) => /^[a-z0-9][a-z0-9-]*$/.test(v) ? true : "Lowercase alphanumeric and hyphens only"
6409
- }),
6410
- agentId: await input({
6411
- message: "Agent UUID on the Hub:",
6412
- validate: (v) => v.length > 0 ? true : "Agent UUID is required"
6413
- })
6641
+ name: body.name,
6642
+ agentId
6414
6643
  };
6415
6644
  }
6416
6645
  async function askPrompt(dotPath, prompt) {
@@ -7221,7 +7450,7 @@ function createFeedbackHandler(config) {
7221
7450
  return { handle };
7222
7451
  }
7223
7452
  //#endregion
7224
- //#region ../server/dist/app-UkoRS4YL.mjs
7453
+ //#region ../server/dist/app-DugUZNsw.mjs
7225
7454
  var __defProp = Object.defineProperty;
7226
7455
  var __exportAll = (all, no_symbols) => {
7227
7456
  let target = {};
@@ -7288,7 +7517,7 @@ const agents = pgTable("agents", {
7288
7517
  name: text("name"),
7289
7518
  organizationId: text("organization_id").notNull().references(() => organizations.id),
7290
7519
  type: text("type").notNull(),
7291
- displayName: text("display_name"),
7520
+ displayName: text("display_name").notNull(),
7292
7521
  delegateMention: text("delegate_mention"),
7293
7522
  inboxId: text("inbox_id").unique().notNull(),
7294
7523
  status: text("status").notNull().default("active"),
@@ -8279,13 +8508,14 @@ async function createAgent(db, data) {
8279
8508
  if (org && org.maxAgents > 0) {
8280
8509
  if (((await db.select({ value: count() }).from(agents).where(and(eq(agents.organizationId, orgId), ne(agents.status, AGENT_STATUSES.DELETED))))[0]?.value ?? 0) >= org.maxAgents) throw new ForbiddenError(`Organization "${orgId}" has reached its agent limit (${org.maxAgents}). Upgrade your plan or delete unused agents.`);
8281
8510
  }
8511
+ const resolvedDisplayName = data.displayName?.trim() || name || "Unnamed Agent";
8282
8512
  try {
8283
8513
  const [agent] = await db.insert(agents).values({
8284
8514
  uuid,
8285
8515
  name,
8286
8516
  organizationId: orgId,
8287
8517
  type: data.type,
8288
- displayName: data.displayName ?? null,
8518
+ displayName: resolvedDisplayName,
8289
8519
  delegateMention: data.delegateMention ?? null,
8290
8520
  inboxId,
8291
8521
  source: data.source ?? null,
@@ -9223,26 +9453,33 @@ const inboxEntries = pgTable("inbox_entries", {
9223
9453
  messageId: text("message_id").notNull().references(() => messages.id),
9224
9454
  chatId: text("chat_id"),
9225
9455
  status: text("status").notNull().default("pending"),
9456
+ notify: boolean("notify").notNull().default(true),
9226
9457
  retryCount: integer("retry_count").notNull().default(0),
9227
9458
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
9228
9459
  deliveredAt: timestamp("delivered_at", { withTimezone: true }),
9229
9460
  ackedAt: timestamp("acked_at", { withTimezone: true })
9230
- }, (table) => [unique("uq_inbox_delivery").on(table.inboxId, table.messageId, table.chatId), index("idx_inbox_pending").on(table.inboxId, table.createdAt)]);
9231
- async function sendMessage(db, chatId, senderId, data) {
9461
+ }, (table) => [
9462
+ unique("uq_inbox_delivery").on(table.inboxId, table.messageId, table.chatId),
9463
+ index("idx_inbox_pending").on(table.inboxId, table.createdAt),
9464
+ index("idx_inbox_pending_notify").on(table.inboxId, table.createdAt).where(sql`status = 'pending' AND notify = true`),
9465
+ index("idx_inbox_chat_silent").on(table.inboxId, table.chatId, table.notify, table.status)
9466
+ ]);
9467
+ async function sendMessage(db, chatId, senderId, data, options = {}) {
9232
9468
  return withSpan("inbox.enqueue", messageAttrs({
9233
9469
  chatId,
9234
9470
  senderAgentId: senderId,
9235
9471
  source: data.source ?? void 0
9236
- }), () => sendMessageInner(db, chatId, senderId, data));
9472
+ }), () => sendMessageInner(db, chatId, senderId, data, options));
9237
9473
  }
9238
- async function sendMessageInner(db, chatId, senderId, data) {
9474
+ async function sendMessageInner(db, chatId, senderId, data, options) {
9239
9475
  return db.transaction(async (tx) => {
9240
- const participants = await tx.select({
9476
+ const [participants, [chatRow]] = await Promise.all([tx.select({
9241
9477
  agentId: chatParticipants.agentId,
9242
9478
  inboxId: agents.inboxId,
9243
9479
  mode: chatParticipants.mode,
9244
9480
  name: agents.name
9245
- }).from(chatParticipants).innerJoin(agents, eq(chatParticipants.agentId, agents.uuid)).where(eq(chatParticipants.chatId, chatId));
9481
+ }).from(chatParticipants).innerJoin(agents, eq(chatParticipants.agentId, agents.uuid)).where(eq(chatParticipants.chatId, chatId)), tx.select({ type: chats.type }).from(chats).where(eq(chats.id, chatId)).limit(1)]);
9482
+ const chatType = chatRow?.type ?? null;
9246
9483
  if (data.replyToInbox !== void 0 && data.replyToInbox !== null) {
9247
9484
  const [senderRow] = await tx.select({ inboxId: agents.inboxId }).from(agents).where(eq(agents.uuid, senderId)).limit(1);
9248
9485
  if (!senderRow || senderRow.inboxId !== data.replyToInbox) throw new BadRequestError("replyToInbox must reference the sender's own inbox");
@@ -9257,13 +9494,32 @@ async function sendMessageInner(db, chatId, senderId, data) {
9257
9494
  ...incomingMeta,
9258
9495
  mentions: mergedMentions
9259
9496
  } : incomingMeta;
9497
+ if (options.enforceGroupMention && chatType === "group") {
9498
+ if (mergedMentions.filter((id) => id !== senderId).length === 0) throw new BadRequestError("Sending to a group chat requires an explicit @mention. Use `agent send <name>` to message a single agent, or @<name> in the content to address one or more group members.");
9499
+ }
9500
+ let outboundContent = data.content;
9501
+ if (options.normalizeMentionsInContent && typeof outboundContent === "string") {
9502
+ const present = new Set(scanMentionTokens(outboundContent));
9503
+ const missingNames = [];
9504
+ for (const id of mergedMentions) {
9505
+ if (id === senderId) continue;
9506
+ const p = participants.find((q) => q.agentId === id);
9507
+ if (!p?.name) continue;
9508
+ if (present.has(p.name.toLowerCase())) continue;
9509
+ missingNames.push(p.name);
9510
+ }
9511
+ if (missingNames.length > 0) {
9512
+ const prefix = missingNames.map((n) => `@${n}`).join(" ");
9513
+ outboundContent = outboundContent.length > 0 ? `${prefix} ${outboundContent}` : prefix;
9514
+ }
9515
+ }
9260
9516
  const messageId = randomUUID();
9261
9517
  const [msg] = await tx.insert(messages).values({
9262
9518
  id: messageId,
9263
9519
  chatId,
9264
9520
  senderId,
9265
9521
  format: data.format,
9266
- content: data.content,
9522
+ content: outboundContent,
9267
9523
  metadata: metadataToStore,
9268
9524
  replyToInbox: data.replyToInbox ?? null,
9269
9525
  replyToChat: data.replyToChat ?? null,
@@ -9271,13 +9527,14 @@ async function sendMessageInner(db, chatId, senderId, data) {
9271
9527
  source: data.source ?? null
9272
9528
  }).returning();
9273
9529
  const mentionSet = new Set(mergedMentions);
9274
- const entries = participants.filter((p) => p.agentId !== senderId).filter((p) => p.mode !== "mention_only" || mentionSet.has(p.agentId)).map((p) => ({
9530
+ const entries = participants.filter((p) => p.agentId !== senderId).map((p) => ({
9275
9531
  inboxId: p.inboxId,
9276
9532
  messageId,
9277
- chatId
9533
+ chatId,
9534
+ notify: p.mode !== "mention_only" || mentionSet.has(p.agentId)
9278
9535
  }));
9279
9536
  if (entries.length > 0) await tx.insert(inboxEntries).values(entries);
9280
- const recipients = entries.map((e) => e.inboxId);
9537
+ const recipients = entries.filter((e) => e.notify).map((e) => e.inboxId);
9281
9538
  if (data.inReplyTo) {
9282
9539
  const [original] = await tx.select({
9283
9540
  replyToInbox: messages.replyToInbox,
@@ -9308,14 +9565,23 @@ async function sendToAgent(db, senderUuid, targetName, data) {
9308
9565
  if (!sender) throw new NotFoundError(`Agent "${senderUuid}" not found`);
9309
9566
  const [target] = await db.select({ uuid: agents.uuid }).from(agents).where(and(eq(agents.organizationId, sender.organizationId), eq(agents.name, targetName), ne(agents.status, "deleted"))).limit(1);
9310
9567
  if (!target) throw new NotFoundError(`Agent "${targetName}" not found${/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(targetName) ? " — `agent send` expects an agent NAME, not a uuid. Run `first-tree-hub agent list` to see available names." : ""}`);
9311
- return sendMessage(db, (await findOrCreateDirectChat(db, senderUuid, target.uuid)).id, senderUuid, {
9568
+ const chat = await findOrCreateDirectChat(db, senderUuid, target.uuid);
9569
+ const incomingMeta = data.metadata ?? {};
9570
+ const existingMentionsRaw = incomingMeta.mentions;
9571
+ const existingMentions = Array.isArray(existingMentionsRaw) ? existingMentionsRaw.filter((m) => typeof m === "string") : [];
9572
+ const mergedMentions = existingMentions.includes(target.uuid) ? existingMentions : [...existingMentions, target.uuid];
9573
+ const metadata = {
9574
+ ...incomingMeta,
9575
+ mentions: mergedMentions
9576
+ };
9577
+ return sendMessage(db, chat.id, senderUuid, {
9312
9578
  format: data.format,
9313
9579
  content: data.content,
9314
- metadata: data.metadata,
9580
+ metadata,
9315
9581
  replyToInbox: data.replyToInbox,
9316
9582
  replyToChat: data.replyToChat,
9317
9583
  source: data.source
9318
- });
9584
+ }, { normalizeMentionsInContent: true });
9319
9585
  }
9320
9586
  async function editMessage(db, chatId, messageId, senderId, data) {
9321
9587
  const [msg] = await db.select().from(messages).where(eq(messages.id, messageId)).limit(1);
@@ -10059,7 +10325,7 @@ async function adminChatRoutes(app) {
10059
10325
  ...body,
10060
10326
  source: "hub_ui"
10061
10327
  });
10062
- const result = await sendMessage(app.db, chatId, member.agentId, prepared);
10328
+ const result = await sendMessage(app.db, chatId, member.agentId, prepared, { enforceGroupMention: true });
10063
10329
  notifyRecipients(app.notifier, result.recipients, result.message.id);
10064
10330
  return reply.status(201).send({
10065
10331
  id: result.message.id,
@@ -11882,7 +12148,7 @@ async function buildClientMessagePayloadsForInbox(db, inboxId, items) {
11882
12148
  replyToChat: o.replyToChat
11883
12149
  });
11884
12150
  }
11885
- return items.map(({ entryChatId, message: m }) => ({
12151
+ return items.map(({ entryChatId, message: m, precedingMessages = [] }) => ({
11886
12152
  id: m.id,
11887
12153
  chatId: m.chatId,
11888
12154
  senderId: m.senderId,
@@ -11896,7 +12162,8 @@ async function buildClientMessagePayloadsForInbox(db, inboxId, items) {
11896
12162
  createdAt: m.createdAt,
11897
12163
  configVersion: version,
11898
12164
  recipientMode: modeByChat.get(entryChatId ?? m.chatId) ?? "full",
11899
- inReplyToSnapshot: m.inReplyTo ? snapshotById.get(m.inReplyTo) ?? null : null
12165
+ inReplyToSnapshot: m.inReplyTo ? snapshotById.get(m.inReplyTo) ?? null : null,
12166
+ precedingMessages
11900
12167
  }));
11901
12168
  }
11902
12169
  async function resolveAgentId(db, source) {
@@ -11907,6 +12174,7 @@ async function resolveAgentId(db, source) {
11907
12174
  }
11908
12175
  const DEFAULT_INBOX_TIMEOUT_SECONDS = 300;
11909
12176
  const DEFAULT_MAX_RETRY_COUNT = 3;
12177
+ const PRECEDING_CONTEXT_WINDOW_SECONDS = 1440 * 60;
11910
12178
  async function pollInbox(db, inboxId, limit) {
11911
12179
  return withSpan("inbox.deliver", {
11912
12180
  "inbox.id": inboxId,
@@ -11920,7 +12188,7 @@ async function pollInboxInner(db, inboxId, limit) {
11920
12188
  SET status = 'delivered', delivered_at = NOW()
11921
12189
  WHERE id IN (
11922
12190
  SELECT id FROM inbox_entries
11923
- WHERE inbox_id = ${inboxId} AND status = 'pending'
12191
+ WHERE inbox_id = ${inboxId} AND status = 'pending' AND notify = true
11924
12192
  ORDER BY created_at
11925
12193
  LIMIT ${limit}
11926
12194
  FOR UPDATE SKIP LOCKED
@@ -11928,6 +12196,8 @@ async function pollInboxInner(db, inboxId, limit) {
11928
12196
  RETURNING *
11929
12197
  `);
11930
12198
  if (claimed.length === 0) return [];
12199
+ claimed.sort((a, b) => a.created_at.localeCompare(b.created_at));
12200
+ const precedingByEntryId = await collectPrecedingContext(tx, inboxId, claimed);
11931
12201
  const messageIds = claimed.map((e) => e.message_id);
11932
12202
  const msgs = await tx.select().from(messages).where(inArray(messages.id, messageIds));
11933
12203
  const msgMap = new Map(msgs.map((m) => [m.id, m]));
@@ -11936,6 +12206,7 @@ async function pollInboxInner(db, inboxId, limit) {
11936
12206
  if (!msg) throw new Error(`Unexpected: message ${entry.message_id} not found`);
11937
12207
  return {
11938
12208
  entryChatId: entry.chat_id,
12209
+ precedingMessages: precedingByEntryId.get(entry.id) ?? [],
11939
12210
  message: {
11940
12211
  id: msg.id,
11941
12212
  chatId: msg.chatId,
@@ -11969,6 +12240,69 @@ async function pollInboxInner(db, inboxId, limit) {
11969
12240
  });
11970
12241
  });
11971
12242
  }
12243
+ /**
12244
+ * Per claimed trigger: SELECT silent (notify=false) pending rows in the same
12245
+ * chat that occurred between the previous trigger in this batch (or beginning
12246
+ * of time) and this trigger, capped by `PRECEDING_CONTEXT_MAX_ENTRIES` and
12247
+ * `PRECEDING_CONTEXT_WINDOW_SECONDS`. Returned messages are oldest-first.
12248
+ *
12249
+ * Side effect: bulk-ack ALL silent pending rows in each chat with
12250
+ * created_at < latest_trigger.created_at — including ones that fell outside
12251
+ * the window/cap. Otherwise stale silent rows would accumulate and re-load
12252
+ * on every poll.
12253
+ */
12254
+ async function collectPrecedingContext(tx, inboxId, triggers) {
12255
+ const result = /* @__PURE__ */ new Map();
12256
+ const byChat = /* @__PURE__ */ new Map();
12257
+ for (const t of triggers) {
12258
+ if (t.chat_id === null) continue;
12259
+ const list = byChat.get(t.chat_id) ?? [];
12260
+ list.push(t);
12261
+ byChat.set(t.chat_id, list);
12262
+ }
12263
+ for (const [chatId, chatTriggers] of byChat) {
12264
+ chatTriggers.sort((a, b) => a.created_at.localeCompare(b.created_at));
12265
+ let prevCreatedAt = null;
12266
+ for (const trigger of chatTriggers) {
12267
+ const preceding = (await tx.execute(sql`
12268
+ SELECT ie.id, m.id AS message_id, m.sender_id, m.format, m.content, m.metadata,
12269
+ m.created_at
12270
+ FROM inbox_entries ie
12271
+ JOIN messages m ON m.id = ie.message_id
12272
+ WHERE ie.inbox_id = ${inboxId}
12273
+ AND ie.chat_id = ${chatId}
12274
+ AND ie.status = 'pending'
12275
+ AND ie.notify = false
12276
+ AND ie.created_at < ${trigger.created_at}
12277
+ ${prevCreatedAt === null ? sql`` : sql`AND ie.created_at > ${prevCreatedAt}`}
12278
+ AND ie.created_at > NOW() - make_interval(secs => ${PRECEDING_CONTEXT_WINDOW_SECONDS})
12279
+ ORDER BY ie.created_at DESC
12280
+ LIMIT ${50}
12281
+ FOR UPDATE OF ie SKIP LOCKED
12282
+ `)).map((r) => ({
12283
+ id: r.message_id,
12284
+ senderId: r.sender_id,
12285
+ format: r.format,
12286
+ content: r.content,
12287
+ metadata: r.metadata ?? {},
12288
+ createdAt: r.created_at
12289
+ })).reverse();
12290
+ result.set(trigger.id, preceding);
12291
+ prevCreatedAt = trigger.created_at;
12292
+ }
12293
+ const latestTrigger = chatTriggers[chatTriggers.length - 1];
12294
+ if (latestTrigger) await tx.execute(sql`
12295
+ UPDATE inbox_entries
12296
+ SET status = 'acked', acked_at = NOW()
12297
+ WHERE inbox_id = ${inboxId}
12298
+ AND chat_id = ${chatId}
12299
+ AND status = 'pending'
12300
+ AND notify = false
12301
+ AND created_at < ${latestTrigger.created_at}
12302
+ `);
12303
+ }
12304
+ return result;
12305
+ }
11972
12306
  async function ackEntry$2(db, entryId, inboxId) {
11973
12307
  return withSpan("inbox.ack", {
11974
12308
  [FIRST_TREE_HUB_ATTR.INBOX_ENTRY_ID]: String(entryId),
@@ -12007,6 +12341,49 @@ async function resetTimedOutEntries(db, timeoutSeconds = DEFAULT_INBOX_TIMEOUT_S
12007
12341
  failed: failedResult.length
12008
12342
  };
12009
12343
  }
12344
+ /** Default age (30 days) past which silent rows that no notify-true delivery
12345
+ * ever picked up are physically deleted. */
12346
+ const SILENT_ROW_GC_MAX_AGE_SECONDS = 720 * 60 * 60;
12347
+ /**
12348
+ * Garbage-collect silent inbox rows so the table doesn't grow forever in
12349
+ * chats where a `mention_only` agent is never @mentioned.
12350
+ *
12351
+ * Two cleanup paths:
12352
+ *
12353
+ * 1. `notify=false AND status='acked'` of any age — these are fully
12354
+ * consumed (either bundled into a previous trigger or aged out via the
12355
+ * bulk-ack in `collectPrecedingContext`); keep them only as long as
12356
+ * the corresponding message rows we link to. The unique constraint
12357
+ * `(inbox_id, message_id, chat_id)` means leaving them around blocks
12358
+ * legitimate retries with the same key.
12359
+ *
12360
+ * 2. `notify=false AND status='pending' AND created_at < NOW() - maxAge` —
12361
+ * stale silent rows that no trigger ever caught up with. After 30
12362
+ * days they're useless as preceding context (the @mention almost
12363
+ * certainly already happened or the chat went dormant).
12364
+ *
12365
+ * Returns the number of rows deleted in each bucket so the background task
12366
+ * can log meaningful counts.
12367
+ */
12368
+ async function pruneStaleSilentEntries(db, maxAgeSeconds = SILENT_ROW_GC_MAX_AGE_SECONDS) {
12369
+ const ackedResult = await db.execute(sql`
12370
+ DELETE FROM inbox_entries
12371
+ WHERE notify = false
12372
+ AND status = 'acked'
12373
+ RETURNING id
12374
+ `);
12375
+ const staleResult = await db.execute(sql`
12376
+ DELETE FROM inbox_entries
12377
+ WHERE notify = false
12378
+ AND status = 'pending'
12379
+ AND created_at < NOW() - make_interval(secs => ${maxAgeSeconds})
12380
+ RETURNING id
12381
+ `);
12382
+ return {
12383
+ ackedDeleted: ackedResult.length,
12384
+ stalePendingDeleted: staleResult.length
12385
+ };
12386
+ }
12010
12387
  async function agentInboxRoutes(app) {
12011
12388
  app.get("/", async (request) => {
12012
12389
  const identity = requireAgent(request);
@@ -12054,7 +12431,10 @@ async function agentMessageRoutes(app) {
12054
12431
  await assertParticipant(app.db, request.params.chatId, identity.uuid);
12055
12432
  const body = sendMessageSchema.parse(request.body);
12056
12433
  const prepared = await prepareImageOutbound(app.db, app.notifier, request.params.chatId, body);
12057
- const { message: msg, recipients } = await sendMessage(app.db, request.params.chatId, identity.uuid, prepared);
12434
+ const { message: msg, recipients } = await sendMessage(app.db, request.params.chatId, identity.uuid, prepared, {
12435
+ enforceGroupMention: true,
12436
+ normalizeMentionsInContent: true
12437
+ });
12058
12438
  notifyRecipients(app.notifier, recipients, msg.id);
12059
12439
  return reply.status(201).send({
12060
12440
  ...msg,
@@ -14167,6 +14547,11 @@ function createBackgroundTasks(app, instanceId, adapterManager, kaelRuntime) {
14167
14547
  const timeoutSeconds = configs.inbox_timeout_seconds ?? 300;
14168
14548
  const maxRetries = configs.max_retry_count ?? 3;
14169
14549
  await resetTimedOutEntries(app.db, timeoutSeconds, maxRetries);
14550
+ const pruned = await pruneStaleSilentEntries(app.db);
14551
+ if (pruned.ackedDeleted > 0 || pruned.stalePendingDeleted > 0) log.debug({
14552
+ ackedDeleted: pruned.ackedDeleted,
14553
+ stalePendingDeleted: pruned.stalePendingDeleted
14554
+ }, "pruned silent inbox rows");
14170
14555
  } catch (err) {
14171
14556
  log.error({ err }, "failed to reset timed-out inbox entries");
14172
14557
  }
@@ -15329,4 +15714,4 @@ function createExecuteUpdate({ managed }) {
15329
15714
  };
15330
15715
  }
15331
15716
  //#endregion
15332
- export { resolveCliInvocation as A, resolveReplyToFromEnv as B, checkServerHealth as C, getClientServiceStatus as D, printResults as E, ClientRuntime as F, ClientOrgMismatchError$1 as G, print as H, handleClientOrgMismatch as I, SessionRegistry as J, FirstTreeHubSDK as K, rotateClientIdWithBackup as L, ensurePostgres as M, isDockerAvailable as N, installClientService as O, stopPostgres as P, createOwner as R, checkServerConfig as S, checkWebSocket as T, setJsonMode as U, blank as V, status as W, applyClientLoggerConfig as X, cleanWorkspaces as Y, configureClientLoggerForService as Z, checkBackgroundService as _, COMMAND_VERSION as a, checkDocker as b, promptMissingFields as c, onboardCheck as d, onboardCreate as f, checkAgentConfigs as g, runMigrations as h, startServer as i, uninstallClientService as j, isServiceSupported as k, formatCheckReport as l, runHomeMigration as m, declineUpdate as n, isInteractive as o, saveOnboardState as p, SdkError as q, promptUpdate as r, promptAddAgent as s, createExecuteUpdate as t, loadOnboardState as u, checkClientConfig as v, checkServerReachable as w, checkNodeVersion as x, checkDatabase as y, hasUser as z };
15717
+ export { configureClientLoggerForService as $, installClientService as A, createOwner as B, checkNodeVersion as C, checkWebSocket as D, checkServerReachable as E, isDockerAvailable as F, setJsonMode as G, resolveReplyToFromEnv as H, stopPostgres as I, FirstTreeHubSDK as J, status as K, ClientRuntime as L, resolveCliInvocation as M, uninstallClientService as N, printResults as O, ensurePostgres as P, applyClientLoggerConfig as Q, handleClientOrgMismatch as R, checkDocker as S, checkServerHealth as T, blank as U, hasUser as V, print as W, SessionRegistry as X, SdkError as Y, cleanWorkspaces as Z, runMigrations as _, COMMAND_VERSION as a, checkClientConfig as b, promptMissingFields as c, onboardCheck as d, onboardCreate as f, migrateLocalAgentDirs as g, createApiNameResolver as h, startServer as i, isServiceSupported as j, getClientServiceStatus as k, formatCheckReport as l, runHomeMigration as m, declineUpdate as n, isInteractive as o, saveOnboardState as p, ClientOrgMismatchError$1 as q, promptUpdate as r, promptAddAgent as s, createExecuteUpdate as t, loadOnboardState as u, checkAgentConfigs as v, checkServerConfig as w, checkDatabase as x, checkBackgroundService as y, rotateClientIdWithBackup as z };