@agent-team-foundation/first-tree-hub 0.12.4 → 0.12.6
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/{bootstrap-C_K2CKXC.mjs → bootstrap-BCZC1ki6.mjs} +15 -5
- package/dist/cli/index.mjs +16 -14
- package/dist/{client-D1TDiik_-NV_lkhfI.mjs → client-B89AKi3Q-DAyGdQSq.mjs} +182 -86
- package/dist/{client-0RrgrMjR-CylTJGEb.mjs → client-GOgUQxVe-Dqk9oZf9.mjs} +2 -2
- package/dist/{dist-CMhywpXB.mjs → dist-xP6NpdMp.mjs} +146 -8
- package/dist/drizzle/0036_github_entity_chat_mappings.sql +47 -0
- package/dist/drizzle/0037_github_app_installations.sql +52 -0
- package/dist/drizzle/meta/_journal.json +14 -0
- package/dist/{feishu-tkZS0vvL.mjs → feishu-CsfadBKa.mjs} +1 -1
- package/dist/index.mjs +5 -5
- package/dist/{invitation-C299fxkP-CZRV665C.mjs → invitation-C299fxkP-DFBBuUcj.mjs} +1 -1
- package/dist/{saas-connect-S71rG182.mjs → saas-connect-RCN8zL5e.mjs} +1657 -411
- package/dist/web/assets/{index-RNegidl2.js → index-BHNq2Nl1.js} +1 -1
- package/dist/web/assets/index-BaLvRwAX.js +416 -0
- package/dist/web/assets/index-BdW7weV1.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-BG9RRx2e.js +0 -401
- package/dist/web/assets/index-CbOOQaWp.css +0 -1
|
@@ -576,12 +576,22 @@ const serverConfigSchema = defineConfig({
|
|
|
576
576
|
}),
|
|
577
577
|
githubTokenRepos: field(z.string().optional(), { env: "FIRST_TREE_HUB_CONTEXT_TREE_GITHUB_TOKEN_REPOS" })
|
|
578
578
|
}),
|
|
579
|
-
oauth: optional({
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
579
|
+
oauth: optional({ githubApp: optional({
|
|
580
|
+
appId: field(z.string().min(1), { env: "FIRST_TREE_HUB_GITHUB_APP_ID" }),
|
|
581
|
+
clientId: field(z.string().min(1), { env: "FIRST_TREE_HUB_GITHUB_APP_CLIENT_ID" }),
|
|
582
|
+
clientSecret: field(z.string().min(1), {
|
|
583
|
+
env: "FIRST_TREE_HUB_GITHUB_APP_CLIENT_SECRET",
|
|
583
584
|
secret: true
|
|
584
|
-
})
|
|
585
|
+
}),
|
|
586
|
+
privateKeyPem: field(z.string().min(1), {
|
|
587
|
+
env: "FIRST_TREE_HUB_GITHUB_APP_PRIVATE_KEY",
|
|
588
|
+
secret: true
|
|
589
|
+
}),
|
|
590
|
+
webhookSecret: field(z.string().min(1), {
|
|
591
|
+
env: "FIRST_TREE_HUB_GITHUB_APP_WEBHOOK_SECRET",
|
|
592
|
+
secret: true
|
|
593
|
+
}),
|
|
594
|
+
slug: field(z.string().min(1).optional(), { env: "FIRST_TREE_HUB_GITHUB_APP_SLUG" })
|
|
585
595
|
}) }),
|
|
586
596
|
cors: optional({ origin: field(z.string(), { env: "FIRST_TREE_HUB_CORS_ORIGIN" }) }),
|
|
587
597
|
trustProxy: field(z.boolean().default(false), { env: "FIRST_TREE_HUB_TRUST_PROXY" }),
|
package/dist/cli/index.mjs
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import "../observability-BAScT_5S-BcW9HgkG.mjs";
|
|
3
|
-
import { $ as formatStaleReason, A as checkDocker, B as isServiceSupported, C as createApiNameResolver, D as checkBackgroundService, E as checkAgentConfigs, F as checkWebSocket, H as restartClientService, I as printResults, J as stopPostgres, L as reconcileAgentConfigs, M as checkServerConfig, N as checkServerHealth, O as checkClientConfig, P as checkServerReachable, Q as findStaleAliases, R as getClientServiceStatus, S as runHomeMigration, T as runMigrations, U as startClientService, W as stopClientService, X as handleClientOrgMismatch, Y as ClientRuntime, _ as formatCheckReport, a as declineUpdate, at as
|
|
3
|
+
import { $ as formatStaleReason, A as checkDocker, B as isServiceSupported, C as createApiNameResolver, D as checkBackgroundService, E as checkAgentConfigs, F as checkWebSocket, H as restartClientService, I as printResults, J as stopPostgres, L as reconcileAgentConfigs, M as checkServerConfig, N as checkServerHealth, O as checkClientConfig, P as checkServerReachable, Q as findStaleAliases, R as getClientServiceStatus, S as runHomeMigration, T as runMigrations, U as startClientService, W as stopClientService, X as handleClientOrgMismatch, Y as ClientRuntime, _ as formatCheckReport, a as declineUpdate, at as fail, b as onboardCreate, c as detectInstallMode, ct as ClientUserMismatchError, d as startServer, dt as SessionRegistry, et as removeLocalAgent, f as reconcileLocalRuntimeProviders, ft as cleanWorkspaces, g as promptMissingFields, h as promptAddAgent, ht as configureClientLoggerForService, i as createExecuteUpdate, it as resolveSenderName, j as checkNodeVersion, k as checkDatabase, l as fetchLatestVersion, lt as FirstTreeHubSDK, m as isInteractive, mt as applyClientLoggerConfig, o as promptUpdate, ot as success, p as uploadClientCapabilities, pt as probeCapabilities, r as registerSaaSConnectCommand, rt as resolveReplyToFromEnv, s as PACKAGE_NAME, st as ClientOrgMismatchError, tt as createOwner, u as installGlobalLatest, ut as SdkError, v as loadOnboardState, w as migrateLocalAgentDirs, x as saveOnboardState, y as onboardCheck, z as installClientService } from "../saas-connect-RCN8zL5e.mjs";
|
|
4
4
|
import "../logger-core-BTmvdflj-DjW8FM4T.mjs";
|
|
5
|
-
import { C as resetConfigMeta, E as setConfigValue, S as resetConfig, T as serverConfigSchema, _ as getConfigValue, a as ensureFreshAdminToken, c as resolveServerUrl, d as DEFAULT_CONFIG_DIR, f as DEFAULT_DATA_DIR, h as clientConfigSchema, i as ensureFreshAccessToken, l as saveAgentConfig, m as agentConfigSchema, o as loadCredentials, p as DEFAULT_HOME_DIR, u as saveCredentials, v as initConfig, w as resolveConfigReadonly, x as readConfigFile, y as loadAgents } from "../bootstrap-
|
|
5
|
+
import { C as resetConfigMeta, E as setConfigValue, S as resetConfig, T as serverConfigSchema, _ as getConfigValue, a as ensureFreshAdminToken, c as resolveServerUrl, d as DEFAULT_CONFIG_DIR, f as DEFAULT_DATA_DIR, h as clientConfigSchema, i as ensureFreshAccessToken, l as saveAgentConfig, m as agentConfigSchema, o as loadCredentials, p as DEFAULT_HOME_DIR, u as saveCredentials, v as initConfig, w as resolveConfigReadonly, x as readConfigFile, y as loadAgents } from "../bootstrap-BCZC1ki6.mjs";
|
|
6
6
|
import { a as print, n as CLI_USER_AGENT, o as setJsonMode, r as COMMAND_VERSION, t as cliFetch } from "../cli-fetch--tiwKm5S.mjs";
|
|
7
|
-
import "../dist-
|
|
8
|
-
import { n as bindFeishuUser, t as bindFeishuBot } from "../feishu-
|
|
7
|
+
import "../dist-xP6NpdMp.mjs";
|
|
8
|
+
import { n as bindFeishuUser, t as bindFeishuBot } from "../feishu-CsfadBKa.mjs";
|
|
9
9
|
import "../errors-CF5evtJt-B0NTIVPt.mjs";
|
|
10
10
|
import "../src-DNBS5Yjj.mjs";
|
|
11
|
-
import "../client-
|
|
11
|
+
import "../client-B89AKi3Q-DAyGdQSq.mjs";
|
|
12
12
|
import "../invitation-Bg0TRiyx-BsZH4GCS.mjs";
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
import { existsSync, mkdirSync, readFileSync, readdirSync } from "node:fs";
|
|
@@ -215,14 +215,16 @@ function resolveLocalAgent(agentName) {
|
|
|
215
215
|
schema: agentConfigSchema,
|
|
216
216
|
agentsDir
|
|
217
217
|
});
|
|
218
|
-
|
|
218
|
+
const resolution = resolveSenderName({
|
|
219
|
+
override: agentName,
|
|
220
|
+
envAgentId: process.env.FIRST_TREE_HUB_AGENT_ID,
|
|
221
|
+
agents
|
|
222
|
+
});
|
|
219
223
|
let resolvedName;
|
|
220
|
-
if (
|
|
221
|
-
else if (
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
resolvedName = only;
|
|
225
|
-
} else fail("AMBIGUOUS_AGENT", `Multiple agents configured — specify --agent <name>. Available: ${[...agents.keys()].join(", ")}`, 2);
|
|
224
|
+
if (resolution.kind === "ok") resolvedName = resolution.name;
|
|
225
|
+
else if (resolution.kind === "none") fail("MISSING_AGENT", "No agent configured. Run `first-tree-hub agent add` first.", 2);
|
|
226
|
+
else if (resolution.kind === "envMismatch") fail("ENV_AGENT_NOT_LOCAL", `FIRST_TREE_HUB_AGENT_ID="${resolution.envAgentId}" is not configured on this machine. Available local agents: ${resolution.available.join(", ")}. Pick one explicitly: \`first-tree-hub agent send --agent <senderName> <recipientName> "..."\`.`, 2);
|
|
227
|
+
else fail("AMBIGUOUS_AGENT", `Multiple agents are configured on this machine (${resolution.available.join(", ")}) and FIRST_TREE_HUB_AGENT_ID is not set, so the CLI can't tell which one is the sender. Specify it explicitly: \`first-tree-hub agent send --agent <senderName> <recipientName> "..."\` (--agent picks the SENDER; the recipient is the next positional argument).`, 2);
|
|
226
228
|
const cfg = agents.get(resolvedName);
|
|
227
229
|
if (!cfg) fail("UNKNOWN_AGENT", `Agent "${resolvedName}" not found in ${agentsDir}`, 2);
|
|
228
230
|
let serverUrl;
|
|
@@ -1670,13 +1672,13 @@ function isSecretField(schema, dotPath) {
|
|
|
1670
1672
|
//#region src/commands/onboard.ts
|
|
1671
1673
|
async function promptMissing(args) {
|
|
1672
1674
|
if (!args.server) try {
|
|
1673
|
-
const { resolveServerUrl } = await import("../bootstrap-
|
|
1675
|
+
const { resolveServerUrl } = await import("../bootstrap-BCZC1ki6.mjs").then((n) => n.r);
|
|
1674
1676
|
resolveServerUrl();
|
|
1675
1677
|
} catch {
|
|
1676
1678
|
args.server = await input({ message: "Hub server URL:" });
|
|
1677
1679
|
saveOnboardState(args);
|
|
1678
1680
|
}
|
|
1679
|
-
const { loadCredentials } = await import("../bootstrap-
|
|
1681
|
+
const { loadCredentials } = await import("../bootstrap-BCZC1ki6.mjs").then((n) => n.r);
|
|
1680
1682
|
if (!loadCredentials()) throw new Error("No saved credentials. Run `first-tree-hub client connect <server-url>` before onboarding.");
|
|
1681
1683
|
if (!args.id) {
|
|
1682
1684
|
args.id = await input({ message: "Agent ID:" });
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { O as withSpan, f as messageAttrs, s as createLogger } from "./observability-BAScT_5S-BcW9HgkG.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { $ as questionMessageContentSchema, M as extractMentions, O as defaultParticipantMode, Q as questionAnswerMessageContentSchema, _ as clientCapabilitiesSchema, a as AGENT_VISIBILITY, h as agentTypeSchema, i as AGENT_STATUSES, it as scanMentionTokens } from "./dist-xP6NpdMp.mjs";
|
|
3
3
|
import { a as ConflictError, i as ClientUserMismatchError, l as organizations, n as BadRequestError, o as ForbiddenError, s as NotFoundError, u as users } from "./errors-CF5evtJt-B0NTIVPt.mjs";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
5
|
import { and, desc, eq, inArray, isNotNull, lt, ne, or, sql } from "drizzle-orm";
|
|
6
6
|
import { bigserial, boolean, index, integer, jsonb, pgTable, primaryKey, text, timestamp, unique } from "drizzle-orm/pg-core";
|
|
7
|
-
//#region ../server/dist/client-
|
|
7
|
+
//#region ../server/dist/client-B89AKi3Q.mjs
|
|
8
8
|
/**
|
|
9
9
|
* Client connections. A client is a single SDK process (AgentRuntime) that may
|
|
10
10
|
* host multiple agents. From the unified-user-token milestone on, a client is
|
|
@@ -185,6 +185,140 @@ function invalidateChatAudience(chatId) {
|
|
|
185
185
|
cache.delete(chatId);
|
|
186
186
|
}
|
|
187
187
|
/**
|
|
188
|
+
* Single source of truth for writing `chat_participants`.
|
|
189
|
+
*
|
|
190
|
+
* **This is the ONLY place in the codebase that may `INSERT` into the
|
|
191
|
+
* `chat_participants` table.** Do not call `tx.insert(chatParticipants)`
|
|
192
|
+
* or `db.insert(chatParticipants)` from anywhere else. The original bug
|
|
193
|
+
* (docs/chat-participant-mode-fix-design.md §1.1) was caused by ten
|
|
194
|
+
* scattered insert sites each re-deriving the `mode` rule, several of
|
|
195
|
+
* which violated `group + non-human ⇒ mention_only`. Re-introducing a
|
|
196
|
+
* second writer reopens that hole — please don't.
|
|
197
|
+
*
|
|
198
|
+
* Test fixtures under `src/__tests__/` that deliberately seed
|
|
199
|
+
* pathological rows (e.g. cross-org pollution tests) may bypass this
|
|
200
|
+
* rule; they are setting up "what bad data looks like" rather than
|
|
201
|
+
* exercising the production write path.
|
|
202
|
+
*
|
|
203
|
+
* All callers that need to add a participant — `createChat`, `addParticipant`,
|
|
204
|
+
* `ensureParticipant`, `joinChat`, `createMeChat`, `addMeChatParticipants`,
|
|
205
|
+
* `findOrCreateDirectChat`, `findOrCreateChatForChannel`, `joinAsParticipant`,
|
|
206
|
+
* … — go through `addChatParticipants`. The function performs ONE round-trip
|
|
207
|
+
* to read `chats.type` + every involved `agents.type`, runs each row through
|
|
208
|
+
* `defaultParticipantMode`, and inserts the result. `agents.type` is parsed
|
|
209
|
+
* through the shared `agentTypeSchema` so schema drift surfaces loudly
|
|
210
|
+
* instead of silently coercing to a default.
|
|
211
|
+
*
|
|
212
|
+
* `changeChatType` complements it on the type-flip path: when a `direct`
|
|
213
|
+
* chat is being upgraded to `group` by the very next participant insert, the
|
|
214
|
+
* existing non-human rows must be re-graded to `mention_only`. Callers that
|
|
215
|
+
* trigger an upgrade are expected to invoke `changeChatType` BEFORE
|
|
216
|
+
* `addChatParticipants`, inside the same transaction, so the new row picks
|
|
217
|
+
* up the post-upgrade `chats.type` and existing rows get re-graded together.
|
|
218
|
+
*/
|
|
219
|
+
/**
|
|
220
|
+
* Insert participant rows whose `mode` is derived from `(chats.type, agents.type)`.
|
|
221
|
+
*
|
|
222
|
+
* Reads:
|
|
223
|
+
* - `chats.type` for the target chat (NotFoundError on missing)
|
|
224
|
+
* - `agents.type` for every requested participant (BadRequestError on missing)
|
|
225
|
+
*
|
|
226
|
+
* Mode derivation:
|
|
227
|
+
* - for each row, `peerAgentTypes` is the type of every OTHER participant
|
|
228
|
+
* being inserted in the same call PLUS every EXISTING participant of
|
|
229
|
+
* the chat. This matters only for `direct` chats; the helper ignores
|
|
230
|
+
* it for `group` / `thread`.
|
|
231
|
+
*
|
|
232
|
+
* Writes one INSERT (multi-row) per call.
|
|
233
|
+
*
|
|
234
|
+
* No watcher / audience-cache side effects — the caller owns those, since
|
|
235
|
+
* different entrypoints have different surrounding work (state-carry, watcher
|
|
236
|
+
* recompute, audience invalidation). Keeping this module side-effect-free
|
|
237
|
+
* makes it testable from any tx context.
|
|
238
|
+
*/
|
|
239
|
+
async function addChatParticipants(tx, chatId, participants, options = {}) {
|
|
240
|
+
if (participants.length === 0) return;
|
|
241
|
+
const [chat] = await tx.select({ type: chats.type }).from(chats).where(eq(chats.id, chatId)).limit(1);
|
|
242
|
+
if (!chat) throw new NotFoundError(`Chat "${chatId}" not found`);
|
|
243
|
+
if (chat.type !== "direct" && chat.type !== "group" && chat.type !== "thread") throw new Error(`Unexpected chat type "${chat.type}" for chat "${chatId}"`);
|
|
244
|
+
const chatType = chat.type;
|
|
245
|
+
const agentIds = participants.map((p) => p.agentId);
|
|
246
|
+
const agentRows = await tx.select({
|
|
247
|
+
uuid: agents.uuid,
|
|
248
|
+
type: agents.type
|
|
249
|
+
}).from(agents).where(inArray(agents.uuid, agentIds));
|
|
250
|
+
const agentTypeById = /* @__PURE__ */ new Map();
|
|
251
|
+
for (const row of agentRows) agentTypeById.set(row.uuid, row.type);
|
|
252
|
+
const missing = agentIds.filter((id) => !agentTypeById.has(id));
|
|
253
|
+
if (missing.length > 0) throw new BadRequestError(`Agents not found: ${missing.join(", ")}`);
|
|
254
|
+
if (options.assertHuman) {
|
|
255
|
+
const nonHuman = agentRows.filter((a) => a.type !== "human");
|
|
256
|
+
if (nonHuman.length > 0) throw new BadRequestError(`assertHuman violated: agents must be of type 'human' but got ${nonHuman.map((a) => `${a.uuid}=${a.type}`).join(", ")}`);
|
|
257
|
+
}
|
|
258
|
+
let existingAgentTypes = [];
|
|
259
|
+
if (chatType === "direct") existingAgentTypes = await loadExistingAgentTypes(tx, chatId, new Set(agentIds));
|
|
260
|
+
const rows = participants.map((spec) => {
|
|
261
|
+
const rawAgentType = agentTypeById.get(spec.agentId);
|
|
262
|
+
if (rawAgentType === void 0) throw new Error("Unexpected: agent type lookup unset after presence check");
|
|
263
|
+
const agentType = agentTypeSchema.parse(rawAgentType);
|
|
264
|
+
const peerTypesForRow = chatType === "direct" ? [...existingAgentTypes, ...participants.filter((p) => p.agentId !== spec.agentId).map((p) => agentTypeById.get(p.agentId)).filter((t) => t !== void 0)].map((t) => agentTypeSchema.parse(t)) : [];
|
|
265
|
+
return {
|
|
266
|
+
chatId,
|
|
267
|
+
agentId: spec.agentId,
|
|
268
|
+
role: spec.role ?? "member",
|
|
269
|
+
mode: defaultParticipantMode(chatType, agentType, peerTypesForRow),
|
|
270
|
+
lastReadAt: spec.carriedReadState?.lastReadAt ?? null,
|
|
271
|
+
unreadMentionCount: spec.carriedReadState?.unreadMentionCount ?? 0
|
|
272
|
+
};
|
|
273
|
+
});
|
|
274
|
+
const insert = tx.insert(chatParticipants).values(rows);
|
|
275
|
+
if (options.onConflictDoNothing) await insert.onConflictDoNothing({ target: [chatParticipants.chatId, chatParticipants.agentId] });
|
|
276
|
+
else await insert;
|
|
277
|
+
}
|
|
278
|
+
async function loadExistingAgentTypes(tx, chatId, excludeAgentIds) {
|
|
279
|
+
return (await tx.select({
|
|
280
|
+
type: agents.type,
|
|
281
|
+
agentId: chatParticipants.agentId
|
|
282
|
+
}).from(chatParticipants).innerJoin(agents, eq(chatParticipants.agentId, agents.uuid)).where(eq(chatParticipants.chatId, chatId))).filter((r) => !excludeAgentIds.has(r.agentId)).map((r) => r.type);
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Upgrade `chats.type` from `direct` → `group` AND re-grade every existing
|
|
286
|
+
* non-human participant to `mention_only`. Idempotent: if `chat.type` is
|
|
287
|
+
* already `group` (or any non-`direct` value), no-op.
|
|
288
|
+
*
|
|
289
|
+
* Callers that are about to insert a 3rd participant on a `direct` chat
|
|
290
|
+
* invoke this BEFORE `addChatParticipants` so the new row picks up the
|
|
291
|
+
* post-upgrade `chats.type` and the existing rows are re-graded in the
|
|
292
|
+
* same transaction.
|
|
293
|
+
*
|
|
294
|
+
* Note: this is the replacement for `services/chat.ts`'s
|
|
295
|
+
* `maybeUpgradeDirectToGroup` (the one in `services/watcher.ts` is
|
|
296
|
+
* removed). Keep the rename: `changeChatType` is more precise about the
|
|
297
|
+
* primary mutation; `maybe…ToGroup` overstated the conditional gate.
|
|
298
|
+
*/
|
|
299
|
+
async function changeChatType(tx, chatId, newType) {
|
|
300
|
+
const [chat] = await tx.select({ type: chats.type }).from(chats).where(eq(chats.id, chatId)).limit(1);
|
|
301
|
+
if (!chat) throw new NotFoundError(`Chat "${chatId}" not found`);
|
|
302
|
+
if (chat.type === newType) return;
|
|
303
|
+
if (newType === "group" && chat.type !== "direct") throw new BadRequestError(`Cannot change chat type from "${chat.type}" to "${newType}"`);
|
|
304
|
+
await tx.update(chats).set({
|
|
305
|
+
type: newType,
|
|
306
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
307
|
+
}).where(eq(chats.id, chatId));
|
|
308
|
+
const ids = (await tx.select({ agentId: chatParticipants.agentId }).from(chatParticipants).innerJoin(agents, eq(chatParticipants.agentId, agents.uuid)).where(and(eq(chatParticipants.chatId, chatId), ne(agents.type, "human")))).map((r) => r.agentId);
|
|
309
|
+
if (ids.length === 0) return;
|
|
310
|
+
await tx.update(chatParticipants).set({ mode: "mention_only" }).where(and(eq(chatParticipants.chatId, chatId), inArray(chatParticipants.agentId, ids)));
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Heuristic for whether an insert about to happen would push the chat past
|
|
314
|
+
* the direct → group threshold. Pure helper so callers can decide whether
|
|
315
|
+
* to call `changeChatType` before `addChatParticipants` without re-deriving
|
|
316
|
+
* the rule locally.
|
|
317
|
+
*/
|
|
318
|
+
function wouldUpgradeToGroup(currentParticipantCount, newParticipantCount) {
|
|
319
|
+
return currentParticipantCount + newParticipantCount >= 3;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
188
322
|
* Chat-first workspace — watcher subscription helpers.
|
|
189
323
|
*
|
|
190
324
|
* Watchers (rows in `chat_subscriptions`) are non-speaking observers. A
|
|
@@ -276,24 +410,6 @@ async function recomputeWatchersForMember(db, memberId) {
|
|
|
276
410
|
for (const { chatId } of rows) await recomputeChatWatchers(db, chatId);
|
|
277
411
|
}
|
|
278
412
|
/**
|
|
279
|
-
* Mirror of `services/chat.ts` `maybeUpgradeDirectToGroup`. Inlined here so
|
|
280
|
-
* `joinAsParticipant` keeps the upgrade rule + the state carry in one
|
|
281
|
-
* transaction without depending on chat.ts (avoids a circular import).
|
|
282
|
-
*/
|
|
283
|
-
async function maybeUpgradeDirectToGroup$1(tx, chatId, existingParticipantIds) {
|
|
284
|
-
if (existingParticipantIds.length + 1 < 3) return;
|
|
285
|
-
const [chat] = await tx.select({ type: chats.type }).from(chats).where(eq(chats.id, chatId)).limit(1);
|
|
286
|
-
if (!chat || chat.type !== "direct") return;
|
|
287
|
-
await tx.update(chats).set({
|
|
288
|
-
type: "group",
|
|
289
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
290
|
-
}).where(eq(chats.id, chatId));
|
|
291
|
-
if (existingParticipantIds.length === 0) return;
|
|
292
|
-
const ids = (await tx.select({ uuid: agents.uuid }).from(agents).where(and(inArray(agents.uuid, existingParticipantIds), ne(agents.type, "human")))).map((r) => r.uuid);
|
|
293
|
-
if (ids.length === 0) return;
|
|
294
|
-
await tx.update(chatParticipants).set({ mode: "mention_only" }).where(and(eq(chatParticipants.chatId, chatId), inArray(chatParticipants.agentId, ids)));
|
|
295
|
-
}
|
|
296
|
-
/**
|
|
297
413
|
* Watcher → speaking participant. State-carry transaction.
|
|
298
414
|
*
|
|
299
415
|
* 1. DELETE the watcher row (returning read state).
|
|
@@ -318,15 +434,15 @@ async function joinAsParticipant(db, chatId, humanAgentId) {
|
|
|
318
434
|
inserted: false,
|
|
319
435
|
carried: carriedRow ?? null
|
|
320
436
|
};
|
|
321
|
-
|
|
322
|
-
await tx
|
|
323
|
-
chatId,
|
|
437
|
+
if (wouldUpgradeToGroup((await tx.select({ agentId: chatParticipants.agentId }).from(chatParticipants).where(eq(chatParticipants.chatId, chatId))).length, 1)) await changeChatType(tx, chatId, "group");
|
|
438
|
+
await addChatParticipants(tx, chatId, [{
|
|
324
439
|
agentId: humanAgentId,
|
|
325
440
|
role: "member",
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
441
|
+
carriedReadState: carriedRow ? {
|
|
442
|
+
lastReadAt: carriedRow.lastReadAt,
|
|
443
|
+
unreadMentionCount: carriedRow.unreadMentionCount
|
|
444
|
+
} : void 0
|
|
445
|
+
}], { assertHuman: true });
|
|
330
446
|
return {
|
|
331
447
|
chatId,
|
|
332
448
|
inserted: true,
|
|
@@ -405,28 +521,6 @@ function ensureCanJoin(membership) {
|
|
|
405
521
|
if (membership === "participant") throw new ConflictError("Already a participant in this chat");
|
|
406
522
|
if (membership === null) throw new ForbiddenError("Not a watcher of this chat — open the chat from your workspace before joining");
|
|
407
523
|
}
|
|
408
|
-
/**
|
|
409
|
-
* When a direct chat grows past 2 participants, upgrade it to `group` and
|
|
410
|
-
* flip every existing non-human agent participant to `mention_only` — see
|
|
411
|
-
* proposals/hub-agent-messaging-reply-and-mentions §3.3. The caller is
|
|
412
|
-
* expected to insert the new participant AFTER this runs, so the "existing"
|
|
413
|
-
* set excludes them.
|
|
414
|
-
*
|
|
415
|
-
* Idempotent: if the chat is already a group, no-op.
|
|
416
|
-
*/
|
|
417
|
-
async function maybeUpgradeDirectToGroup(db, chatId, existingParticipantIds, newParticipantCount) {
|
|
418
|
-
if (existingParticipantIds.length + newParticipantCount < 3) return;
|
|
419
|
-
const [chat] = await db.select({ type: chats.type }).from(chats).where(eq(chats.id, chatId)).limit(1);
|
|
420
|
-
if (!chat || chat.type !== "direct") return;
|
|
421
|
-
await db.update(chats).set({
|
|
422
|
-
type: "group",
|
|
423
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
424
|
-
}).where(eq(chats.id, chatId));
|
|
425
|
-
if (existingParticipantIds.length === 0) return;
|
|
426
|
-
const ids = (await db.select({ uuid: agents.uuid }).from(agents).where(and(inArray(agents.uuid, existingParticipantIds), ne(agents.type, "human")))).map((a) => a.uuid);
|
|
427
|
-
if (ids.length === 0) return;
|
|
428
|
-
await db.update(chatParticipants).set({ mode: "mention_only" }).where(and(eq(chatParticipants.chatId, chatId), inArray(chatParticipants.agentId, ids)));
|
|
429
|
-
}
|
|
430
524
|
async function createChat(db, creatorId, data) {
|
|
431
525
|
const chatId = randomUUID();
|
|
432
526
|
const allParticipantIds = new Set([creatorId, ...data.participantIds]);
|
|
@@ -444,7 +538,6 @@ async function createChat(db, creatorId, data) {
|
|
|
444
538
|
const orgId = creator.organizationId;
|
|
445
539
|
const crossOrg = existingAgents.filter((a) => a.organizationId !== orgId);
|
|
446
540
|
if (crossOrg.length > 0) throw new BadRequestError(`Cross-organization chat not allowed: ${crossOrg.map((a) => a.id).join(", ")}`);
|
|
447
|
-
const isDirectAgentOnly = data.type === "direct" && existingAgents.every((a) => a.type !== "human");
|
|
448
541
|
return db.transaction(async (tx) => {
|
|
449
542
|
const [chat] = await tx.insert(chats).values({
|
|
450
543
|
id: chatId,
|
|
@@ -453,13 +546,10 @@ async function createChat(db, creatorId, data) {
|
|
|
453
546
|
topic: data.topic ?? null,
|
|
454
547
|
metadata: data.metadata ?? {}
|
|
455
548
|
}).returning();
|
|
456
|
-
|
|
457
|
-
chatId,
|
|
549
|
+
await addChatParticipants(tx, chatId, [...allParticipantIds].map((agentId) => ({
|
|
458
550
|
agentId,
|
|
459
|
-
role: agentId === creatorId ? "owner" : "member"
|
|
460
|
-
|
|
461
|
-
}));
|
|
462
|
-
await tx.insert(chatParticipants).values(participantRows);
|
|
551
|
+
role: agentId === creatorId ? "owner" : "member"
|
|
552
|
+
})));
|
|
463
553
|
await recomputeChatWatchers(tx, chatId);
|
|
464
554
|
const participants = await tx.select().from(chatParticipants).where(eq(chatParticipants.chatId, chatId));
|
|
465
555
|
if (!chat) throw new Error("Unexpected: INSERT RETURNING produced no row");
|
|
@@ -518,18 +608,23 @@ async function assertParticipant(db, chatId, agentId) {
|
|
|
518
608
|
const [row] = await db.select({ chatId: chatParticipants.chatId }).from(chatParticipants).where(and(eq(chatParticipants.chatId, chatId), eq(chatParticipants.agentId, agentId))).limit(1);
|
|
519
609
|
if (!row) throw new ForbiddenError("Not a participant of this chat");
|
|
520
610
|
}
|
|
611
|
+
/**
|
|
612
|
+
* Non-throwing membership check. Used by routing logic that needs to fall
|
|
613
|
+
* back to a different chat when the candidate target isn't a member of the
|
|
614
|
+
* caller's current chat (see `sendToAgent`'s current-chat routing branch).
|
|
615
|
+
*/
|
|
616
|
+
async function isParticipant(db, chatId, agentId) {
|
|
617
|
+
const [row] = await db.select({ chatId: chatParticipants.chatId }).from(chatParticipants).where(and(eq(chatParticipants.chatId, chatId), eq(chatParticipants.agentId, agentId))).limit(1);
|
|
618
|
+
return Boolean(row);
|
|
619
|
+
}
|
|
521
620
|
/** Ensure an agent is a participant of a chat. Silently adds them if not already. */
|
|
522
621
|
async function ensureParticipant(db, chatId, agentId) {
|
|
523
622
|
const [existing] = await db.select({ agentId: chatParticipants.agentId }).from(chatParticipants).where(and(eq(chatParticipants.chatId, chatId), eq(chatParticipants.agentId, agentId))).limit(1);
|
|
524
623
|
if (existing) return;
|
|
525
624
|
await db.transaction(async (tx) => {
|
|
526
|
-
|
|
625
|
+
if (wouldUpgradeToGroup((await tx.select({ agentId: chatParticipants.agentId }).from(chatParticipants).where(eq(chatParticipants.chatId, chatId))).length, 1)) await changeChatType(tx, chatId, "group");
|
|
527
626
|
await tx.delete(chatSubscriptions).where(and(eq(chatSubscriptions.chatId, chatId), eq(chatSubscriptions.agentId, agentId)));
|
|
528
|
-
await tx
|
|
529
|
-
chatId,
|
|
530
|
-
agentId,
|
|
531
|
-
mode: "full"
|
|
532
|
-
}).onConflictDoNothing({ target: [chatParticipants.chatId, chatParticipants.agentId] });
|
|
627
|
+
await addChatParticipants(tx, chatId, [{ agentId }], { onConflictDoNothing: true });
|
|
533
628
|
await recomputeChatWatchers(tx, chatId);
|
|
534
629
|
});
|
|
535
630
|
invalidateChatAudience(chatId);
|
|
@@ -546,13 +641,9 @@ async function addParticipant(db, chatId, requesterId, data) {
|
|
|
546
641
|
const [existing] = await db.select({ chatId: chatParticipants.chatId }).from(chatParticipants).where(and(eq(chatParticipants.chatId, chatId), eq(chatParticipants.agentId, data.agentId))).limit(1);
|
|
547
642
|
if (existing) throw new ConflictError(`Agent "${data.agentId}" is already a participant`);
|
|
548
643
|
await db.transaction(async (tx) => {
|
|
549
|
-
|
|
644
|
+
if (wouldUpgradeToGroup((await tx.select({ agentId: chatParticipants.agentId }).from(chatParticipants).where(eq(chatParticipants.chatId, chatId))).length, 1)) await changeChatType(tx, chatId, "group");
|
|
550
645
|
await tx.delete(chatSubscriptions).where(and(eq(chatSubscriptions.chatId, chatId), eq(chatSubscriptions.agentId, data.agentId)));
|
|
551
|
-
await tx.
|
|
552
|
-
chatId,
|
|
553
|
-
agentId: data.agentId,
|
|
554
|
-
mode: data.mode ?? "full"
|
|
555
|
-
});
|
|
646
|
+
await addChatParticipants(tx, chatId, [{ agentId: data.agentId }]);
|
|
556
647
|
await recomputeChatWatchers(tx, chatId);
|
|
557
648
|
});
|
|
558
649
|
invalidateChatAudience(chatId);
|
|
@@ -660,15 +751,15 @@ async function joinChat(db, chatId, memberId, humanAgentId) {
|
|
|
660
751
|
lastReadAt: chatSubscriptions.lastReadAt,
|
|
661
752
|
unreadMentionCount: chatSubscriptions.unreadMentionCount
|
|
662
753
|
});
|
|
663
|
-
await
|
|
664
|
-
await tx
|
|
665
|
-
chatId,
|
|
754
|
+
if (wouldUpgradeToGroup(participantAgentIds.length, 1)) await changeChatType(tx, chatId, "group");
|
|
755
|
+
await addChatParticipants(tx, chatId, [{
|
|
666
756
|
agentId: humanAgentId,
|
|
667
757
|
role: "member",
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
758
|
+
carriedReadState: carriedRow ? {
|
|
759
|
+
lastReadAt: carriedRow.lastReadAt,
|
|
760
|
+
unreadMentionCount: carriedRow.unreadMentionCount
|
|
761
|
+
} : void 0
|
|
762
|
+
}], { assertHuman: true });
|
|
672
763
|
await recomputeChatWatchers(tx, chatId);
|
|
673
764
|
});
|
|
674
765
|
invalidateChatAudience(chatId);
|
|
@@ -710,7 +801,6 @@ async function findOrCreateDirectChat(db, agentAId, agentBId) {
|
|
|
710
801
|
const directChats = await db.select().from(chats).where(and(inArray(chats.id, commonChatIds), eq(chats.type, "direct"), eq(chats.organizationId, orgId))).orderBy(chats.createdAt, chats.id).limit(1);
|
|
711
802
|
if (directChats.length > 0 && directChats[0]) return directChats[0];
|
|
712
803
|
}
|
|
713
|
-
const mode = agentA.type !== "human" && agentB.type !== "human" ? "mention_only" : "full";
|
|
714
804
|
const chatId = randomUUID();
|
|
715
805
|
return db.transaction(async (tx) => {
|
|
716
806
|
const [chat] = await tx.insert(chats).values({
|
|
@@ -718,16 +808,12 @@ async function findOrCreateDirectChat(db, agentAId, agentBId) {
|
|
|
718
808
|
organizationId: orgId,
|
|
719
809
|
type: "direct"
|
|
720
810
|
}).returning();
|
|
721
|
-
await tx
|
|
722
|
-
chatId,
|
|
811
|
+
await addChatParticipants(tx, chatId, [{
|
|
723
812
|
agentId: agentAId,
|
|
724
|
-
role: "member"
|
|
725
|
-
mode
|
|
813
|
+
role: "member"
|
|
726
814
|
}, {
|
|
727
|
-
chatId,
|
|
728
815
|
agentId: agentBId,
|
|
729
|
-
role: "member"
|
|
730
|
-
mode
|
|
816
|
+
role: "member"
|
|
731
817
|
}]);
|
|
732
818
|
await recomputeChatWatchers(tx, chatId);
|
|
733
819
|
if (!chat) throw new Error("Unexpected: INSERT RETURNING produced no row");
|
|
@@ -1684,7 +1770,6 @@ async function sendToAgent(db, senderUuid, targetName, data) {
|
|
|
1684
1770
|
if (!sender) throw new NotFoundError(`Agent "${senderUuid}" not found`);
|
|
1685
1771
|
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);
|
|
1686
1772
|
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." : ""}`);
|
|
1687
|
-
const chat = await findOrCreateDirectChat(db, senderUuid, target.uuid);
|
|
1688
1773
|
const incomingMeta = data.metadata ?? {};
|
|
1689
1774
|
const existingMentionsRaw = incomingMeta.mentions;
|
|
1690
1775
|
const existingMentions = Array.isArray(existingMentionsRaw) ? existingMentionsRaw.filter((m) => typeof m === "string") : [];
|
|
@@ -1693,7 +1778,18 @@ async function sendToAgent(db, senderUuid, targetName, data) {
|
|
|
1693
1778
|
...incomingMeta,
|
|
1694
1779
|
mentions: mergedMentions
|
|
1695
1780
|
};
|
|
1696
|
-
|
|
1781
|
+
if (data.replyToChat) {
|
|
1782
|
+
const [targetIsMember, senderIsMember] = await Promise.all([isParticipant(db, data.replyToChat, target.uuid), isParticipant(db, data.replyToChat, senderUuid)]);
|
|
1783
|
+
if (targetIsMember && senderIsMember) return sendMessage(db, data.replyToChat, senderUuid, {
|
|
1784
|
+
format: data.format,
|
|
1785
|
+
content: data.content,
|
|
1786
|
+
metadata,
|
|
1787
|
+
replyToInbox: void 0,
|
|
1788
|
+
replyToChat: void 0,
|
|
1789
|
+
source: data.source
|
|
1790
|
+
}, { normalizeMentionsInContent: true });
|
|
1791
|
+
}
|
|
1792
|
+
return sendMessage(db, (await findOrCreateDirectChat(db, senderUuid, target.uuid)).id, senderUuid, {
|
|
1697
1793
|
format: data.format,
|
|
1698
1794
|
content: data.content,
|
|
1699
1795
|
metadata,
|
|
@@ -2032,4 +2128,4 @@ async function cleanupStaleClients(db, staleSeconds = 60) {
|
|
|
2032
2128
|
return result.length;
|
|
2033
2129
|
}
|
|
2034
2130
|
//#endregion
|
|
2035
|
-
export {
|
|
2131
|
+
export { messages as $, getOnlineCount as A, listActiveAgentsPinnedToClient as B, ensureCanJoin as C, getCachedAudience as D, getActivityOverview as E, invalidateChatAudience as F, listChatsForMember as G, listAgentsWithRuntime as H, joinAsParticipant as I, listMessages as J, listClients as K, joinChat as L, heartbeatClient as M, heartbeatInstance as N, getChatDetail as O, inboxEntries as P, members as Q, leaveAsParticipant as R, editMessage as S, findOrCreateDirectChat as T, listChatParticipantsWithNames as U, listAgentsManagedByUser as V, listChats as W, markStaleAgents as X, listMyPinnedAgents as Y, markSupersededByChat as Z, clients as _, touchAgent as _t, agentVisibilityCondition as a, registerChatMessageDispatcher as at, deriveAuthState as b, upsertSessionState as bt, assertParticipant as c, resetActivity as ct, chatParticipants as d, sendMessage as dt, notifyRecipients as et, chatSubscriptions as f, sendToAgent as ft, cleanupStalePresence as g, submitAnswer as gt, cleanupStaleClients as h, setRuntimeState as ht, agentPresence as i, recomputeWatchersForMember as it, getPresence as j, getClient as k, bindAgent as l, resolveChatMembership as lt, claimClient as m, setOffline as mt, addParticipant as n, recomputeChatWatchers as nt, agents as o, registerClient as ot, chats as p, serverInstances as pt, listClientsForOrgAdmin as q, agentChatSessions as r, recomputeWatchersForAgent as rt, assertClientOwner as s, removeParticipant as st, addChatParticipants as t, pendingQuestions as tt, changeChatType as u, retireClient as ut, createChat as v, unbindAgent as vt, ensureParticipant as w, disconnectClient as x, createNotifier as y, updateClientCapabilities as yt, leaveChat as z };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import "./observability-BAScT_5S-BcW9HgkG.mjs";
|
|
2
2
|
import "./logger-core-BTmvdflj-DjW8FM4T.mjs";
|
|
3
|
-
import "./dist-
|
|
3
|
+
import "./dist-xP6NpdMp.mjs";
|
|
4
4
|
import "./errors-CF5evtJt-B0NTIVPt.mjs";
|
|
5
5
|
import "./src-DNBS5Yjj.mjs";
|
|
6
|
-
import {
|
|
6
|
+
import { Y as listMyPinnedAgents } from "./client-B89AKi3Q-DAyGdQSq.mjs";
|
|
7
7
|
export { listMyPinnedAgents };
|