@agent-team-foundation/first-tree-hub 0.9.11 → 0.10.1
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.
- package/dist/cli/index.mjs +35 -19
- package/dist/{core-CuSIXoof.mjs → core-BgiFGT7Y.mjs} +435 -50
- package/dist/drizzle/0024_display_name_not_null.sql +31 -0
- package/dist/drizzle/0025_inbox_silent_entries.sql +53 -0
- package/dist/drizzle/meta/_journal.json +14 -0
- package/dist/{feishu-B2sjp6Z6.mjs → feishu-DEmwoNn_.mjs} +45 -7
- package/dist/index.mjs +2 -2
- package/dist/web/assets/{index-DStPeqrX.css → index-Cd290Lq6.css} +1 -1
- package/dist/web/assets/index-xi7JmCtW.js +361 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-Dwp1u5SF.js +0 -371
|
@@ -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
|
|
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().
|
|
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()
|
|
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()
|
|
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()
|
|
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 ??
|
|
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
|
-
|
|
3689
|
-
|
|
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
|
|
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
|
-
|
|
6301
|
-
|
|
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
|
-
|
|
6318
|
-
|
|
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-
|
|
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
|
|
6403
|
-
|
|
6404
|
-
|
|
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:
|
|
6407
|
-
|
|
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-
|
|
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:
|
|
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) => [
|
|
9231
|
-
|
|
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:
|
|
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).
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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 };
|