@agent-team-foundation/first-tree-hub 0.12.3 → 0.12.5
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 +13 -11
- package/dist/{client-D1TDiik_-gxtXN9bj.mjs → client-DL5vHhvQ-CnYGq2x-.mjs} +23 -4
- package/dist/{client-0RrgrMjR-DPyuu6Ls.mjs → client-DSM_opoz-BH5eegXb.mjs} +2 -2
- package/dist/{dist-DHHd2dar.mjs → dist-BwPlBZWi.mjs} +27 -142
- package/dist/drizzle/0035_drop_hub_tasks.sql +7 -0
- package/dist/drizzle/0036_github_entity_chat_mappings.sql +47 -0
- package/dist/drizzle/meta/_journal.json +14 -0
- package/dist/{feishu-fLnwqCOs.mjs → feishu-CKGzIamp.mjs} +1 -1
- package/dist/index.mjs +4 -4
- package/dist/{invitation-C299fxkP-B89eqDos.mjs → invitation-C299fxkP-Dts66QTU.mjs} +1 -1
- package/dist/{saas-connect-_lNV0Liy.mjs → saas-connect-DYjvx5yr.mjs} +636 -932
- package/dist/web/assets/index-BXDLOc-s.js +406 -0
- package/dist/web/assets/index-CbOOQaWp.css +1 -0
- package/dist/web/assets/{index-DcMORzyx.js → index-Dyo6TAWC.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-B4EaL8S9.css +0 -1
- package/dist/web/assets/index-CJr7zpx-.js +0 -401
|
@@ -2,10 +2,10 @@ import { a as __toCommonJS, o as __toESM, t as __commonJSMin } from "./chunk-BSw
|
|
|
2
2
|
import { A as FIRST_TREE_HUB_ATTR, C as stampOrgScope, D as untrustedAttrs, E as startWsConnectionSpan, M as require_pino, O as withSpan, S as stampChatResource, _ as rootLogger$1, a as buildRateLimitError, c as currentTraceId, g as reportErrorToRoot, i as bodyCaptureOnSendHook, j as redactUrl, k as withWsMessageSpan, l as decodeJwtForTrace, m as observabilityPlugin, n as applyLoggerConfig, o as classifyJoseError, r as attachRequestContext, s as createLogger$1, t as adapterAttrs, u as endWsConnectionSpan, x as stampAgentResource, y as setWsConnectionAttrs } from "./observability-BAScT_5S-BcW9HgkG.mjs";
|
|
3
3
|
import { C as resetConfigMeta, E as setConfigValue, S as resetConfig, T as serverConfigSchema, b as migrateLegacyHome, c as resolveServerUrl, d as DEFAULT_CONFIG_DIR, f as DEFAULT_DATA_DIR$1, g as collectMissingPrompts, h as clientConfigSchema, i as ensureFreshAccessToken, l as saveAgentConfig, m as agentConfigSchema, o as loadCredentials, p as DEFAULT_HOME_DIR$1, u as saveCredentials, v as initConfig, w as resolveConfigReadonly, y as loadAgents } from "./bootstrap-C_K2CKXC.mjs";
|
|
4
4
|
import { a as print, i as blank, n as CLI_USER_AGENT, r as COMMAND_VERSION, s as status, t as cliFetch } from "./cli-fetch--tiwKm5S.mjs";
|
|
5
|
-
import { $ as
|
|
5
|
+
import { $ as refreshTokenSchema, A as dryRunAgentRuntimeConfigSchema, B as isRedactedEnvValue, C as createAgentSchema, D as createOrgFromMeSchema, E as createMemberSchema, F as imageInlineContentSchema, G as messageSourceSchema$1, H as joinByInvitationSchema, I as inboxAckFrameSchema, J as paginationQuerySchema, K as notificationQuerySchema, L as inboxDeliverFrameSchema$1, M as githubCallbackQuerySchema, N as githubDevCallbackQuerySchema, O as defaultRuntimeConfigPayload, P as githubStartQuerySchema, Q as rebindAgentSchema, R as inboxPollQuerySchema, S as createAdapterMappingSchema, T as createMeChatSchema, U as listMeChatsQuerySchema, V as isReservedAgentName$1, W as loginSchema, Y as patchOnboardingSchema, _t as updateClientCapabilitiesSchema, a as AGENT_VISIBILITY, at as sendToAgentSchema, b as contextTreeSnapshotSchema, bt as wsAuthFrameSchema, c as ORG_SETTINGS_NAMESPACES$1, ct as sessionEventSchema$1, d as addParticipantSchema, dt as stripCode, et as runtimeStateMessageSchema, f as agentBindRequestSchema, ft as submitQuestionAnswerSchema, g as chatMetadataSchema$1, gt as updateChatSchema, h as agentTypeSchema$1, ht as updateAgentSchema, i as AGENT_STATUSES, it as sendMessageSchema, k as delegateFeishuUserSchema, l as WS_AUTH_FRAME_TIMEOUT_MS, lt as sessionReconcileRequestSchema, m as agentRuntimeConfigPayloadSchema$1, mt as updateAgentRuntimeConfigSchema, n as AGENT_NAME_REGEX$1, ot as sessionCompletionMessageSchema, p as agentPinnedMessageSchema$1, pt as updateAdapterConfigSchema, q as onboardingEventSchema, r as AGENT_SELECTOR_HEADER$1, rt as selfServiceFeishuBotSchema, s as MENTION_REGEX, st as sessionEventMessageSchema, t as AGENT_BIND_REJECT_REASONS, tt as safeRedirectPath, u as addMeChatParticipantsSchema, ut as sessionStateMessageSchema, v as clientRegisterSchema, vt as updateMemberSchema, w as createChatSchema, x as createAdapterConfigSchema, y as connectTokenExchangeSchema, yt as updateOrganizationSchema, z as isOrgSettingNamespace } from "./dist-BwPlBZWi.mjs";
|
|
6
6
|
import { a as ConflictError, c as UnauthorizedError, i as ClientUserMismatchError$1, l as organizations, n as BadRequestError, o as ForbiddenError, r as ClientOrgMismatchError$1, s as NotFoundError, t as AppError, u as users } from "./errors-CF5evtJt-B0NTIVPt.mjs";
|
|
7
7
|
import { n as init_esm, r as trace, t as esm_exports } from "./esm-iadMkGbV.mjs";
|
|
8
|
-
import { $ as pendingQuestions, A as heartbeatClient, B as listAgentsWithRuntime, C as findOrCreateDirectChat, D as getClient, E as getChatDetail, F as joinChat, G as listClientsForOrgAdmin, H as listChats, I as leaveAsParticipant, J as markStaleAgents, K as listMessages, L as leaveChat, M as inboxEntries, N as invalidateChatAudience, O as getOnlineCount, P as joinAsParticipant, Q as notifyRecipients, R as listActiveAgentsPinnedToClient, S as ensureParticipant$1, T as getCachedAudience, U as listChatsForMember, V as listChatParticipantsWithNames, W as listClients, X as members, Y as markSupersededByChat, Z as messages, _ as createNotifier, _t as updateClientCapabilities, a as agents, at as removeParticipant, b as editMessage, c as bindAgent, ct as retireClient, d as chats, dt as serverInstances, et as recomputeChatWatchers, f as claimClient, ft as setOffline, g as createChat, gt as unbindAgent, h as clients, ht as touchAgent, i as agentVisibilityCondition, it as registerClient, j as heartbeatInstance, k as getPresence, l as chatParticipants, lt as sendMessage, m as cleanupStalePresence, mt as submitAnswer, n as agentChatSessions, nt as recomputeWatchersForMember, o as assertClientOwner, ot as resetActivity, p as cleanupStaleClients, pt as setRuntimeState, r as agentPresence, rt as registerChatMessageDispatcher, s as assertParticipant, st as resolveChatMembership, t as addParticipant, tt as recomputeWatchersForAgent, u as chatSubscriptions, ut as sendToAgent$1, v as deriveAuthState, vt as upsertSessionState, w as getActivityOverview, x as ensureCanJoin, y as disconnectClient, z as listAgentsManagedByUser } from "./client-
|
|
8
|
+
import { $ as pendingQuestions, A as heartbeatClient, B as listAgentsWithRuntime, C as findOrCreateDirectChat, D as getClient, E as getChatDetail, F as joinChat, G as listClientsForOrgAdmin, H as listChats, I as leaveAsParticipant, J as markStaleAgents, K as listMessages, L as leaveChat, M as inboxEntries, N as invalidateChatAudience, O as getOnlineCount, P as joinAsParticipant, Q as notifyRecipients, R as listActiveAgentsPinnedToClient, S as ensureParticipant$1, T as getCachedAudience, U as listChatsForMember, V as listChatParticipantsWithNames, W as listClients, X as members, Y as markSupersededByChat, Z as messages, _ as createNotifier, _t as updateClientCapabilities, a as agents, at as removeParticipant, b as editMessage, c as bindAgent, ct as retireClient, d as chats, dt as serverInstances, et as recomputeChatWatchers, f as claimClient, ft as setOffline, g as createChat, gt as unbindAgent, h as clients, ht as touchAgent, i as agentVisibilityCondition, it as registerClient, j as heartbeatInstance, k as getPresence, l as chatParticipants, lt as sendMessage, m as cleanupStalePresence, mt as submitAnswer, n as agentChatSessions, nt as recomputeWatchersForMember, o as assertClientOwner, ot as resetActivity, p as cleanupStaleClients, pt as setRuntimeState, r as agentPresence, rt as registerChatMessageDispatcher, s as assertParticipant, st as resolveChatMembership, t as addParticipant, tt as recomputeWatchersForAgent, u as chatSubscriptions, ut as sendToAgent$1, v as deriveAuthState, vt as upsertSessionState, w as getActivityOverview, x as ensureCanJoin, y as disconnectClient, z as listAgentsManagedByUser } from "./client-DL5vHhvQ-CnYGq2x-.mjs";
|
|
9
9
|
import { a as invitationRedemptions, c as recordRedemption, i as getActiveInvitation, l as rotateInvitation, n as ensureActiveInvitation, o as invitations, r as findActiveByToken, t as buildInviteUrl, u as uuidv7 } from "./invitation-Bg0TRiyx-BsZH4GCS.mjs";
|
|
10
10
|
import { createRequire } from "node:module";
|
|
11
11
|
import { ZodError, z } from "zod";
|
|
@@ -730,6 +730,30 @@ z.object({
|
|
|
730
730
|
expiresIn: z.number(),
|
|
731
731
|
command: z.string()
|
|
732
732
|
});
|
|
733
|
+
const githubEntityTypeSchema = z.enum([
|
|
734
|
+
"issue",
|
|
735
|
+
"pull_request",
|
|
736
|
+
"discussion",
|
|
737
|
+
"commit"
|
|
738
|
+
]);
|
|
739
|
+
const githubChatMetadataSchema = z.object({
|
|
740
|
+
source: z.literal("github"),
|
|
741
|
+
entityType: githubEntityTypeSchema,
|
|
742
|
+
entityKey: z.string().min(1),
|
|
743
|
+
entityUrl: z.string().url().optional()
|
|
744
|
+
});
|
|
745
|
+
const feishuChatMetadataSchema = z.object({
|
|
746
|
+
source: z.literal("feishu"),
|
|
747
|
+
externalChannelId: z.string().min(1)
|
|
748
|
+
});
|
|
749
|
+
const chatMetadataSchema = z.discriminatedUnion("source", [githubChatMetadataSchema, feishuChatMetadataSchema]);
|
|
750
|
+
/**
|
|
751
|
+
* `createChat` callers may not set metadata at all (admin-created group chats,
|
|
752
|
+
* me-chats, …), so the input schema accepts either an empty object or one of
|
|
753
|
+
* the typed variants. The empty `{}` arm is `.strict()` so a caller cannot
|
|
754
|
+
* sneak through `{ source: "github" }` without the required fields.
|
|
755
|
+
*/
|
|
756
|
+
const optionalChatMetadataSchema = z.union([z.object({}).strict(), chatMetadataSchema]);
|
|
733
757
|
const chatTypeSchema = z.enum([
|
|
734
758
|
"direct",
|
|
735
759
|
"group",
|
|
@@ -739,7 +763,7 @@ z.object({
|
|
|
739
763
|
type: chatTypeSchema,
|
|
740
764
|
topic: z.string().max(500).optional(),
|
|
741
765
|
participantIds: z.array(z.string()).min(1),
|
|
742
|
-
metadata:
|
|
766
|
+
metadata: optionalChatMetadataSchema.optional()
|
|
743
767
|
});
|
|
744
768
|
const chatParticipantSchema = z.object({
|
|
745
769
|
agentId: z.string(),
|
|
@@ -1015,7 +1039,6 @@ const messageFormatSchema = z.enum([
|
|
|
1015
1039
|
"card",
|
|
1016
1040
|
"reference",
|
|
1017
1041
|
"file",
|
|
1018
|
-
"task",
|
|
1019
1042
|
"question",
|
|
1020
1043
|
"question_answer"
|
|
1021
1044
|
]);
|
|
@@ -1199,9 +1222,7 @@ const meChatRowSchema = z.object({
|
|
|
1199
1222
|
lastMessageAt: z.string().nullable(),
|
|
1200
1223
|
lastMessagePreview: z.string().nullable(),
|
|
1201
1224
|
unreadMentionCount: z.number().int(),
|
|
1202
|
-
canReply: z.boolean()
|
|
1203
|
-
taskId: z.string().nullable(),
|
|
1204
|
-
taskStatus: z.string().nullable()
|
|
1225
|
+
canReply: z.boolean()
|
|
1205
1226
|
});
|
|
1206
1227
|
z.object({
|
|
1207
1228
|
rows: z.array(meChatRowSchema),
|
|
@@ -1677,94 +1698,6 @@ z.object({
|
|
|
1677
1698
|
totalMessages: z.number(),
|
|
1678
1699
|
byOrganization: z.array(orgStatsSchema)
|
|
1679
1700
|
});
|
|
1680
|
-
const taskStatusSchema = z.enum([
|
|
1681
|
-
"pending",
|
|
1682
|
-
"assigned",
|
|
1683
|
-
"working",
|
|
1684
|
-
"completed",
|
|
1685
|
-
"failed",
|
|
1686
|
-
"cancelled"
|
|
1687
|
-
]);
|
|
1688
|
-
const taskCreatorTypeSchema = z.enum(["agent", "admin"]);
|
|
1689
|
-
const taskMessageEventSchema = z.enum([
|
|
1690
|
-
"assigned",
|
|
1691
|
-
"status_changed",
|
|
1692
|
-
"cancelled"
|
|
1693
|
-
]);
|
|
1694
|
-
z.object({
|
|
1695
|
-
taskId: z.string(),
|
|
1696
|
-
event: taskMessageEventSchema,
|
|
1697
|
-
title: z.string(),
|
|
1698
|
-
body: z.string().default(""),
|
|
1699
|
-
status: taskStatusSchema,
|
|
1700
|
-
fromStatus: taskStatusSchema.optional(),
|
|
1701
|
-
originRef: z.string().nullable().optional()
|
|
1702
|
-
});
|
|
1703
|
-
z.object({
|
|
1704
|
-
title: z.string().min(1).max(500),
|
|
1705
|
-
body: z.string().optional(),
|
|
1706
|
-
assigneeAgentId: z.string().optional(),
|
|
1707
|
-
originRef: z.string().max(500).optional(),
|
|
1708
|
-
metadata: z.record(z.string(), z.unknown()).optional()
|
|
1709
|
-
}).extend({ organizationId: z.string().optional() });
|
|
1710
|
-
z.object({
|
|
1711
|
-
status: z.enum([
|
|
1712
|
-
"working",
|
|
1713
|
-
"completed",
|
|
1714
|
-
"failed"
|
|
1715
|
-
]),
|
|
1716
|
-
result: z.string().optional()
|
|
1717
|
-
});
|
|
1718
|
-
z.object({
|
|
1719
|
-
assigneeAgentId: z.string().nullable().optional(),
|
|
1720
|
-
status: taskStatusSchema.optional(),
|
|
1721
|
-
result: z.string().optional()
|
|
1722
|
-
});
|
|
1723
|
-
z.object({ chatId: z.string().min(1) });
|
|
1724
|
-
const taskSchema = z.object({
|
|
1725
|
-
id: z.string(),
|
|
1726
|
-
organizationId: z.string(),
|
|
1727
|
-
title: z.string(),
|
|
1728
|
-
body: z.string(),
|
|
1729
|
-
status: taskStatusSchema,
|
|
1730
|
-
assigneeAgentId: z.string().nullable(),
|
|
1731
|
-
createdByType: taskCreatorTypeSchema,
|
|
1732
|
-
createdById: z.string(),
|
|
1733
|
-
originRef: z.string().nullable(),
|
|
1734
|
-
result: z.string().nullable(),
|
|
1735
|
-
metadata: z.record(z.string(), z.unknown()),
|
|
1736
|
-
createdAt: z.string(),
|
|
1737
|
-
updatedAt: z.string(),
|
|
1738
|
-
cancelledAt: z.string().nullable(),
|
|
1739
|
-
cancelledByType: taskCreatorTypeSchema.nullable(),
|
|
1740
|
-
cancelledById: z.string().nullable()
|
|
1741
|
-
});
|
|
1742
|
-
const taskChatLinkSchema = z.object({
|
|
1743
|
-
taskId: z.string(),
|
|
1744
|
-
chatId: z.string(),
|
|
1745
|
-
linkedByAgentId: z.string().nullable(),
|
|
1746
|
-
linkedAt: z.string()
|
|
1747
|
-
});
|
|
1748
|
-
taskSchema.extend({ chats: z.array(taskChatLinkSchema) });
|
|
1749
|
-
z.object({
|
|
1750
|
-
status: taskStatusSchema.optional(),
|
|
1751
|
-
assigneeAgentId: z.string().optional(),
|
|
1752
|
-
originRef: z.string().optional(),
|
|
1753
|
-
limit: z.coerce.number().int().min(1).max(100).default(20),
|
|
1754
|
-
cursor: z.string().optional()
|
|
1755
|
-
});
|
|
1756
|
-
const taskHealthSignalSchema = z.enum([
|
|
1757
|
-
"normal",
|
|
1758
|
-
"idle_island",
|
|
1759
|
-
"awaiting_reply",
|
|
1760
|
-
"no_chat",
|
|
1761
|
-
"not_applicable"
|
|
1762
|
-
]);
|
|
1763
|
-
z.object({
|
|
1764
|
-
taskId: z.string(),
|
|
1765
|
-
signal: taskHealthSignalSchema,
|
|
1766
|
-
reason: z.string()
|
|
1767
|
-
});
|
|
1768
1701
|
const userStatusSchema = z.enum(["active", "suspended"]);
|
|
1769
1702
|
z.object({
|
|
1770
1703
|
id: z.string(),
|
|
@@ -2876,7 +2809,126 @@ function getHandlerFactory(type) {
|
|
|
2876
2809
|
}
|
|
2877
2810
|
return factory;
|
|
2878
2811
|
}
|
|
2879
|
-
join(DEFAULT_DATA_DIR, "context-tree");
|
|
2812
|
+
const CONTEXT_TREE_DIR = join(DEFAULT_DATA_DIR, "context-tree");
|
|
2813
|
+
/**
|
|
2814
|
+
* Sync the shared Context Tree git clone.
|
|
2815
|
+
*
|
|
2816
|
+
* Clones on first run, pulls on subsequent runs.
|
|
2817
|
+
* Returns the binding on success, null on failure (graceful degradation).
|
|
2818
|
+
*/
|
|
2819
|
+
async function syncContextTree(serverUrl, getAccessToken, log, userAgent) {
|
|
2820
|
+
try {
|
|
2821
|
+
execFileSync("git", ["--version"], { stdio: "ignore" });
|
|
2822
|
+
} catch {
|
|
2823
|
+
log("Context Tree sync skipped: git is not installed");
|
|
2824
|
+
return null;
|
|
2825
|
+
}
|
|
2826
|
+
let repo;
|
|
2827
|
+
let branch;
|
|
2828
|
+
try {
|
|
2829
|
+
const config = await new FirstTreeHubSDK({
|
|
2830
|
+
serverUrl,
|
|
2831
|
+
getAccessToken,
|
|
2832
|
+
userAgent
|
|
2833
|
+
}).getContextTreeConfig();
|
|
2834
|
+
if (!config.repo) {
|
|
2835
|
+
log("Context Tree sync skipped: not configured on server");
|
|
2836
|
+
return null;
|
|
2837
|
+
}
|
|
2838
|
+
repo = config.repo;
|
|
2839
|
+
branch = config.branch ?? "main";
|
|
2840
|
+
} catch (err) {
|
|
2841
|
+
log(`Context Tree sync skipped: failed to fetch config from server (${err instanceof Error ? err.message : String(err)})`);
|
|
2842
|
+
return null;
|
|
2843
|
+
}
|
|
2844
|
+
try {
|
|
2845
|
+
if (existsSync(join(CONTEXT_TREE_DIR, ".git"))) {
|
|
2846
|
+
if (execFileSync("git", [
|
|
2847
|
+
"rev-parse",
|
|
2848
|
+
"--abbrev-ref",
|
|
2849
|
+
"HEAD"
|
|
2850
|
+
], {
|
|
2851
|
+
cwd: CONTEXT_TREE_DIR,
|
|
2852
|
+
encoding: "utf-8",
|
|
2853
|
+
timeout: 5e3
|
|
2854
|
+
}).trim() !== branch) {
|
|
2855
|
+
execFileSync("git", ["checkout", branch], {
|
|
2856
|
+
cwd: CONTEXT_TREE_DIR,
|
|
2857
|
+
stdio: "pipe",
|
|
2858
|
+
timeout: 1e4
|
|
2859
|
+
});
|
|
2860
|
+
log(`Context Tree switched to branch ${branch}`);
|
|
2861
|
+
}
|
|
2862
|
+
execFileSync("git", ["pull", "--ff-only"], {
|
|
2863
|
+
cwd: CONTEXT_TREE_DIR,
|
|
2864
|
+
stdio: "pipe",
|
|
2865
|
+
timeout: 3e4
|
|
2866
|
+
});
|
|
2867
|
+
log(`Context Tree updated (pull)`);
|
|
2868
|
+
} else {
|
|
2869
|
+
mkdirSync(CONTEXT_TREE_DIR, { recursive: true });
|
|
2870
|
+
execFileSync("git", [
|
|
2871
|
+
"clone",
|
|
2872
|
+
"--branch",
|
|
2873
|
+
branch,
|
|
2874
|
+
"--single-branch",
|
|
2875
|
+
repo,
|
|
2876
|
+
CONTEXT_TREE_DIR
|
|
2877
|
+
], {
|
|
2878
|
+
stdio: "pipe",
|
|
2879
|
+
timeout: 6e4
|
|
2880
|
+
});
|
|
2881
|
+
log(`Context Tree cloned from ${repo} (branch: ${branch})`);
|
|
2882
|
+
}
|
|
2883
|
+
return {
|
|
2884
|
+
path: CONTEXT_TREE_DIR,
|
|
2885
|
+
repoUrl: repo,
|
|
2886
|
+
branch
|
|
2887
|
+
};
|
|
2888
|
+
} catch (err) {
|
|
2889
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2890
|
+
log(`Context Tree sync failed: ${msg}`);
|
|
2891
|
+
log("Check that git credentials (SSH key or credential helper) are configured for this repo");
|
|
2892
|
+
if ((msg.includes("cannot fast-forward") || msg.includes("not possible to fast-forward") || msg.includes("CONFLICT")) && existsSync(join(CONTEXT_TREE_DIR, ".git"))) {
|
|
2893
|
+
log("Diverged history detected, attempting fresh clone...");
|
|
2894
|
+
try {
|
|
2895
|
+
rmSync(CONTEXT_TREE_DIR, {
|
|
2896
|
+
recursive: true,
|
|
2897
|
+
force: true
|
|
2898
|
+
});
|
|
2899
|
+
mkdirSync(CONTEXT_TREE_DIR, { recursive: true });
|
|
2900
|
+
execFileSync("git", [
|
|
2901
|
+
"clone",
|
|
2902
|
+
"--branch",
|
|
2903
|
+
branch,
|
|
2904
|
+
"--single-branch",
|
|
2905
|
+
repo,
|
|
2906
|
+
CONTEXT_TREE_DIR
|
|
2907
|
+
], {
|
|
2908
|
+
stdio: "pipe",
|
|
2909
|
+
timeout: 6e4
|
|
2910
|
+
});
|
|
2911
|
+
log("Context Tree re-cloned successfully");
|
|
2912
|
+
return {
|
|
2913
|
+
path: CONTEXT_TREE_DIR,
|
|
2914
|
+
repoUrl: repo,
|
|
2915
|
+
branch
|
|
2916
|
+
};
|
|
2917
|
+
} catch {
|
|
2918
|
+
log("Context Tree re-clone also failed, continuing without context");
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
2921
|
+
if (existsSync(join(CONTEXT_TREE_DIR, ".git"))) {
|
|
2922
|
+
log("Using existing Context Tree clone despite sync failure");
|
|
2923
|
+
return {
|
|
2924
|
+
path: CONTEXT_TREE_DIR,
|
|
2925
|
+
repoUrl: repo,
|
|
2926
|
+
branch
|
|
2927
|
+
};
|
|
2928
|
+
}
|
|
2929
|
+
return null;
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
2880
2932
|
/**
|
|
2881
2933
|
* Marker file written into every workspace so the Codex CLI's project-root
|
|
2882
2934
|
* detection (configured via `project_root_markers: ["first-tree-workspace"]`)
|
|
@@ -3003,7 +3055,8 @@ function generateToolsDoc() {
|
|
|
3003
3055
|
You are running inside **Agent Hub**, a messaging platform for agent teams.
|
|
3004
3056
|
|
|
3005
3057
|
- Messages from other team members arrive as your prompt input
|
|
3006
|
-
- Each message includes a \`[From:
|
|
3058
|
+
- Each message includes a \`[From: <agent-name>]\` header — that name is also
|
|
3059
|
+
what you pass back to \`agent send\` to reply to or address that agent
|
|
3007
3060
|
- **Your final text response is automatically delivered** to the chat — just respond normally
|
|
3008
3061
|
- For **proactive communication** (sending to other agents, other chats, or structured data),
|
|
3009
3062
|
use the \`first-tree-hub\` CLI below
|
|
@@ -3018,8 +3071,8 @@ These are injected automatically when the agent process starts:
|
|
|
3018
3071
|
|----------|-------------|
|
|
3019
3072
|
| \`FIRST_TREE_HUB_SERVER_URL\` | Server address for API calls |
|
|
3020
3073
|
| \`FIRST_TREE_HUB_ACCESS_TOKEN\` | User member access JWT (short-lived) |
|
|
3021
|
-
| \`FIRST_TREE_HUB_AGENT_ID\` |
|
|
3022
|
-
| \`FIRST_TREE_HUB_CHAT_ID\` |
|
|
3074
|
+
| \`FIRST_TREE_HUB_AGENT_ID\` | YOUR own agent UUID. The CLI reads it to identify you as the sender — never pass it as a \`send\` target. |
|
|
3075
|
+
| \`FIRST_TREE_HUB_CHAT_ID\` | The chat this session is currently bound to. The CLI uses it to route messages — you don't need to pass it manually. |
|
|
3023
3076
|
|
|
3024
3077
|
The \`first-tree-hub\` CLI reads these automatically — no extra setup needed.
|
|
3025
3078
|
|
|
@@ -3029,13 +3082,18 @@ Use the \`first-tree-hub agent send\` CLI — it reads the env vars above and
|
|
|
3029
3082
|
attaches the \`Authorization\` + \`X-Agent-Id\` headers automatically:
|
|
3030
3083
|
|
|
3031
3084
|
\`\`\`bash
|
|
3032
|
-
# Send to another agent —
|
|
3033
|
-
#
|
|
3085
|
+
# Send to another agent — first positional argument is the recipient's NAME
|
|
3086
|
+
# (NOT a uuid; uuids in chat history / participant lists are not accepted).
|
|
3034
3087
|
# Run \`first-tree-hub agent list\` to see available names.
|
|
3088
|
+
#
|
|
3089
|
+
# Routing: if the recipient is a participant of your current chat (typically
|
|
3090
|
+
# the case in a group chat where someone @-mentioned you to talk to them),
|
|
3091
|
+
# the message stays in that chat. Otherwise it falls back to a direct chat
|
|
3092
|
+
# between you and the recipient. You don't need to think about which.
|
|
3035
3093
|
first-tree-hub agent send <agentName> "your message"
|
|
3036
3094
|
|
|
3037
|
-
# Send
|
|
3038
|
-
#
|
|
3095
|
+
# Send into a specific chat by id — use this only when you explicitly want
|
|
3096
|
+
# to address a chat your current session is NOT bound to.
|
|
3039
3097
|
first-tree-hub agent send --chat <chatId> "your message"
|
|
3040
3098
|
|
|
3041
3099
|
# Send markdown (default format is text)
|
|
@@ -6894,6 +6952,36 @@ function resolveReplyToFromEnv(env, override) {
|
|
|
6894
6952
|
replyToChat: override.replyToChat ?? (envComplete ? envChatId : void 0)
|
|
6895
6953
|
};
|
|
6896
6954
|
}
|
|
6955
|
+
function resolveSenderName(input) {
|
|
6956
|
+
const { override, envAgentId, agents } = input;
|
|
6957
|
+
if (agents.size === 0) return { kind: "none" };
|
|
6958
|
+
if (override) return {
|
|
6959
|
+
kind: "ok",
|
|
6960
|
+
name: override
|
|
6961
|
+
};
|
|
6962
|
+
if (envAgentId) {
|
|
6963
|
+
for (const [name, cfg] of agents) if (cfg.agentId === envAgentId) return {
|
|
6964
|
+
kind: "ok",
|
|
6965
|
+
name
|
|
6966
|
+
};
|
|
6967
|
+
return {
|
|
6968
|
+
kind: "envMismatch",
|
|
6969
|
+
envAgentId,
|
|
6970
|
+
available: [...agents.keys()]
|
|
6971
|
+
};
|
|
6972
|
+
}
|
|
6973
|
+
if (agents.size === 1) {
|
|
6974
|
+
const [only] = [...agents.keys()];
|
|
6975
|
+
if (only) return {
|
|
6976
|
+
kind: "ok",
|
|
6977
|
+
name: only
|
|
6978
|
+
};
|
|
6979
|
+
}
|
|
6980
|
+
return {
|
|
6981
|
+
kind: "ambiguous",
|
|
6982
|
+
available: [...agents.keys()]
|
|
6983
|
+
};
|
|
6984
|
+
}
|
|
6897
6985
|
//#endregion
|
|
6898
6986
|
//#region src/core/admin.ts
|
|
6899
6987
|
/**
|
|
@@ -7213,6 +7301,14 @@ var ClientRuntime = class {
|
|
|
7213
7301
|
watcher = null;
|
|
7214
7302
|
debounceTimer = null;
|
|
7215
7303
|
/**
|
|
7304
|
+
* Per-org Context Tree binding resolved at `start()`. Threaded through every
|
|
7305
|
+
* `slot.start()` so handlers can copy `AGENT.md` / root `NODE.md` into the
|
|
7306
|
+
* agent workspace's `.agent/context/` and install the first-tree skill.
|
|
7307
|
+
* `null` when the user has no primary org, the org has no tree configured,
|
|
7308
|
+
* or git sync failed — handlers degrade gracefully (empty context dir).
|
|
7309
|
+
*/
|
|
7310
|
+
contextTreeBinding = null;
|
|
7311
|
+
/**
|
|
7216
7312
|
* Directory we write auto-registered agent configs into (same path that
|
|
7217
7313
|
* `first-tree-hub agent add` uses). Set by `watchAgentsDir` so the
|
|
7218
7314
|
* `agent:pinned` handler knows where to materialise new configs.
|
|
@@ -7272,6 +7368,7 @@ var ClientRuntime = class {
|
|
|
7272
7368
|
this.agentIds.add(config.agentId);
|
|
7273
7369
|
}
|
|
7274
7370
|
async start() {
|
|
7371
|
+
this.contextTreeBinding = await syncContextTree(this.serverUrl, (opts) => ensureFreshAccessToken(opts), (msg) => print.status("[context-tree]", msg), CLI_USER_AGENT);
|
|
7275
7372
|
if (this.options.currentVersion && this.options.update) this.updateManager = UpdateManager.attach(this.connection, {
|
|
7276
7373
|
currentVersion: this.options.currentVersion,
|
|
7277
7374
|
...this.options.update,
|
|
@@ -7290,7 +7387,7 @@ var ClientRuntime = class {
|
|
|
7290
7387
|
}
|
|
7291
7388
|
await Promise.allSettled(this.agents.map(async (agent) => {
|
|
7292
7389
|
try {
|
|
7293
|
-
const identity = await agent.slot.start();
|
|
7390
|
+
const identity = await agent.slot.start(this.contextTreeBinding);
|
|
7294
7391
|
print.check(true, `${agent.name}: connected`, `agent: ${identity.displayName ?? identity.agentId}`);
|
|
7295
7392
|
} catch (error) {
|
|
7296
7393
|
const msg = error instanceof Error ? error.message : String(error);
|
|
@@ -7417,7 +7514,7 @@ var ClientRuntime = class {
|
|
|
7417
7514
|
startAgent(name) {
|
|
7418
7515
|
const entry = this.agents.find((a) => a.name === name);
|
|
7419
7516
|
if (!entry) return;
|
|
7420
|
-
entry.slot.start().then((identity) => {
|
|
7517
|
+
entry.slot.start(this.contextTreeBinding).then((identity) => {
|
|
7421
7518
|
print.check(true, `${name}: connected`, `agent: ${identity.displayName ?? identity.agentId}`);
|
|
7422
7519
|
}).catch((err) => {
|
|
7423
7520
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -9054,7 +9151,7 @@ async function onboardCreate(args) {
|
|
|
9054
9151
|
}
|
|
9055
9152
|
const runtimeAgent = args.type === "human" ? args.assistant : args.id;
|
|
9056
9153
|
if (args.feishuBotAppId && args.feishuBotAppSecret) {
|
|
9057
|
-
const { bindFeishuBot } = await import("./feishu-
|
|
9154
|
+
const { bindFeishuBot } = await import("./feishu-CKGzIamp.mjs").then((n) => n.r);
|
|
9058
9155
|
const targetAgentUuid = args.type === "human" ? assistantUuid : primary.uuid;
|
|
9059
9156
|
if (!targetAgentUuid) print.line(`Warning: Cannot bind Feishu bot — no runtime agent available for "${args.id}".\n`);
|
|
9060
9157
|
else {
|
|
@@ -10267,7 +10364,7 @@ function createFeedbackHandler(config) {
|
|
|
10267
10364
|
return { handle };
|
|
10268
10365
|
}
|
|
10269
10366
|
//#endregion
|
|
10270
|
-
//#region ../server/dist/app-
|
|
10367
|
+
//#region ../server/dist/app-kJNM9Cf1.mjs
|
|
10271
10368
|
var import_fastify_opentelemetry = /* @__PURE__ */ __toESM(require_fastify_opentelemetry(), 1);
|
|
10272
10369
|
init_esm();
|
|
10273
10370
|
var __defProp = Object.defineProperty;
|
|
@@ -10292,50 +10389,6 @@ const adapterAgentMappings = pgTable("adapter_agent_mappings", {
|
|
|
10292
10389
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
|
|
10293
10390
|
}, (table) => [unique("uq_adapter_agent_mapping").on(table.platform, table.externalUserId)]);
|
|
10294
10391
|
/**
|
|
10295
|
-
* Tasks — lightweight work units. Process descriptors, not tickets.
|
|
10296
|
-
* Immutable status state machine: pending → assigned → working → (completed | failed | cancelled).
|
|
10297
|
-
* Sub-tasks (parent_task_id) are deferred to a later phase.
|
|
10298
|
-
*
|
|
10299
|
-
* Referential integrity (org / assignee / chat) is enforced at the service layer,
|
|
10300
|
-
* not via DB foreign keys — see `services/task.ts`.
|
|
10301
|
-
*/
|
|
10302
|
-
const tasks = pgTable("tasks", {
|
|
10303
|
-
id: text("id").primaryKey(),
|
|
10304
|
-
organizationId: text("organization_id").notNull(),
|
|
10305
|
-
title: text("title").notNull(),
|
|
10306
|
-
body: text("body").notNull().default(""),
|
|
10307
|
-
status: text("status").$type().notNull(),
|
|
10308
|
-
assigneeAgentId: text("assignee_agent_id"),
|
|
10309
|
-
createdByType: text("created_by_type").$type().notNull(),
|
|
10310
|
-
createdById: text("created_by_id").notNull(),
|
|
10311
|
-
originRef: text("origin_ref"),
|
|
10312
|
-
result: text("result"),
|
|
10313
|
-
metadata: jsonb("metadata").$type().notNull().default({}),
|
|
10314
|
-
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
10315
|
-
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
|
10316
|
-
cancelledAt: timestamp("cancelled_at", { withTimezone: true }),
|
|
10317
|
-
cancelledByType: text("cancelled_by_type").$type(),
|
|
10318
|
-
cancelledById: text("cancelled_by_id")
|
|
10319
|
-
}, (table) => [
|
|
10320
|
-
index("idx_tasks_org_status").on(table.organizationId, table.status),
|
|
10321
|
-
index("idx_tasks_assignee_status").on(table.assigneeAgentId, table.status),
|
|
10322
|
-
index("idx_tasks_origin_ref").on(table.originRef),
|
|
10323
|
-
index("idx_tasks_org_created_at").on(table.organizationId, table.createdAt)
|
|
10324
|
-
]);
|
|
10325
|
-
/**
|
|
10326
|
-
* Task ↔ Chat association (M:N). A task may be executed across multiple chats;
|
|
10327
|
-
* a chat may host work for multiple tasks over its lifetime.
|
|
10328
|
-
*
|
|
10329
|
-
* No FK constraints — when a task or chat is deleted, the service layer is
|
|
10330
|
-
* responsible for deleting linked rows here first.
|
|
10331
|
-
*/
|
|
10332
|
-
const taskChats = pgTable("task_chats", {
|
|
10333
|
-
taskId: text("task_id").notNull(),
|
|
10334
|
-
chatId: text("chat_id").notNull(),
|
|
10335
|
-
linkedByAgentId: text("linked_by_agent_id"),
|
|
10336
|
-
linkedAt: timestamp("linked_at", { withTimezone: true }).notNull().defaultNow()
|
|
10337
|
-
}, (table) => [primaryKey({ columns: [table.taskId, table.chatId] }), index("idx_task_chats_chat").on(table.chatId)]);
|
|
10338
|
-
/**
|
|
10339
10392
|
* Pull the JWT-verified `UserScope` off the request. The `userAuthHook`
|
|
10340
10393
|
* middleware populates `request.user` synchronously before any handler
|
|
10341
10394
|
* runs; this helper just narrows the optional and throws a clean 401 if
|
|
@@ -10470,31 +10523,6 @@ async function assertAgentManageableByUser(db, userId, agentUuid) {
|
|
|
10470
10523
|
return scope;
|
|
10471
10524
|
}
|
|
10472
10525
|
/**
|
|
10473
|
-
* Gate access to a task. Allowed for any active member of the task's org —
|
|
10474
|
-
* mirrors the original inline gate in `api/tasks.ts` that this helper
|
|
10475
|
-
* replaces. Returns both the task's org row and the caller's resolved
|
|
10476
|
-
* `OrgScope`, so handlers can read `scope.memberId` for audit fields.
|
|
10477
|
-
*/
|
|
10478
|
-
async function requireTaskAccess(request, db) {
|
|
10479
|
-
const { userId } = requireUser(request);
|
|
10480
|
-
const { taskId } = request.params;
|
|
10481
|
-
const [task] = await db.select({ organizationId: tasks.organizationId }).from(tasks).where(eq(tasks.id, taskId)).limit(1);
|
|
10482
|
-
if (!task) throw new NotFoundError(`Task "${taskId}" not found`);
|
|
10483
|
-
const caller = await resolveCallerInOrg(db, userId, task.organizationId);
|
|
10484
|
-
const scope = {
|
|
10485
|
-
userId,
|
|
10486
|
-
organizationId: task.organizationId,
|
|
10487
|
-
memberId: caller.memberId,
|
|
10488
|
-
role: caller.role,
|
|
10489
|
-
humanAgentId: caller.humanAgentId
|
|
10490
|
-
};
|
|
10491
|
-
stampOrgScope(request, scope);
|
|
10492
|
-
return {
|
|
10493
|
-
task,
|
|
10494
|
-
scope
|
|
10495
|
-
};
|
|
10496
|
-
}
|
|
10497
|
-
/**
|
|
10498
10526
|
* Assert every agent in `agentIds` is visible to `scope` and lives in
|
|
10499
10527
|
* `scope.organizationId`. Used by chat-create to keep visibility rules out of
|
|
10500
10528
|
* the service layer's signature.
|
|
@@ -10930,10 +10958,9 @@ async function ensureDefaultOrganization(db) {
|
|
|
10930
10958
|
return org ?? existing;
|
|
10931
10959
|
}
|
|
10932
10960
|
/**
|
|
10933
|
-
* Names beginning with `__` are reserved for Hub-internal pseudo agents
|
|
10934
|
-
*
|
|
10935
|
-
*
|
|
10936
|
-
* real account.
|
|
10961
|
+
* Names beginning with `__` are reserved for Hub-internal pseudo agents.
|
|
10962
|
+
* User-facing creation must not be able to squat on them, otherwise
|
|
10963
|
+
* internal traffic could be routed through a real account.
|
|
10937
10964
|
*/
|
|
10938
10965
|
const RESERVED_AGENT_NAME_PREFIX = "__";
|
|
10939
10966
|
/**
|
|
@@ -11503,16 +11530,17 @@ async function findOrCreateChatForChannel(db, data) {
|
|
|
11503
11530
|
return db.transaction(async (tx) => {
|
|
11504
11531
|
const [botAgent] = await tx.select({ organizationId: agents.organizationId }).from(agents).where(eq(agents.uuid, data.botAgentId)).limit(1);
|
|
11505
11532
|
const orgId = botAgent?.organizationId ?? await resolveDefaultOrgId(db);
|
|
11533
|
+
const metadata = chatMetadataSchema$1.parse({
|
|
11534
|
+
source: data.platform,
|
|
11535
|
+
externalChannelId: data.externalChannelId
|
|
11536
|
+
});
|
|
11506
11537
|
await tx.insert(chats).values({
|
|
11507
11538
|
id: chatId,
|
|
11508
11539
|
organizationId: orgId,
|
|
11509
11540
|
type: internalType,
|
|
11510
11541
|
topic: data.topic ?? null,
|
|
11511
11542
|
lifecyclePolicy: "adapter_managed",
|
|
11512
|
-
metadata
|
|
11513
|
-
source: data.platform,
|
|
11514
|
-
externalChannelId: data.externalChannelId
|
|
11515
|
-
}
|
|
11543
|
+
metadata
|
|
11516
11544
|
});
|
|
11517
11545
|
const participants = data.botAgentId === data.senderAgentId ? [{
|
|
11518
11546
|
chatId,
|
|
@@ -12143,462 +12171,6 @@ async function agentSendToAgentRoutes(app) {
|
|
|
12143
12171
|
});
|
|
12144
12172
|
});
|
|
12145
12173
|
}
|
|
12146
|
-
/** Legal status transitions. Service enforces; API maps violations to 400. */
|
|
12147
|
-
const STATUS_TRANSITIONS = {
|
|
12148
|
-
pending: ["assigned", "cancelled"],
|
|
12149
|
-
assigned: ["working", "cancelled"],
|
|
12150
|
-
working: [
|
|
12151
|
-
"completed",
|
|
12152
|
-
"failed",
|
|
12153
|
-
"cancelled"
|
|
12154
|
-
],
|
|
12155
|
-
completed: [],
|
|
12156
|
-
failed: [],
|
|
12157
|
-
cancelled: []
|
|
12158
|
-
};
|
|
12159
|
-
function isLegalTransition(from, to) {
|
|
12160
|
-
return STATUS_TRANSITIONS[from]?.includes(to) ?? false;
|
|
12161
|
-
}
|
|
12162
|
-
function isTerminal(status) {
|
|
12163
|
-
return TASK_TERMINAL_STATUSES.includes(status);
|
|
12164
|
-
}
|
|
12165
|
-
/**
|
|
12166
|
-
* Reserved name for the hub-owned task notifier pseudo agent. The `__` prefix
|
|
12167
|
-
* is rejected by `createAgent`, so real users cannot squat on this identity.
|
|
12168
|
-
*/
|
|
12169
|
-
const SYSTEM_TASKS_AGENT_NAME = "__hub_system_tasks";
|
|
12170
|
-
/**
|
|
12171
|
-
* Ensure a task-notifier pseudo agent exists in the given organization and
|
|
12172
|
-
* return its UUID. Used as the sender for task notification messages so they
|
|
12173
|
-
* flow through the normal chat/inbox pipeline. Idempotent under concurrent
|
|
12174
|
-
* creation via the unique `(organization_id, name)` constraint.
|
|
12175
|
-
*/
|
|
12176
|
-
async function ensureSystemTasksAgent(db, organizationId) {
|
|
12177
|
-
const [existing] = await db.select({ uuid: agents.uuid }).from(agents).where(and(eq(agents.organizationId, organizationId), eq(agents.name, SYSTEM_TASKS_AGENT_NAME), ne(agents.status, AGENT_STATUSES.DELETED))).limit(1);
|
|
12178
|
-
if (existing) return existing.uuid;
|
|
12179
|
-
const uuid = uuidv7();
|
|
12180
|
-
const inboxId = `inbox_${uuid}`;
|
|
12181
|
-
const [adminMember] = await db.select({ id: members.id }).from(members).where(and(eq(members.organizationId, organizationId), eq(members.role, "admin"))).orderBy(asc(members.createdAt)).limit(1);
|
|
12182
|
-
if (!adminMember) throw new ConflictError(`Cannot create system tasks agent in organization "${organizationId}" — no admin member exists.`);
|
|
12183
|
-
try {
|
|
12184
|
-
const [created] = await db.insert(agents).values({
|
|
12185
|
-
uuid,
|
|
12186
|
-
name: SYSTEM_TASKS_AGENT_NAME,
|
|
12187
|
-
organizationId,
|
|
12188
|
-
type: AGENT_TYPES.AUTONOMOUS_AGENT,
|
|
12189
|
-
displayName: "System · Tasks",
|
|
12190
|
-
inboxId,
|
|
12191
|
-
status: AGENT_STATUSES.ACTIVE,
|
|
12192
|
-
source: AGENT_SOURCES.ADMIN_API,
|
|
12193
|
-
metadata: {
|
|
12194
|
-
system: true,
|
|
12195
|
-
role: "task-notifier"
|
|
12196
|
-
},
|
|
12197
|
-
managerId: adminMember.id
|
|
12198
|
-
}).returning({ uuid: agents.uuid });
|
|
12199
|
-
if (created) return created.uuid;
|
|
12200
|
-
} catch (err) {
|
|
12201
|
-
if ((err?.code ?? err?.cause?.code ?? "") !== "23505") throw err;
|
|
12202
|
-
}
|
|
12203
|
-
const [row] = await db.select({ uuid: agents.uuid }).from(agents).where(and(eq(agents.organizationId, organizationId), eq(agents.name, SYSTEM_TASKS_AGENT_NAME))).limit(1);
|
|
12204
|
-
if (!row) throw new Error("ensureSystemTasksAgent: agent missing after conflict");
|
|
12205
|
-
return row.uuid;
|
|
12206
|
-
}
|
|
12207
|
-
function resolveCreator(actor) {
|
|
12208
|
-
if (actor.type === "agent") return {
|
|
12209
|
-
type: TASK_CREATOR_TYPES.AGENT,
|
|
12210
|
-
id: actor.agentId
|
|
12211
|
-
};
|
|
12212
|
-
return {
|
|
12213
|
-
type: TASK_CREATOR_TYPES.ADMIN,
|
|
12214
|
-
id: actor.adminId
|
|
12215
|
-
};
|
|
12216
|
-
}
|
|
12217
|
-
/**
|
|
12218
|
-
* Assert the task allows the given agent actor to mutate its chat associations.
|
|
12219
|
-
* Only the creator or assignee (for agents) or any admin may do so.
|
|
12220
|
-
*/
|
|
12221
|
-
function assertCanMutateTaskChats(task, actor) {
|
|
12222
|
-
if (actor.type === "admin") return;
|
|
12223
|
-
const isAssignee = task.assigneeAgentId === actor.agentId;
|
|
12224
|
-
const isCreator = task.createdByType === TASK_CREATOR_TYPES.AGENT && task.createdById === actor.agentId;
|
|
12225
|
-
if (!isAssignee && !isCreator) throw new ForbiddenError("Only the task creator or assignee may modify its chat associations");
|
|
12226
|
-
}
|
|
12227
|
-
async function loadAssigneeOrThrow(db, assigneeAgentId, expectedOrgId) {
|
|
12228
|
-
const [assignee] = await db.select({
|
|
12229
|
-
uuid: agents.uuid,
|
|
12230
|
-
organizationId: agents.organizationId,
|
|
12231
|
-
status: agents.status
|
|
12232
|
-
}).from(agents).where(eq(agents.uuid, assigneeAgentId)).limit(1);
|
|
12233
|
-
if (!assignee || assignee.status === AGENT_STATUSES.DELETED) throw new BadRequestError(`Assignee agent "${assigneeAgentId}" not found`);
|
|
12234
|
-
if (assignee.organizationId !== expectedOrgId) throw new BadRequestError("Assignee agent belongs to a different organization");
|
|
12235
|
-
if (assignee.status === AGENT_STATUSES.SUSPENDED) throw new BadRequestError(`Assignee agent "${assigneeAgentId}" is suspended`);
|
|
12236
|
-
return assignee;
|
|
12237
|
-
}
|
|
12238
|
-
/**
|
|
12239
|
-
* Create a task.
|
|
12240
|
-
*
|
|
12241
|
-
* Initial status is determined by assignee:
|
|
12242
|
-
* - no assignee → "pending"
|
|
12243
|
-
* - assignee is an agent and equals the creator → "working" (work-first; no notification)
|
|
12244
|
-
* - assignee set and differs from creator → "assigned" (task-first; notification dispatched)
|
|
12245
|
-
*
|
|
12246
|
-
* Task-first notifications go through the regular message+inbox pipeline via a per-org
|
|
12247
|
-
* task-notifier pseudo agent. The caller is responsible for triggering notifier fan-out
|
|
12248
|
-
* using the returned notification recipients.
|
|
12249
|
-
*/
|
|
12250
|
-
async function createTask(db, actor, input) {
|
|
12251
|
-
const [org] = await db.select({ id: organizations.id }).from(organizations).where(eq(organizations.id, input.organizationId)).limit(1);
|
|
12252
|
-
if (!org) throw new NotFoundError(`Organization "${input.organizationId}" not found`);
|
|
12253
|
-
if (input.assigneeAgentId) await loadAssigneeOrThrow(db, input.assigneeAgentId, input.organizationId);
|
|
12254
|
-
if (actor.type === "agent" && actor.organizationId !== input.organizationId) throw new ForbiddenError("Cannot create tasks in a different organization");
|
|
12255
|
-
const creator = resolveCreator(actor);
|
|
12256
|
-
const selfAssigned = input.assigneeAgentId !== void 0 && actor.type === "agent" && input.assigneeAgentId === actor.agentId;
|
|
12257
|
-
let initialStatus;
|
|
12258
|
-
if (!input.assigneeAgentId) initialStatus = TASK_STATUSES.PENDING;
|
|
12259
|
-
else if (selfAssigned) initialStatus = TASK_STATUSES.WORKING;
|
|
12260
|
-
else initialStatus = TASK_STATUSES.ASSIGNED;
|
|
12261
|
-
const taskId = uuidv7();
|
|
12262
|
-
const [task] = await db.insert(tasks).values({
|
|
12263
|
-
id: taskId,
|
|
12264
|
-
organizationId: input.organizationId,
|
|
12265
|
-
title: input.title,
|
|
12266
|
-
body: input.body ?? "",
|
|
12267
|
-
status: initialStatus,
|
|
12268
|
-
assigneeAgentId: input.assigneeAgentId ?? null,
|
|
12269
|
-
createdByType: creator.type,
|
|
12270
|
-
createdById: creator.id,
|
|
12271
|
-
originRef: input.originRef ?? null,
|
|
12272
|
-
metadata: input.metadata ?? {}
|
|
12273
|
-
}).returning();
|
|
12274
|
-
if (!task) throw new Error("Unexpected: INSERT RETURNING produced no row");
|
|
12275
|
-
let notification;
|
|
12276
|
-
if (initialStatus === TASK_STATUSES.ASSIGNED && task.assigneeAgentId) notification = await dispatchTaskSystemMessage(db, task, "assigned");
|
|
12277
|
-
return {
|
|
12278
|
-
task,
|
|
12279
|
-
notification
|
|
12280
|
-
};
|
|
12281
|
-
}
|
|
12282
|
-
/** Compose and send a system message describing a task state change to the assignee's chat. */
|
|
12283
|
-
async function dispatchTaskSystemMessage(db, task, event, fromStatus) {
|
|
12284
|
-
if (!task.assigneeAgentId) return void 0;
|
|
12285
|
-
const systemAgentId = await ensureSystemTasksAgent(db, task.organizationId);
|
|
12286
|
-
if (systemAgentId === task.assigneeAgentId) return void 0;
|
|
12287
|
-
const chat = await findOrCreateDirectChat(db, systemAgentId, task.assigneeAgentId);
|
|
12288
|
-
const content = {
|
|
12289
|
-
taskId: task.id,
|
|
12290
|
-
event,
|
|
12291
|
-
title: task.title,
|
|
12292
|
-
body: task.body,
|
|
12293
|
-
status: task.status,
|
|
12294
|
-
...fromStatus ? { fromStatus } : {},
|
|
12295
|
-
originRef: task.originRef
|
|
12296
|
-
};
|
|
12297
|
-
return sendMessage(db, chat.id, systemAgentId, {
|
|
12298
|
-
format: "task",
|
|
12299
|
-
content,
|
|
12300
|
-
metadata: {
|
|
12301
|
-
taskId: task.id,
|
|
12302
|
-
event,
|
|
12303
|
-
mentions: [task.assigneeAgentId]
|
|
12304
|
-
}
|
|
12305
|
-
});
|
|
12306
|
-
}
|
|
12307
|
-
/**
|
|
12308
|
-
* Fetch a task, optionally asserting it belongs to `expectedOrgId`. Cross-org
|
|
12309
|
-
* access is reported as NotFound so we don't leak existence across tenants.
|
|
12310
|
-
*/
|
|
12311
|
-
async function getTask(db, taskId, expectedOrgId) {
|
|
12312
|
-
const [task] = await db.select().from(tasks).where(eq(tasks.id, taskId)).limit(1);
|
|
12313
|
-
if (!task) throw new NotFoundError(`Task "${taskId}" not found`);
|
|
12314
|
-
if (expectedOrgId !== void 0 && task.organizationId !== expectedOrgId) throw new NotFoundError(`Task "${taskId}" not found`);
|
|
12315
|
-
return task;
|
|
12316
|
-
}
|
|
12317
|
-
async function getTaskDetail(db, taskId, expectedOrgId) {
|
|
12318
|
-
const task = await getTask(db, taskId, expectedOrgId);
|
|
12319
|
-
const links = await db.select().from(taskChats).where(eq(taskChats.taskId, taskId));
|
|
12320
|
-
return {
|
|
12321
|
-
...task,
|
|
12322
|
-
chats: links.map((c) => ({
|
|
12323
|
-
taskId: c.taskId,
|
|
12324
|
-
chatId: c.chatId,
|
|
12325
|
-
linkedByAgentId: c.linkedByAgentId,
|
|
12326
|
-
linkedAt: c.linkedAt.toISOString()
|
|
12327
|
-
}))
|
|
12328
|
-
};
|
|
12329
|
-
}
|
|
12330
|
-
async function listTasks(db, organizationId, query) {
|
|
12331
|
-
const conditions = [eq(tasks.organizationId, organizationId)];
|
|
12332
|
-
if (query.status) conditions.push(eq(tasks.status, query.status));
|
|
12333
|
-
if (query.assigneeAgentId) conditions.push(eq(tasks.assigneeAgentId, query.assigneeAgentId));
|
|
12334
|
-
if (query.originRef) conditions.push(eq(tasks.originRef, query.originRef));
|
|
12335
|
-
if (query.cursor) conditions.push(lt(tasks.createdAt, new Date(query.cursor)));
|
|
12336
|
-
const rows = await db.select().from(tasks).where(and(...conditions)).orderBy(desc(tasks.createdAt)).limit(query.limit + 1);
|
|
12337
|
-
const hasMore = rows.length > query.limit;
|
|
12338
|
-
const items = hasMore ? rows.slice(0, query.limit) : rows;
|
|
12339
|
-
const last = items[items.length - 1];
|
|
12340
|
-
return {
|
|
12341
|
-
items,
|
|
12342
|
-
nextCursor: hasMore && last ? last.createdAt.toISOString() : null
|
|
12343
|
-
};
|
|
12344
|
-
}
|
|
12345
|
-
/** Agent self-report: working / completed / failed. */
|
|
12346
|
-
async function updateTaskStatus(db, taskId, actor, data) {
|
|
12347
|
-
const existing = await getTask(db, taskId);
|
|
12348
|
-
if (actor.type !== "agent") throw new ForbiddenError("updateTaskStatus is for agent self-report; use adminUpdateTask for admin actions");
|
|
12349
|
-
if (existing.assigneeAgentId !== actor.agentId) throw new ForbiddenError("Only the assignee may update this task");
|
|
12350
|
-
const from = existing.status;
|
|
12351
|
-
const to = data.status;
|
|
12352
|
-
if (!isLegalTransition(from, to)) throw new BadRequestError(`Illegal status transition: ${from} → ${to}`);
|
|
12353
|
-
if (to === TASK_STATUSES.COMPLETED && data.result === void 0) throw new BadRequestError("Completion requires a result (may be an empty string)");
|
|
12354
|
-
const updates = {
|
|
12355
|
-
status: to,
|
|
12356
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
12357
|
-
};
|
|
12358
|
-
if (data.result !== void 0) updates.result = data.result;
|
|
12359
|
-
const [updated] = await db.update(tasks).set(updates).where(eq(tasks.id, taskId)).returning();
|
|
12360
|
-
if (!updated) throw new Error("Unexpected: UPDATE RETURNING produced no row");
|
|
12361
|
-
return { task: updated };
|
|
12362
|
-
}
|
|
12363
|
-
/** Admin-facing update: may re-assign while pending, or force a status transition (still gated by state machine). */
|
|
12364
|
-
async function adminUpdateTask(db, taskId, actor, data) {
|
|
12365
|
-
if (actor.type !== "admin") throw new ForbiddenError("adminUpdateTask requires admin actor");
|
|
12366
|
-
const existing = await getTask(db, taskId);
|
|
12367
|
-
if (data.status === TASK_STATUSES.CANCELLED) {
|
|
12368
|
-
if (isTerminal(existing.status)) throw new ConflictError(`Task is already in terminal state "${existing.status}"`);
|
|
12369
|
-
return cancelTask(db, taskId, actor);
|
|
12370
|
-
}
|
|
12371
|
-
const updates = { updatedAt: /* @__PURE__ */ new Date() };
|
|
12372
|
-
let notify = false;
|
|
12373
|
-
if (data.assigneeAgentId !== void 0) {
|
|
12374
|
-
if (existing.status !== TASK_STATUSES.PENDING && data.assigneeAgentId !== existing.assigneeAgentId) throw new BadRequestError("Cannot reassign a task that is not pending");
|
|
12375
|
-
if (data.assigneeAgentId !== null) {
|
|
12376
|
-
await loadAssigneeOrThrow(db, data.assigneeAgentId, existing.organizationId);
|
|
12377
|
-
updates.assigneeAgentId = data.assigneeAgentId;
|
|
12378
|
-
updates.status = TASK_STATUSES.ASSIGNED;
|
|
12379
|
-
notify = true;
|
|
12380
|
-
} else {
|
|
12381
|
-
updates.assigneeAgentId = null;
|
|
12382
|
-
updates.status = TASK_STATUSES.PENDING;
|
|
12383
|
-
}
|
|
12384
|
-
}
|
|
12385
|
-
if (data.status !== void 0 && data.status !== existing.status) {
|
|
12386
|
-
const from = updates.status ?? existing.status;
|
|
12387
|
-
if (!isLegalTransition(from, data.status)) throw new BadRequestError(`Illegal status transition: ${from} → ${data.status}`);
|
|
12388
|
-
updates.status = data.status;
|
|
12389
|
-
}
|
|
12390
|
-
if (data.result !== void 0) updates.result = data.result;
|
|
12391
|
-
const resolvedStatus = updates.status ?? existing.status;
|
|
12392
|
-
const resolvedAssignee = updates.assigneeAgentId === void 0 ? existing.assigneeAgentId : updates.assigneeAgentId;
|
|
12393
|
-
if (resolvedStatus === TASK_STATUSES.ASSIGNED && !resolvedAssignee) throw new BadRequestError("Cannot set status to \"assigned\" without an assignee");
|
|
12394
|
-
const [updated] = await db.update(tasks).set(updates).where(eq(tasks.id, taskId)).returning();
|
|
12395
|
-
if (!updated) throw new Error("Unexpected: UPDATE RETURNING produced no row");
|
|
12396
|
-
let notification;
|
|
12397
|
-
if (notify && updated.assigneeAgentId) notification = await dispatchTaskSystemMessage(db, updated, "assigned");
|
|
12398
|
-
return {
|
|
12399
|
-
task: updated,
|
|
12400
|
-
notification
|
|
12401
|
-
};
|
|
12402
|
-
}
|
|
12403
|
-
async function cancelTask(db, taskId, actor) {
|
|
12404
|
-
const existing = await getTask(db, taskId);
|
|
12405
|
-
if (isTerminal(existing.status)) throw new ConflictError(`Task is already in terminal state "${existing.status}"`);
|
|
12406
|
-
if (actor.type === "agent") {
|
|
12407
|
-
const isAssignee = existing.assigneeAgentId === actor.agentId;
|
|
12408
|
-
const isCreator = existing.createdByType === TASK_CREATOR_TYPES.AGENT && existing.createdById === actor.agentId;
|
|
12409
|
-
if (!isAssignee && !isCreator) throw new ForbiddenError("Only the assignee or creator may cancel this task");
|
|
12410
|
-
}
|
|
12411
|
-
const now = /* @__PURE__ */ new Date();
|
|
12412
|
-
const { type: cancelType, id: cancelId } = resolveCreator(actor);
|
|
12413
|
-
const [updated] = await db.update(tasks).set({
|
|
12414
|
-
status: TASK_STATUSES.CANCELLED,
|
|
12415
|
-
cancelledAt: now,
|
|
12416
|
-
cancelledByType: cancelType,
|
|
12417
|
-
cancelledById: cancelId,
|
|
12418
|
-
updatedAt: now
|
|
12419
|
-
}).where(eq(tasks.id, taskId)).returning();
|
|
12420
|
-
if (!updated) throw new Error("Unexpected: UPDATE RETURNING produced no row");
|
|
12421
|
-
let notification;
|
|
12422
|
-
if (updated.assigneeAgentId && !(actor.type === "agent" && actor.agentId === updated.assigneeAgentId)) notification = await dispatchTaskSystemMessage(db, updated, "cancelled", existing.status);
|
|
12423
|
-
return {
|
|
12424
|
-
task: updated,
|
|
12425
|
-
notification
|
|
12426
|
-
};
|
|
12427
|
-
}
|
|
12428
|
-
async function linkChatToTask(db, taskId, chatId, actor) {
|
|
12429
|
-
const task = await getTask(db, taskId);
|
|
12430
|
-
if (actor.type === "agent" && task.organizationId !== actor.organizationId) throw new NotFoundError(`Task "${taskId}" not found`);
|
|
12431
|
-
assertCanMutateTaskChats(task, actor);
|
|
12432
|
-
const [chat] = await db.select({ organizationId: chats.organizationId }).from(chats).where(eq(chats.id, chatId)).limit(1);
|
|
12433
|
-
if (!chat) throw new NotFoundError(`Chat "${chatId}" not found`);
|
|
12434
|
-
if (chat.organizationId !== task.organizationId) throw new BadRequestError("Chat belongs to a different organization");
|
|
12435
|
-
if (actor.type === "agent") await assertParticipant(db, chatId, actor.agentId);
|
|
12436
|
-
const linkedBy = actor.type === "agent" ? actor.agentId : null;
|
|
12437
|
-
await db.insert(taskChats).values({
|
|
12438
|
-
taskId,
|
|
12439
|
-
chatId,
|
|
12440
|
-
linkedByAgentId: linkedBy
|
|
12441
|
-
}).onConflictDoNothing();
|
|
12442
|
-
}
|
|
12443
|
-
async function unlinkChatFromTask(db, taskId, chatId, actor) {
|
|
12444
|
-
const task = await getTask(db, taskId);
|
|
12445
|
-
if (actor.type === "agent" && task.organizationId !== actor.organizationId) throw new NotFoundError(`Task "${taskId}" not found`);
|
|
12446
|
-
assertCanMutateTaskChats(task, actor);
|
|
12447
|
-
if ((await db.delete(taskChats).where(and(eq(taskChats.taskId, taskId), eq(taskChats.chatId, chatId))).returning({ chatId: taskChats.chatId })).length === 0) throw new NotFoundError(`Chat "${chatId}" is not linked to task "${taskId}"`);
|
|
12448
|
-
}
|
|
12449
|
-
/**
|
|
12450
|
-
* Derive a health signal for a task. Only meaningful for `working` tasks.
|
|
12451
|
-
* See hub-task-design Section 9 for the rules this implements.
|
|
12452
|
-
*
|
|
12453
|
-
* Algorithm (per linked chat for the assignee):
|
|
12454
|
-
* 1. No session row OR state != 'active' → idle_island candidate
|
|
12455
|
-
* 2. Session active, last message from assignee → awaiting_reply candidate
|
|
12456
|
-
* 3. Session active, last message from other → normal candidate
|
|
12457
|
-
* Across all linked chats, normal wins over awaiting_reply, which wins over idle_island.
|
|
12458
|
-
*/
|
|
12459
|
-
async function getTaskHealth(db, taskId, expectedOrgId) {
|
|
12460
|
-
const task = await getTask(db, taskId, expectedOrgId);
|
|
12461
|
-
if (task.status !== TASK_STATUSES.WORKING) return {
|
|
12462
|
-
taskId,
|
|
12463
|
-
signal: TASK_HEALTH_SIGNALS.NOT_APPLICABLE,
|
|
12464
|
-
reason: `Task status is "${task.status}" — health is only computed for working tasks`
|
|
12465
|
-
};
|
|
12466
|
-
if (!task.assigneeAgentId) return {
|
|
12467
|
-
taskId,
|
|
12468
|
-
signal: TASK_HEALTH_SIGNALS.NO_CHAT,
|
|
12469
|
-
reason: "Task has no assignee"
|
|
12470
|
-
};
|
|
12471
|
-
const linked = await db.select({
|
|
12472
|
-
chatId: taskChats.chatId,
|
|
12473
|
-
sessionState: agentChatSessions.state
|
|
12474
|
-
}).from(taskChats).leftJoin(agentChatSessions, and(eq(agentChatSessions.chatId, taskChats.chatId), eq(agentChatSessions.agentId, task.assigneeAgentId))).where(eq(taskChats.taskId, taskId));
|
|
12475
|
-
if (linked.length === 0) return {
|
|
12476
|
-
taskId,
|
|
12477
|
-
signal: TASK_HEALTH_SIGNALS.NO_CHAT,
|
|
12478
|
-
reason: "Task has no linked chats"
|
|
12479
|
-
};
|
|
12480
|
-
const chatSignals = [];
|
|
12481
|
-
for (const row of linked) {
|
|
12482
|
-
if (row.sessionState !== "active") {
|
|
12483
|
-
chatSignals.push(TASK_HEALTH_SIGNALS.IDLE_ISLAND);
|
|
12484
|
-
continue;
|
|
12485
|
-
}
|
|
12486
|
-
const [last] = await db.select({ senderId: messages.senderId }).from(messages).where(eq(messages.chatId, row.chatId)).orderBy(desc(messages.createdAt)).limit(1);
|
|
12487
|
-
if (!last) {
|
|
12488
|
-
chatSignals.push(TASK_HEALTH_SIGNALS.IDLE_ISLAND);
|
|
12489
|
-
continue;
|
|
12490
|
-
}
|
|
12491
|
-
if (last.senderId === task.assigneeAgentId) chatSignals.push(TASK_HEALTH_SIGNALS.AWAITING_REPLY);
|
|
12492
|
-
else chatSignals.push(TASK_HEALTH_SIGNALS.NORMAL);
|
|
12493
|
-
}
|
|
12494
|
-
if (chatSignals.includes(TASK_HEALTH_SIGNALS.NORMAL)) return {
|
|
12495
|
-
taskId,
|
|
12496
|
-
signal: TASK_HEALTH_SIGNALS.NORMAL,
|
|
12497
|
-
reason: "At least one linked chat is actively progressing"
|
|
12498
|
-
};
|
|
12499
|
-
if (chatSignals.includes(TASK_HEALTH_SIGNALS.AWAITING_REPLY)) return {
|
|
12500
|
-
taskId,
|
|
12501
|
-
signal: TASK_HEALTH_SIGNALS.AWAITING_REPLY,
|
|
12502
|
-
reason: "Assignee sent the last message and is waiting for a reply"
|
|
12503
|
-
};
|
|
12504
|
-
return {
|
|
12505
|
-
taskId,
|
|
12506
|
-
signal: TASK_HEALTH_SIGNALS.IDLE_ISLAND,
|
|
12507
|
-
reason: "No active session found for the assignee in any linked chat"
|
|
12508
|
-
};
|
|
12509
|
-
}
|
|
12510
|
-
/** Serialize a task row for API output. */
|
|
12511
|
-
function serializeTask(task) {
|
|
12512
|
-
return {
|
|
12513
|
-
...task,
|
|
12514
|
-
createdAt: task.createdAt.toISOString(),
|
|
12515
|
-
updatedAt: task.updatedAt.toISOString(),
|
|
12516
|
-
cancelledAt: task.cancelledAt ? task.cancelledAt.toISOString() : null
|
|
12517
|
-
};
|
|
12518
|
-
}
|
|
12519
|
-
function dispatch$2(notifier, result) {
|
|
12520
|
-
if (!result) return;
|
|
12521
|
-
notifyRecipients(notifier, result.recipients, result.message.id);
|
|
12522
|
-
}
|
|
12523
|
-
async function agentTaskRoutes(app) {
|
|
12524
|
-
/** Create a task. Agent creator; assignee defaults to self (work-first) if omitted. */
|
|
12525
|
-
app.post("/", async (request, reply) => {
|
|
12526
|
-
const identity = requireAgent(request);
|
|
12527
|
-
const body = createTaskSchema.parse(request.body);
|
|
12528
|
-
const { task, notification } = await createTask(app.db, {
|
|
12529
|
-
type: "agent",
|
|
12530
|
-
agentId: identity.uuid,
|
|
12531
|
-
organizationId: identity.organizationId
|
|
12532
|
-
}, {
|
|
12533
|
-
...body,
|
|
12534
|
-
organizationId: identity.organizationId
|
|
12535
|
-
});
|
|
12536
|
-
dispatch$2(app.notifier, notification);
|
|
12537
|
-
return reply.status(201).send(serializeTask(task));
|
|
12538
|
-
});
|
|
12539
|
-
app.get("/", async (request) => {
|
|
12540
|
-
const identity = requireAgent(request);
|
|
12541
|
-
const query = taskListQuerySchema.parse(request.query);
|
|
12542
|
-
const result = await listTasks(app.db, identity.organizationId, query);
|
|
12543
|
-
return {
|
|
12544
|
-
items: result.items.map((t) => serializeTask(t)),
|
|
12545
|
-
nextCursor: result.nextCursor
|
|
12546
|
-
};
|
|
12547
|
-
});
|
|
12548
|
-
app.get("/:taskId", async (request) => {
|
|
12549
|
-
const identity = requireAgent(request);
|
|
12550
|
-
const detail = await getTaskDetail(app.db, request.params.taskId, identity.organizationId);
|
|
12551
|
-
return {
|
|
12552
|
-
...serializeTask(detail),
|
|
12553
|
-
chats: detail.chats
|
|
12554
|
-
};
|
|
12555
|
-
});
|
|
12556
|
-
/** Agent self-report: working / completed / failed. */
|
|
12557
|
-
app.patch("/:taskId", async (request) => {
|
|
12558
|
-
const identity = requireAgent(request);
|
|
12559
|
-
const body = updateTaskStatusSchema.parse(request.body);
|
|
12560
|
-
const { task } = await updateTaskStatus(app.db, request.params.taskId, {
|
|
12561
|
-
type: "agent",
|
|
12562
|
-
agentId: identity.uuid,
|
|
12563
|
-
organizationId: identity.organizationId
|
|
12564
|
-
}, body);
|
|
12565
|
-
return serializeTask(task);
|
|
12566
|
-
});
|
|
12567
|
-
app.post("/:taskId/cancel", async (request) => {
|
|
12568
|
-
const identity = requireAgent(request);
|
|
12569
|
-
const { task, notification } = await cancelTask(app.db, request.params.taskId, {
|
|
12570
|
-
type: "agent",
|
|
12571
|
-
agentId: identity.uuid,
|
|
12572
|
-
organizationId: identity.organizationId
|
|
12573
|
-
});
|
|
12574
|
-
dispatch$2(app.notifier, notification);
|
|
12575
|
-
return serializeTask(task);
|
|
12576
|
-
});
|
|
12577
|
-
app.post("/:taskId/chats", async (request, reply) => {
|
|
12578
|
-
const identity = requireAgent(request);
|
|
12579
|
-
const body = linkTaskChatSchema.parse(request.body);
|
|
12580
|
-
await linkChatToTask(app.db, request.params.taskId, body.chatId, {
|
|
12581
|
-
type: "agent",
|
|
12582
|
-
agentId: identity.uuid,
|
|
12583
|
-
organizationId: identity.organizationId
|
|
12584
|
-
});
|
|
12585
|
-
return reply.status(204).send();
|
|
12586
|
-
});
|
|
12587
|
-
app.delete("/:taskId/chats/:chatId", async (request, reply) => {
|
|
12588
|
-
const identity = requireAgent(request);
|
|
12589
|
-
await unlinkChatFromTask(app.db, request.params.taskId, request.params.chatId, {
|
|
12590
|
-
type: "agent",
|
|
12591
|
-
agentId: identity.uuid,
|
|
12592
|
-
organizationId: identity.organizationId
|
|
12593
|
-
});
|
|
12594
|
-
return reply.status(204).send();
|
|
12595
|
-
});
|
|
12596
|
-
/** Task health signal — only meaningful while task.status === "working". */
|
|
12597
|
-
app.get("/:taskId/health", async (request) => {
|
|
12598
|
-
const identity = requireAgent(request);
|
|
12599
|
-
return getTaskHealth(app.db, request.params.taskId, identity.organizationId);
|
|
12600
|
-
});
|
|
12601
|
-
}
|
|
12602
12174
|
/** WS close code: agent already connected from another client. */
|
|
12603
12175
|
const WS_CLOSE_ALREADY_CONNECTED = 4009;
|
|
12604
12176
|
/** Track active WS connections per agentId. At most one entry per agent. */
|
|
@@ -15174,9 +14746,7 @@ async function listMeChats(db, humanAgentId, organizationId, query) {
|
|
|
15174
14746
|
lastMessageAt: toDate(r.last_message_at)?.toISOString() ?? null,
|
|
15175
14747
|
lastMessagePreview: r.last_message_preview,
|
|
15176
14748
|
unreadMentionCount: r.unread_mention_count,
|
|
15177
|
-
canReply: r.membership_kind === "participant"
|
|
15178
|
-
taskId: null,
|
|
15179
|
-
taskStatus: null
|
|
14749
|
+
canReply: r.membership_kind === "participant"
|
|
15180
14750
|
};
|
|
15181
14751
|
}),
|
|
15182
14752
|
nextCursor
|
|
@@ -16894,7 +16464,7 @@ async function healthzRoutes(app) {
|
|
|
16894
16464
|
* `api/orgs/invitations.ts` (Class B, admin-gated).
|
|
16895
16465
|
*/
|
|
16896
16466
|
async function publicInvitationRoutes(app) {
|
|
16897
|
-
const { previewInvitation } = await import("./invitation-C299fxkP-
|
|
16467
|
+
const { previewInvitation } = await import("./invitation-C299fxkP-Dts66QTU.mjs");
|
|
16898
16468
|
app.get("/:token/preview", async (request, reply) => {
|
|
16899
16469
|
if (!request.params.token) throw new UnauthorizedError("Token required");
|
|
16900
16470
|
const preview = await previewInvitation(app.db, request.params.token);
|
|
@@ -17074,7 +16644,7 @@ async function meRoutes(app) {
|
|
|
17074
16644
|
*/
|
|
17075
16645
|
app.get("/me/pinned-agents", async (request) => {
|
|
17076
16646
|
const { userId } = requireUser(request);
|
|
17077
|
-
const { listMyPinnedAgents } = await import("./client-
|
|
16647
|
+
const { listMyPinnedAgents } = await import("./client-DSM_opoz-BH5eegXb.mjs");
|
|
17078
16648
|
return listMyPinnedAgents(app.db, { userId });
|
|
17079
16649
|
});
|
|
17080
16650
|
/**
|
|
@@ -17887,39 +17457,6 @@ function enrichOutput(namespace, out, orgId, publicUrl) {
|
|
|
17887
17457
|
}
|
|
17888
17458
|
return out;
|
|
17889
17459
|
}
|
|
17890
|
-
function dispatch$1(notifier, result) {
|
|
17891
|
-
if (!result) return;
|
|
17892
|
-
notifyRecipients(notifier, result.recipients, result.message.id);
|
|
17893
|
-
}
|
|
17894
|
-
/** Class B — `/api/v1/orgs/:orgId/tasks`. Per-task ops live in api/tasks.ts. */
|
|
17895
|
-
async function orgTaskRoutes(app) {
|
|
17896
|
-
app.get("/", async (request) => {
|
|
17897
|
-
const scope = await requireOrgMembership(request, app.db);
|
|
17898
|
-
const query = taskListQuerySchema.parse(request.query);
|
|
17899
|
-
const result = await listTasks(app.db, scope.organizationId, query);
|
|
17900
|
-
return {
|
|
17901
|
-
items: result.items.map((t) => serializeTask(t)),
|
|
17902
|
-
nextCursor: result.nextCursor
|
|
17903
|
-
};
|
|
17904
|
-
});
|
|
17905
|
-
app.post("/", async (request, reply) => {
|
|
17906
|
-
const scope = await requireOrgMembership(request, app.db);
|
|
17907
|
-
const body = adminCreateTaskSchema.parse(request.body);
|
|
17908
|
-
const { task, notification } = await createTask(app.db, {
|
|
17909
|
-
type: "admin",
|
|
17910
|
-
adminId: scope.memberId
|
|
17911
|
-
}, {
|
|
17912
|
-
title: body.title,
|
|
17913
|
-
body: body.body,
|
|
17914
|
-
...body.assigneeAgentId !== void 0 ? { assigneeAgentId: body.assigneeAgentId } : {},
|
|
17915
|
-
...body.originRef !== void 0 ? { originRef: body.originRef } : {},
|
|
17916
|
-
...body.metadata !== void 0 ? { metadata: body.metadata } : {},
|
|
17917
|
-
organizationId: scope.organizationId
|
|
17918
|
-
});
|
|
17919
|
-
dispatch$1(app.notifier, notification);
|
|
17920
|
-
return reply.status(201).send(serializeTask(task));
|
|
17921
|
-
});
|
|
17922
|
-
}
|
|
17923
17460
|
async function loadVisibleAgentIds(db, organizationId, memberId) {
|
|
17924
17461
|
const rows = await db.select({ id: agents.uuid }).from(agents).where(and(eq(agents.organizationId, organizationId), ne(agents.status, AGENT_STATUSES.DELETED), or(eq(agents.visibility, AGENT_VISIBILITY.ORGANIZATION), eq(agents.managerId, memberId))));
|
|
17925
17462
|
return new Set(rows.map((r) => r.id));
|
|
@@ -18140,97 +17677,380 @@ async function sessionRoutes(app) {
|
|
|
18140
17677
|
});
|
|
18141
17678
|
});
|
|
18142
17679
|
}
|
|
18143
|
-
function
|
|
18144
|
-
|
|
18145
|
-
|
|
17680
|
+
function isRecord(value) {
|
|
17681
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
17682
|
+
}
|
|
17683
|
+
/** Pull `repository.full_name` ("owner/repo") from a webhook payload, or null. */
|
|
17684
|
+
function repoFullName(payload) {
|
|
17685
|
+
if (!isRecord(payload)) return null;
|
|
17686
|
+
const repo = isRecord(payload.repository) ? payload.repository : null;
|
|
17687
|
+
return typeof repo?.full_name === "string" && repo.full_name.length > 0 ? repo.full_name : null;
|
|
17688
|
+
}
|
|
17689
|
+
function readNumber(value) {
|
|
17690
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
17691
|
+
}
|
|
17692
|
+
function readString(value) {
|
|
17693
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
17694
|
+
}
|
|
17695
|
+
/**
|
|
17696
|
+
* Resolve the entity that a GitHub webhook event belongs to.
|
|
17697
|
+
*
|
|
17698
|
+
* Returns `null` when the event isn't a clustering candidate (event type
|
|
17699
|
+
* outside the §4.1 "core" list, malformed payload). Caller is expected to
|
|
17700
|
+
* skip such events.
|
|
17701
|
+
*
|
|
17702
|
+
* Notes
|
|
17703
|
+
* - `commit_comment` falls back to a `commit` entity keyed on `<repo>@<sha>`
|
|
17704
|
+
* when no associated PR is in the payload — the design hedges on "optionally
|
|
17705
|
+
* resolve to a PR", but doing so requires an extra GitHub API call which we
|
|
17706
|
+
* defer to Phase 1+.
|
|
17707
|
+
*/
|
|
17708
|
+
function extractEventEntity(eventType, payload) {
|
|
17709
|
+
if (!isRecord(payload)) return null;
|
|
17710
|
+
const repo = repoFullName(payload);
|
|
17711
|
+
if (!repo) return null;
|
|
17712
|
+
switch (eventType) {
|
|
17713
|
+
case "issues":
|
|
17714
|
+
case "issue_comment": {
|
|
17715
|
+
const issue = isRecord(payload.issue) ? payload.issue : null;
|
|
17716
|
+
const number = readNumber(issue?.number);
|
|
17717
|
+
if (number === null) return null;
|
|
17718
|
+
return {
|
|
17719
|
+
type: "issue",
|
|
17720
|
+
key: `${repo}#${number}`,
|
|
17721
|
+
title: readString(issue?.title) ?? void 0,
|
|
17722
|
+
url: readString(issue?.html_url) ?? void 0
|
|
17723
|
+
};
|
|
17724
|
+
}
|
|
17725
|
+
case "pull_request":
|
|
17726
|
+
case "pull_request_review":
|
|
17727
|
+
case "pull_request_review_comment": {
|
|
17728
|
+
const pr = isRecord(payload.pull_request) ? payload.pull_request : null;
|
|
17729
|
+
const number = readNumber(pr?.number);
|
|
17730
|
+
if (number === null) return null;
|
|
17731
|
+
return {
|
|
17732
|
+
type: "pull_request",
|
|
17733
|
+
key: `${repo}#${number}`,
|
|
17734
|
+
title: readString(pr?.title) ?? void 0,
|
|
17735
|
+
url: readString(pr?.html_url) ?? void 0
|
|
17736
|
+
};
|
|
17737
|
+
}
|
|
17738
|
+
case "discussion":
|
|
17739
|
+
case "discussion_comment": {
|
|
17740
|
+
const disc = isRecord(payload.discussion) ? payload.discussion : null;
|
|
17741
|
+
const number = readNumber(disc?.number);
|
|
17742
|
+
if (number === null) return null;
|
|
17743
|
+
return {
|
|
17744
|
+
type: "discussion",
|
|
17745
|
+
key: `${repo}#discussion-${number}`,
|
|
17746
|
+
title: readString(disc?.title) ?? void 0,
|
|
17747
|
+
url: readString(disc?.html_url) ?? void 0
|
|
17748
|
+
};
|
|
17749
|
+
}
|
|
17750
|
+
case "commit_comment": {
|
|
17751
|
+
const comment = isRecord(payload.comment) ? payload.comment : null;
|
|
17752
|
+
const sha = readString(comment?.commit_id);
|
|
17753
|
+
if (!sha) return null;
|
|
17754
|
+
return {
|
|
17755
|
+
type: "commit",
|
|
17756
|
+
key: `${repo}@${sha}`,
|
|
17757
|
+
url: readString(comment?.html_url) ?? void 0
|
|
17758
|
+
};
|
|
17759
|
+
}
|
|
17760
|
+
default: return null;
|
|
17761
|
+
}
|
|
17762
|
+
}
|
|
17763
|
+
/**
|
|
17764
|
+
* Closing-keyword regex from
|
|
17765
|
+
* https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue
|
|
17766
|
+
* — `close[sd]? | fix(es|ed)? | resolve[sd]?`. Cross-repo `org/repo#N` is
|
|
17767
|
+
* deliberately excluded (out of scope for Phase 0; see §4.5).
|
|
17768
|
+
*/
|
|
17769
|
+
const FIXES_KEYWORDS_RE = /\b(?:close[sd]?|fix(?:es|ed)?|resolve[sd]?)\s+#(\d+)\b/gi;
|
|
17770
|
+
/**
|
|
17771
|
+
* Parse `Fixes #N` / `Closes #N` / `Resolves #N` references out of a PR body.
|
|
17772
|
+
* Returns ordered, deduplicated entity references for issues in the same repo
|
|
17773
|
+
* (cross-repo refs ignored per §4.5).
|
|
17774
|
+
*
|
|
17775
|
+
* Caller is expected to pass `repoFullName` so we can build the entity key.
|
|
17776
|
+
*/
|
|
17777
|
+
function parseFixesRefs(text, repoFullName) {
|
|
17778
|
+
if (!text) return [];
|
|
17779
|
+
const seen = /* @__PURE__ */ new Set();
|
|
17780
|
+
const out = [];
|
|
17781
|
+
for (const match of text.matchAll(FIXES_KEYWORDS_RE)) {
|
|
17782
|
+
const num = match[1];
|
|
17783
|
+
if (!num) continue;
|
|
17784
|
+
const key = `${repoFullName}#${num}`;
|
|
17785
|
+
if (seen.has(key)) continue;
|
|
17786
|
+
seen.add(key);
|
|
17787
|
+
out.push({
|
|
17788
|
+
type: "issue",
|
|
17789
|
+
key
|
|
17790
|
+
});
|
|
17791
|
+
}
|
|
17792
|
+
return out;
|
|
17793
|
+
}
|
|
17794
|
+
/**
|
|
17795
|
+
* Pick a chat-title prefix from (entity, eventType, action).
|
|
17796
|
+
*
|
|
17797
|
+
* PR review-flow events (`pull_request.review_requested`,
|
|
17798
|
+
* `pull_request_review.*`, `pull_request_review_comment.*`) collapse into a
|
|
17799
|
+
* single "PR Review" prefix so a chat first-touched by a review event is
|
|
17800
|
+
* visibly distinct from one first-touched by `pull_request.opened`. Everything
|
|
17801
|
+
* else just renders the entity type.
|
|
17802
|
+
*
|
|
17803
|
+
* Note: chat titles are written once at chat creation (see
|
|
17804
|
+
* `github-entity-chat.ts::createEntityChat`) — subsequent events for the same
|
|
17805
|
+
* entity reuse the existing title even if their (event, action) maps to a
|
|
17806
|
+
* different prefix. This matches the "entity is the container" semantic.
|
|
17807
|
+
*/
|
|
17808
|
+
function entityTitlePrefix(entity, eventType, action) {
|
|
17809
|
+
if (eventType === "pull_request" && action === "review_requested") return "PR Review";
|
|
17810
|
+
if (eventType === "pull_request_review") return "PR Review";
|
|
17811
|
+
if (eventType === "pull_request_review_comment") return "PR Review";
|
|
17812
|
+
switch (entity.type) {
|
|
17813
|
+
case "issue": return "Issue";
|
|
17814
|
+
case "pull_request": return "PR";
|
|
17815
|
+
case "discussion": return "Discussion";
|
|
17816
|
+
case "commit": return "Commit";
|
|
17817
|
+
}
|
|
17818
|
+
}
|
|
17819
|
+
/**
|
|
17820
|
+
* Strip the leading `owner/` segment from an entity key so the chat title
|
|
17821
|
+
* stays compact. `owner/repo#42` → `repo#42`; `owner/repo@abc1234` →
|
|
17822
|
+
* `repo@abc1234`. The full `owner/repo#N` form is still used as the
|
|
17823
|
+
* clustering primary key (`github_entity_chat_mappings.entity_key`); only the
|
|
17824
|
+
* display string is shortened.
|
|
17825
|
+
*/
|
|
17826
|
+
function shortEntityKey(key) {
|
|
17827
|
+
const slash = key.indexOf("/");
|
|
17828
|
+
return slash === -1 ? key : key.slice(slash + 1);
|
|
17829
|
+
}
|
|
17830
|
+
/**
|
|
17831
|
+
* Render a chat topic from an entity. Used as the chat title; kept short so
|
|
17832
|
+
* the chat-list row doesn't truncate aggressively.
|
|
17833
|
+
*
|
|
17834
|
+
* formatEntityTitle({ type: "pull_request", key: "owner/repo#307", title: "Improve overview" }, "pull_request", "opened")
|
|
17835
|
+
* → "PR repo#307: Improve overview"
|
|
17836
|
+
* formatEntityTitle(<same>, "pull_request", "review_requested")
|
|
17837
|
+
* → "PR Review repo#307: Improve overview"
|
|
17838
|
+
*/
|
|
17839
|
+
function formatEntityTitle(entity, eventType, action) {
|
|
17840
|
+
const head = `${entityTitlePrefix(entity, eventType, action)} ${shortEntityKey(entity.key)}`;
|
|
17841
|
+
if (entity.title && entity.title.length > 0) return `${head}: ${entity.title}`;
|
|
17842
|
+
return head;
|
|
17843
|
+
}
|
|
17844
|
+
const SILENT_EVENT_TYPES = new Set([
|
|
17845
|
+
"workflow_run",
|
|
17846
|
+
"workflow_job",
|
|
17847
|
+
"check_run",
|
|
17848
|
+
"check_suite",
|
|
17849
|
+
"status",
|
|
17850
|
+
"push",
|
|
17851
|
+
"create",
|
|
17852
|
+
"delete",
|
|
17853
|
+
"fork",
|
|
17854
|
+
"watch",
|
|
17855
|
+
"release",
|
|
17856
|
+
"label",
|
|
17857
|
+
"label_created",
|
|
17858
|
+
"label_deleted",
|
|
17859
|
+
"reaction",
|
|
17860
|
+
"member",
|
|
17861
|
+
"membership",
|
|
17862
|
+
"team",
|
|
17863
|
+
"team_add",
|
|
17864
|
+
"organization",
|
|
17865
|
+
"org_block",
|
|
17866
|
+
"project",
|
|
17867
|
+
"project_card",
|
|
17868
|
+
"project_column"
|
|
17869
|
+
]);
|
|
17870
|
+
/**
|
|
17871
|
+
* Per-event-type action-level filters. Frequent low-signal actions that would
|
|
17872
|
+
* otherwise spam an entity chat. `synchronize` (PR branch push) is the most
|
|
17873
|
+
* common offender — it fires on every commit push to a PR branch and never
|
|
17874
|
+
* carries new conversation.
|
|
17875
|
+
*/
|
|
17876
|
+
const SILENT_ACTIONS = {
|
|
17877
|
+
issues: new Set([
|
|
17878
|
+
"labeled",
|
|
17879
|
+
"unlabeled",
|
|
17880
|
+
"milestoned",
|
|
17881
|
+
"demilestoned",
|
|
17882
|
+
"pinned",
|
|
17883
|
+
"unpinned"
|
|
17884
|
+
]),
|
|
17885
|
+
pull_request: new Set([
|
|
17886
|
+
"labeled",
|
|
17887
|
+
"unlabeled",
|
|
17888
|
+
"auto_merge_enabled",
|
|
17889
|
+
"auto_merge_disabled",
|
|
17890
|
+
"synchronize"
|
|
17891
|
+
])
|
|
17892
|
+
};
|
|
17893
|
+
/** True iff the event should be silently 200-OKed without further routing. */
|
|
17894
|
+
function shouldSilent(eventType, payload) {
|
|
17895
|
+
if (SILENT_EVENT_TYPES.has(eventType)) return true;
|
|
17896
|
+
if (!isRecord(payload)) return false;
|
|
17897
|
+
if (readString((isRecord(payload.sender) ? payload.sender : null)?.type) === "Bot") return true;
|
|
17898
|
+
const action = readString(payload.action);
|
|
17899
|
+
if (!action) return false;
|
|
17900
|
+
return SILENT_ACTIONS[eventType]?.has(action) ?? false;
|
|
18146
17901
|
}
|
|
18147
|
-
/**
|
|
18148
|
-
|
|
18149
|
-
|
|
18150
|
-
|
|
18151
|
-
|
|
17902
|
+
/**
|
|
17903
|
+
* GitHub-specific webhook entity → chat clustering (Phase 0).
|
|
17904
|
+
*
|
|
17905
|
+
* Each `(organization, human_agent, delegate_agent, entity)` tuple resolves to
|
|
17906
|
+
* exactly one chat. Future external sources (Linear, Slack, …) get their own
|
|
17907
|
+
* tables — their entity models differ enough that a generic table would slip
|
|
17908
|
+
* back into untyped jsonb.
|
|
17909
|
+
*
|
|
17910
|
+
* `bound_via` distinguishes the first-touch row (`direct`) from a row written
|
|
17911
|
+
* by the `Fixes #N` linker (`fixes_link`). Routing logic ignores the
|
|
17912
|
+
* distinction; it exists for audit and future strategy tweaks.
|
|
17913
|
+
*/
|
|
17914
|
+
const githubEntityChatMappings = pgTable("github_entity_chat_mappings", {
|
|
17915
|
+
organizationId: text("organization_id").notNull().references(() => organizations.id),
|
|
17916
|
+
humanAgentId: text("human_agent_id").notNull().references(() => agents.uuid, { onDelete: "cascade" }),
|
|
17917
|
+
delegateAgentId: text("delegate_agent_id").notNull().references(() => agents.uuid, { onDelete: "cascade" }),
|
|
17918
|
+
entityType: text("entity_type").notNull(),
|
|
17919
|
+
entityKey: text("entity_key").notNull(),
|
|
17920
|
+
chatId: text("chat_id").notNull().references(() => chats.id, { onDelete: "cascade" }),
|
|
17921
|
+
boundAt: timestamp("bound_at", { withTimezone: true }).notNull().defaultNow(),
|
|
17922
|
+
boundVia: text("bound_via").notNull()
|
|
17923
|
+
}, (table) => [primaryKey({ columns: [
|
|
17924
|
+
table.organizationId,
|
|
17925
|
+
table.humanAgentId,
|
|
17926
|
+
table.delegateAgentId,
|
|
17927
|
+
table.entityType,
|
|
17928
|
+
table.entityKey
|
|
17929
|
+
] }), index("idx_github_entity_chat_mappings_chat").on(table.chatId)]);
|
|
17930
|
+
/**
|
|
17931
|
+
* Resolve which chat a GitHub event for (human, delegate, entity) belongs to.
|
|
17932
|
+
*
|
|
17933
|
+
* Three-step strategy from docs/webhook-routing-design.md §4.4:
|
|
17934
|
+
* a. Direct hit — entity already bound; reuse that chat.
|
|
17935
|
+
* b. Fixes-link — any related entity (parsed from `Fixes #N` in a PR body)
|
|
17936
|
+
* already bound; write a `fixes_link` row for this entity pointing at
|
|
17937
|
+
* the same chat, return it.
|
|
17938
|
+
* c. Miss — create a fresh chat via the canonical `createChat` entrypoint
|
|
17939
|
+
* and write a `direct` mapping row.
|
|
17940
|
+
*
|
|
17941
|
+
* Concurrent webhook deliveries for a never-before-seen entity race on (c);
|
|
17942
|
+
* the composite primary key + ON CONFLICT DO NOTHING ensures only one row
|
|
17943
|
+
* survives. The losing caller falls back to a re-read so the chat stays
|
|
17944
|
+
* unique.
|
|
17945
|
+
*/
|
|
17946
|
+
async function resolveTargetChat(db, params) {
|
|
17947
|
+
const { organizationId, humanAgentId, delegateAgentId, entity, relatedEntities, eventType, action } = params;
|
|
17948
|
+
const direct = await lookupMapping(db, organizationId, humanAgentId, delegateAgentId, entity);
|
|
17949
|
+
if (direct) return {
|
|
17950
|
+
chatId: direct.chatId,
|
|
17951
|
+
created: false,
|
|
17952
|
+
boundVia: direct.boundVia
|
|
17953
|
+
};
|
|
17954
|
+
for (const ref of relatedEntities) {
|
|
17955
|
+
const linked = await lookupMapping(db, organizationId, humanAgentId, delegateAgentId, ref);
|
|
17956
|
+
if (!linked) continue;
|
|
17957
|
+
const inserted = await insertMappingIfAbsent(db, {
|
|
17958
|
+
organizationId,
|
|
17959
|
+
humanAgentId,
|
|
17960
|
+
delegateAgentId,
|
|
17961
|
+
entity,
|
|
17962
|
+
chatId: linked.chatId,
|
|
17963
|
+
boundVia: "fixes_link"
|
|
17964
|
+
});
|
|
18152
17965
|
return {
|
|
18153
|
-
|
|
18154
|
-
|
|
17966
|
+
chatId: inserted.chatId,
|
|
17967
|
+
created: false,
|
|
17968
|
+
boundVia: inserted.boundVia
|
|
18155
17969
|
};
|
|
17970
|
+
}
|
|
17971
|
+
const chat = await createEntityChat(db, humanAgentId, delegateAgentId, entity, eventType, action);
|
|
17972
|
+
const inserted = await insertMappingIfAbsent(db, {
|
|
17973
|
+
organizationId,
|
|
17974
|
+
humanAgentId,
|
|
17975
|
+
delegateAgentId,
|
|
17976
|
+
entity,
|
|
17977
|
+
chatId: chat.id,
|
|
17978
|
+
boundVia: "direct"
|
|
18156
17979
|
});
|
|
18157
|
-
|
|
18158
|
-
|
|
18159
|
-
|
|
18160
|
-
|
|
18161
|
-
|
|
18162
|
-
|
|
18163
|
-
|
|
18164
|
-
|
|
18165
|
-
|
|
18166
|
-
|
|
18167
|
-
|
|
18168
|
-
|
|
18169
|
-
|
|
18170
|
-
|
|
18171
|
-
|
|
18172
|
-
|
|
18173
|
-
|
|
18174
|
-
|
|
17980
|
+
return {
|
|
17981
|
+
chatId: inserted.chatId,
|
|
17982
|
+
created: inserted.chatId === chat.id,
|
|
17983
|
+
boundVia: inserted.boundVia
|
|
17984
|
+
};
|
|
17985
|
+
}
|
|
17986
|
+
async function lookupMapping(db, organizationId, humanAgentId, delegateAgentId, entity) {
|
|
17987
|
+
const [row] = await db.select({
|
|
17988
|
+
chatId: githubEntityChatMappings.chatId,
|
|
17989
|
+
boundVia: githubEntityChatMappings.boundVia
|
|
17990
|
+
}).from(githubEntityChatMappings).where(and(eq(githubEntityChatMappings.organizationId, organizationId), eq(githubEntityChatMappings.humanAgentId, humanAgentId), eq(githubEntityChatMappings.delegateAgentId, delegateAgentId), eq(githubEntityChatMappings.entityType, entity.type), eq(githubEntityChatMappings.entityKey, entity.key))).limit(1);
|
|
17991
|
+
if (!row) return null;
|
|
17992
|
+
return {
|
|
17993
|
+
chatId: row.chatId,
|
|
17994
|
+
boundVia: row.boundVia === "fixes_link" ? "fixes_link" : "direct"
|
|
17995
|
+
};
|
|
17996
|
+
}
|
|
17997
|
+
async function insertMappingIfAbsent(db, params) {
|
|
17998
|
+
const [inserted] = await db.insert(githubEntityChatMappings).values({
|
|
17999
|
+
organizationId: params.organizationId,
|
|
18000
|
+
humanAgentId: params.humanAgentId,
|
|
18001
|
+
delegateAgentId: params.delegateAgentId,
|
|
18002
|
+
entityType: params.entity.type,
|
|
18003
|
+
entityKey: params.entity.key,
|
|
18004
|
+
chatId: params.chatId,
|
|
18005
|
+
boundVia: params.boundVia
|
|
18006
|
+
}).onConflictDoNothing({ target: [
|
|
18007
|
+
githubEntityChatMappings.organizationId,
|
|
18008
|
+
githubEntityChatMappings.humanAgentId,
|
|
18009
|
+
githubEntityChatMappings.delegateAgentId,
|
|
18010
|
+
githubEntityChatMappings.entityType,
|
|
18011
|
+
githubEntityChatMappings.entityKey
|
|
18012
|
+
] }).returning({
|
|
18013
|
+
chatId: githubEntityChatMappings.chatId,
|
|
18014
|
+
boundVia: githubEntityChatMappings.boundVia
|
|
18175
18015
|
});
|
|
18176
|
-
|
|
18177
|
-
|
|
18178
|
-
|
|
18016
|
+
if (inserted) return {
|
|
18017
|
+
chatId: inserted.chatId,
|
|
18018
|
+
boundVia: inserted.boundVia === "fixes_link" ? "fixes_link" : "direct"
|
|
18019
|
+
};
|
|
18020
|
+
const winner = await lookupMapping(db, params.organizationId, params.humanAgentId, params.delegateAgentId, params.entity);
|
|
18021
|
+
if (!winner) throw new Error("Unexpected: mapping insert conflicted but row not visible on re-read");
|
|
18022
|
+
return winner;
|
|
18023
|
+
}
|
|
18024
|
+
/**
|
|
18025
|
+
* Create a fresh chat for a (human, delegate, entity) tuple. Goes through the
|
|
18026
|
+
* canonical `createChat` so:
|
|
18027
|
+
* - cross-org participants are rejected (BadRequestError)
|
|
18028
|
+
* - direct agent-only chats automatically get `mode=mention_only`
|
|
18029
|
+
* - watcher rows are recomputed
|
|
18030
|
+
* - a future addParticipant call would upgrade the chat to `group` via
|
|
18031
|
+
* `maybeUpgradeDirectToGroup` instead of raw INSERT shortcuts
|
|
18032
|
+
*/
|
|
18033
|
+
async function createEntityChat(db, humanAgentId, delegateAgentId, entity, eventType, action) {
|
|
18034
|
+
const metadata = chatMetadataSchema$1.parse({
|
|
18035
|
+
source: "github",
|
|
18036
|
+
entityType: entity.type,
|
|
18037
|
+
entityKey: entity.key,
|
|
18038
|
+
...entity.url ? { entityUrl: entity.url } : {}
|
|
18179
18039
|
});
|
|
18040
|
+
return { id: (await createChat(db, humanAgentId, {
|
|
18041
|
+
type: "direct",
|
|
18042
|
+
participantIds: [delegateAgentId],
|
|
18043
|
+
topic: formatEntityTitle(entity, eventType, action),
|
|
18044
|
+
metadata
|
|
18045
|
+
})).id };
|
|
18180
18046
|
}
|
|
18181
18047
|
const log$1 = createLogger$1("GithubWebhook");
|
|
18182
|
-
const GITHUB_ADAPTER_ID = "github-adapter";
|
|
18183
18048
|
function verifySignature(secret, rawBody, signatureHeader) {
|
|
18184
18049
|
const expected = `sha256=${createHmac("sha256", secret).update(rawBody).digest("hex")}`;
|
|
18185
18050
|
const expectedBuf = Buffer.from(expected, "utf8");
|
|
18186
18051
|
const receivedBuf = Buffer.from(signatureHeader, "utf8");
|
|
18187
18052
|
if (expectedBuf.length !== receivedBuf.length || !timingSafeEqual(expectedBuf, receivedBuf)) throw new UnauthorizedError("Invalid webhook signature");
|
|
18188
18053
|
}
|
|
18189
|
-
async function ensureGitHubAdapterAgent(db, organizationId) {
|
|
18190
|
-
const [existing] = await db.select({ uuid: agents.uuid }).from(agents).where(and(eq(agents.organizationId, organizationId), eq(agents.name, GITHUB_ADAPTER_ID))).limit(1);
|
|
18191
|
-
if (existing) return existing.uuid;
|
|
18192
|
-
try {
|
|
18193
|
-
return (await createAgent(db, {
|
|
18194
|
-
name: GITHUB_ADAPTER_ID,
|
|
18195
|
-
type: "autonomous_agent",
|
|
18196
|
-
displayName: "GitHub Adapter",
|
|
18197
|
-
organizationId,
|
|
18198
|
-
metadata: {
|
|
18199
|
-
source: "github",
|
|
18200
|
-
managed: true
|
|
18201
|
-
}
|
|
18202
|
-
})).uuid;
|
|
18203
|
-
} catch (err) {
|
|
18204
|
-
if (err instanceof ConflictError) {
|
|
18205
|
-
const [created] = await db.select({ uuid: agents.uuid }).from(agents).where(and(eq(agents.organizationId, organizationId), eq(agents.name, GITHUB_ADAPTER_ID))).limit(1);
|
|
18206
|
-
if (created) return created.uuid;
|
|
18207
|
-
}
|
|
18208
|
-
throw err;
|
|
18209
|
-
}
|
|
18210
|
-
}
|
|
18211
|
-
async function findTargetAgent(db, organizationId, repoFullName) {
|
|
18212
|
-
const allAgents = await db.select({
|
|
18213
|
-
id: agents.uuid,
|
|
18214
|
-
name: agents.name,
|
|
18215
|
-
metadata: agents.metadata,
|
|
18216
|
-
type: agents.type
|
|
18217
|
-
}).from(agents).where(and(eq(agents.organizationId, organizationId), eq(agents.status, "active")));
|
|
18218
|
-
for (const agent of allAgents) {
|
|
18219
|
-
if (agent.name === GITHUB_ADAPTER_ID) continue;
|
|
18220
|
-
const meta = agent.metadata;
|
|
18221
|
-
if (meta && typeof meta === "object" && "github" in meta) {
|
|
18222
|
-
const github = meta.github;
|
|
18223
|
-
if (isRecord(github) && "repos" in github) {
|
|
18224
|
-
const repos = github.repos;
|
|
18225
|
-
if (Array.isArray(repos) && repos.includes(repoFullName)) return agent.id;
|
|
18226
|
-
}
|
|
18227
|
-
}
|
|
18228
|
-
}
|
|
18229
|
-
return null;
|
|
18230
|
-
}
|
|
18231
|
-
function isRecord(value) {
|
|
18232
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
18233
|
-
}
|
|
18234
18054
|
/** Extract unique @mentions from text. Returns lowercase usernames.
|
|
18235
18055
|
* Excludes email patterns (user@example.com) and team mentions (@org/team). */
|
|
18236
18056
|
function extractMentions$1(text) {
|
|
@@ -18270,10 +18090,18 @@ function evaluateDelegateTarget(target, sourceOrgId) {
|
|
|
18270
18090
|
}
|
|
18271
18091
|
/**
|
|
18272
18092
|
* Route @mentions to delegate agents.
|
|
18273
|
-
*
|
|
18274
|
-
*
|
|
18093
|
+
*
|
|
18094
|
+
* For each mentioned GitHub user who maps to an agent with `delegate_mention`
|
|
18095
|
+
* configured, resolve which chat the event belongs to (via §4.4's
|
|
18096
|
+
* entity-clustering rules) and post a card from the human-bound agent to its
|
|
18097
|
+
* delegate.
|
|
18098
|
+
*
|
|
18099
|
+
* The entity argument is the §4.2 entity for the current event; `relatedRefs`
|
|
18100
|
+
* is the parsed `Fixes #N` list (empty for non-PR events). Both are
|
|
18101
|
+
* pre-computed by the caller so the heavy parsing doesn't run once per
|
|
18102
|
+
* mention.
|
|
18275
18103
|
*/
|
|
18276
|
-
async function routeMentionDelegations(app, organizationId, mentionedNames, ctx) {
|
|
18104
|
+
async function routeMentionDelegations(app, organizationId, mentionedNames, ctx, entity, relatedRefs) {
|
|
18277
18105
|
if (mentionedNames.length === 0) return 0;
|
|
18278
18106
|
const delegates = await app.db.select({
|
|
18279
18107
|
id: agents.uuid,
|
|
@@ -18302,8 +18130,24 @@ async function routeMentionDelegations(app, organizationId, mentionedNames, ctx)
|
|
|
18302
18130
|
continue;
|
|
18303
18131
|
}
|
|
18304
18132
|
try {
|
|
18305
|
-
const
|
|
18306
|
-
|
|
18133
|
+
const resolved = await resolveTargetChat(app.db, {
|
|
18134
|
+
organizationId,
|
|
18135
|
+
humanAgentId: agent.id,
|
|
18136
|
+
delegateAgentId: agent.delegateMention,
|
|
18137
|
+
entity,
|
|
18138
|
+
relatedEntities: relatedRefs,
|
|
18139
|
+
eventType: ctx.event,
|
|
18140
|
+
action: ctx.action ?? ""
|
|
18141
|
+
});
|
|
18142
|
+
log$1.info({
|
|
18143
|
+
chatId: resolved.chatId,
|
|
18144
|
+
entityType: entity.type,
|
|
18145
|
+
entityKey: entity.key,
|
|
18146
|
+
boundVia: resolved.boundVia,
|
|
18147
|
+
created: resolved.created,
|
|
18148
|
+
humanAgent: agent.name
|
|
18149
|
+
}, "resolved entity chat");
|
|
18150
|
+
const { message: msg, recipients } = await sendMessage(app.db, resolved.chatId, agent.id, {
|
|
18307
18151
|
format: "card",
|
|
18308
18152
|
content: {
|
|
18309
18153
|
type: "github_mention",
|
|
@@ -18314,13 +18158,20 @@ async function routeMentionDelegations(app, organizationId, mentionedNames, ctx)
|
|
|
18314
18158
|
sender: ctx.sender,
|
|
18315
18159
|
title: ctx.title,
|
|
18316
18160
|
body: ctx.body,
|
|
18317
|
-
url: ctx.url
|
|
18161
|
+
url: ctx.url,
|
|
18162
|
+
entity: {
|
|
18163
|
+
type: entity.type,
|
|
18164
|
+
key: entity.key,
|
|
18165
|
+
url: entity.url ?? null
|
|
18166
|
+
}
|
|
18318
18167
|
},
|
|
18319
18168
|
metadata: {
|
|
18320
18169
|
source: "github",
|
|
18321
18170
|
event: "mention_delegation",
|
|
18322
18171
|
mentionedUser: agent.name,
|
|
18323
|
-
action: ctx.action
|
|
18172
|
+
action: ctx.action,
|
|
18173
|
+
entityType: entity.type,
|
|
18174
|
+
entityKey: entity.key
|
|
18324
18175
|
}
|
|
18325
18176
|
});
|
|
18326
18177
|
notifyRecipients(app.notifier, recipients, msg.id);
|
|
@@ -18335,43 +18186,6 @@ async function routeMentionDelegations(app, organizationId, mentionedNames, ctx)
|
|
|
18335
18186
|
}
|
|
18336
18187
|
return routed;
|
|
18337
18188
|
}
|
|
18338
|
-
function parseIssuesPayload(body) {
|
|
18339
|
-
if (!isRecord(body)) throw new BadRequestError("Invalid payload: expected object");
|
|
18340
|
-
if (typeof body.action !== "string") throw new BadRequestError("Invalid payload: missing action");
|
|
18341
|
-
if (!isRecord(body.issue)) throw new BadRequestError("Invalid payload: missing issue");
|
|
18342
|
-
if (!isRecord(body.repository)) throw new BadRequestError("Invalid payload: missing repository");
|
|
18343
|
-
if (!isRecord(body.sender)) throw new BadRequestError("Invalid payload: missing sender");
|
|
18344
|
-
const issue = body.issue;
|
|
18345
|
-
const labels = Array.isArray(issue.labels) ? issue.labels.filter((l) => isRecord(l) && typeof l.name === "string") : [];
|
|
18346
|
-
return {
|
|
18347
|
-
action: body.action,
|
|
18348
|
-
issue: {
|
|
18349
|
-
number: typeof issue.number === "number" ? issue.number : 0,
|
|
18350
|
-
title: typeof issue.title === "string" ? issue.title : "",
|
|
18351
|
-
body: typeof issue.body === "string" ? issue.body : null,
|
|
18352
|
-
html_url: typeof issue.html_url === "string" ? issue.html_url : "",
|
|
18353
|
-
labels,
|
|
18354
|
-
state: typeof issue.state === "string" ? issue.state : "open"
|
|
18355
|
-
},
|
|
18356
|
-
repository: { full_name: typeof body.repository.full_name === "string" ? body.repository.full_name : "" },
|
|
18357
|
-
sender: { login: typeof body.sender.login === "string" ? body.sender.login : "" }
|
|
18358
|
-
};
|
|
18359
|
-
}
|
|
18360
|
-
function parseIssueCommentPayload(body) {
|
|
18361
|
-
const base = parseIssuesPayload(body);
|
|
18362
|
-
if (!isRecord(body)) throw new BadRequestError("Invalid payload: expected object");
|
|
18363
|
-
if (!isRecord(body.comment)) throw new BadRequestError("Invalid payload: missing comment");
|
|
18364
|
-
const comment = body.comment;
|
|
18365
|
-
const commentUser = isRecord(comment.user) ? comment.user : { login: "" };
|
|
18366
|
-
return {
|
|
18367
|
-
...base,
|
|
18368
|
-
comment: {
|
|
18369
|
-
body: typeof comment.body === "string" ? comment.body : "",
|
|
18370
|
-
html_url: typeof comment.html_url === "string" ? comment.html_url : "",
|
|
18371
|
-
user: { login: typeof commentUser.login === "string" ? commentUser.login : "" }
|
|
18372
|
-
}
|
|
18373
|
-
};
|
|
18374
|
-
}
|
|
18375
18189
|
async function githubWebhookRoutes(app) {
|
|
18376
18190
|
app.addContentTypeParser("application/json", { parseAs: "buffer" }, (_request, body, done) => {
|
|
18377
18191
|
done(null, body);
|
|
@@ -18401,6 +18215,11 @@ async function githubWebhookRoutes(app) {
|
|
|
18401
18215
|
ok: true,
|
|
18402
18216
|
event: "ping"
|
|
18403
18217
|
});
|
|
18218
|
+
if (shouldSilent(eventType, payload)) return reply.status(200).send({
|
|
18219
|
+
ok: true,
|
|
18220
|
+
event: eventType,
|
|
18221
|
+
silent: true
|
|
18222
|
+
});
|
|
18404
18223
|
const deliveryHeader = request.headers["x-github-delivery"];
|
|
18405
18224
|
const deliveryId = typeof deliveryHeader === "string" && deliveryHeader.length > 0 ? deliveryHeader : null;
|
|
18406
18225
|
if (deliveryId) {
|
|
@@ -18417,16 +18236,17 @@ async function githubWebhookRoutes(app) {
|
|
|
18417
18236
|
}
|
|
18418
18237
|
}
|
|
18419
18238
|
try {
|
|
18420
|
-
if (eventType === "issues") return await handleIssuesEvent(app, orgId, eventType, payload, reply);
|
|
18421
|
-
if (eventType === "issue_comment") return await handleIssueCommentEvent(app, orgId, eventType, payload, reply);
|
|
18422
|
-
let mentionsRouted = 0;
|
|
18423
|
-
const allowedActions = MENTION_ACTIONS[eventType];
|
|
18424
18239
|
const action = isRecord(payload) && typeof payload.action === "string" ? payload.action : void 0;
|
|
18425
|
-
|
|
18240
|
+
const allowedActions = MENTION_ACTIONS[eventType];
|
|
18241
|
+
if (!allowedActions || !action || !allowedActions.includes(action)) return reply.status(200).send({
|
|
18242
|
+
ok: true,
|
|
18243
|
+
event: eventType,
|
|
18244
|
+
handled: false
|
|
18245
|
+
});
|
|
18246
|
+
const mentionsRouted = await handleMentionDelegation(app, orgId, eventType, payload);
|
|
18426
18247
|
return reply.status(200).send({
|
|
18427
18248
|
ok: true,
|
|
18428
18249
|
event: eventType,
|
|
18429
|
-
handled: mentionsRouted > 0,
|
|
18430
18250
|
mentionsRouted
|
|
18431
18251
|
});
|
|
18432
18252
|
} catch (err) {
|
|
@@ -18598,9 +18418,15 @@ async function handleMentionDelegation(app, organizationId, eventType, payload)
|
|
|
18598
18418
|
const textMentions = extractMentions$1(extractEventText(eventType, payload));
|
|
18599
18419
|
const structuralMentions = extractStructuralMentions(eventType, payload);
|
|
18600
18420
|
const mentions = [...new Set([...textMentions, ...structuralMentions])];
|
|
18601
|
-
|
|
18602
|
-
|
|
18603
|
-
return 0;
|
|
18421
|
+
if (mentions.length === 0) return 0;
|
|
18422
|
+
const ctx = extractEventContext(eventType, payload);
|
|
18423
|
+
if (!ctx) return 0;
|
|
18424
|
+
const entity = extractEventEntity(eventType, payload);
|
|
18425
|
+
if (!entity) {
|
|
18426
|
+
log$1.warn({ eventType }, "mention extracted but no entity resolvable; skipping fan-out");
|
|
18427
|
+
return 0;
|
|
18428
|
+
}
|
|
18429
|
+
return routeMentionDelegations(app, organizationId, mentions, ctx, entity, eventType === "pull_request" && ctx.repository.length > 0 ? parseFixesRefs(ctx.body, ctx.repository) : []);
|
|
18604
18430
|
}
|
|
18605
18431
|
/** Actions that represent new/changed content (worth scanning for @mentions).
|
|
18606
18432
|
* Note: `pull_request.review_requested` doesn't carry an @mention in any
|
|
@@ -18621,124 +18447,6 @@ const MENTION_ACTIONS = {
|
|
|
18621
18447
|
discussion_comment: ["created"],
|
|
18622
18448
|
commit_comment: ["created"]
|
|
18623
18449
|
};
|
|
18624
|
-
async function handleIssuesEvent(app, organizationId, eventType, payload, reply) {
|
|
18625
|
-
const data = parseIssuesPayload(payload);
|
|
18626
|
-
if (MENTION_ACTIONS.issues?.includes(data.action)) await handleMentionDelegation(app, organizationId, eventType, payload);
|
|
18627
|
-
if (![
|
|
18628
|
-
"opened",
|
|
18629
|
-
"edited",
|
|
18630
|
-
"labeled"
|
|
18631
|
-
].includes(data.action)) return reply.status(200).send({
|
|
18632
|
-
ok: true,
|
|
18633
|
-
event: "issues",
|
|
18634
|
-
action: data.action,
|
|
18635
|
-
handled: false
|
|
18636
|
-
});
|
|
18637
|
-
const [senderId, targetAgentId] = await Promise.all([ensureGitHubAdapterAgent(app.db, organizationId), findTargetAgent(app.db, organizationId, data.repository.full_name)]);
|
|
18638
|
-
if (!targetAgentId) {
|
|
18639
|
-
log$1.warn({
|
|
18640
|
-
repo: data.repository.full_name,
|
|
18641
|
-
event: "issue"
|
|
18642
|
-
}, "no target agent found for GitHub event");
|
|
18643
|
-
return reply.status(200).send({
|
|
18644
|
-
ok: true,
|
|
18645
|
-
event: "issues",
|
|
18646
|
-
action: data.action,
|
|
18647
|
-
routed: false
|
|
18648
|
-
});
|
|
18649
|
-
}
|
|
18650
|
-
const content = {
|
|
18651
|
-
type: "github_issue",
|
|
18652
|
-
action: data.action,
|
|
18653
|
-
issue: {
|
|
18654
|
-
number: data.issue.number,
|
|
18655
|
-
title: data.issue.title,
|
|
18656
|
-
body: data.issue.body,
|
|
18657
|
-
url: data.issue.html_url,
|
|
18658
|
-
labels: data.issue.labels.map((l) => l.name),
|
|
18659
|
-
state: data.issue.state
|
|
18660
|
-
},
|
|
18661
|
-
repository: data.repository.full_name,
|
|
18662
|
-
sender: data.sender.login
|
|
18663
|
-
};
|
|
18664
|
-
const metadata = {
|
|
18665
|
-
source: "github",
|
|
18666
|
-
event: "issues",
|
|
18667
|
-
action: data.action
|
|
18668
|
-
};
|
|
18669
|
-
const chat = await findOrCreateDirectChat(app.db, senderId, targetAgentId);
|
|
18670
|
-
const { message: msg, recipients } = await sendMessage(app.db, chat.id, senderId, {
|
|
18671
|
-
format: "card",
|
|
18672
|
-
content,
|
|
18673
|
-
metadata
|
|
18674
|
-
});
|
|
18675
|
-
notifyRecipients(app.notifier, recipients, msg.id);
|
|
18676
|
-
return reply.status(200).send({
|
|
18677
|
-
ok: true,
|
|
18678
|
-
event: "issues",
|
|
18679
|
-
action: data.action,
|
|
18680
|
-
routed: true
|
|
18681
|
-
});
|
|
18682
|
-
}
|
|
18683
|
-
async function handleIssueCommentEvent(app, organizationId, eventType, payload, reply) {
|
|
18684
|
-
const data = parseIssueCommentPayload(payload);
|
|
18685
|
-
if (MENTION_ACTIONS.issue_comment?.includes(data.action)) await handleMentionDelegation(app, organizationId, eventType, payload);
|
|
18686
|
-
if (data.action !== "created") return reply.status(200).send({
|
|
18687
|
-
ok: true,
|
|
18688
|
-
event: "issue_comment",
|
|
18689
|
-
action: data.action,
|
|
18690
|
-
handled: false
|
|
18691
|
-
});
|
|
18692
|
-
const [senderId, targetAgentId] = await Promise.all([ensureGitHubAdapterAgent(app.db, organizationId), findTargetAgent(app.db, organizationId, data.repository.full_name)]);
|
|
18693
|
-
if (!targetAgentId) {
|
|
18694
|
-
log$1.warn({
|
|
18695
|
-
repo: data.repository.full_name,
|
|
18696
|
-
event: "issue_comment"
|
|
18697
|
-
}, "no target agent found for GitHub event");
|
|
18698
|
-
return reply.status(200).send({
|
|
18699
|
-
ok: true,
|
|
18700
|
-
event: "issue_comment",
|
|
18701
|
-
action: data.action,
|
|
18702
|
-
routed: false
|
|
18703
|
-
});
|
|
18704
|
-
}
|
|
18705
|
-
const content = {
|
|
18706
|
-
type: "github_issue_comment",
|
|
18707
|
-
action: data.action,
|
|
18708
|
-
issue: {
|
|
18709
|
-
number: data.issue.number,
|
|
18710
|
-
title: data.issue.title,
|
|
18711
|
-
url: data.issue.html_url,
|
|
18712
|
-
labels: data.issue.labels.map((l) => l.name),
|
|
18713
|
-
state: data.issue.state
|
|
18714
|
-
},
|
|
18715
|
-
comment: {
|
|
18716
|
-
body: data.comment.body,
|
|
18717
|
-
url: data.comment.html_url,
|
|
18718
|
-
author: data.comment.user.login
|
|
18719
|
-
},
|
|
18720
|
-
repository: data.repository.full_name,
|
|
18721
|
-
sender: data.sender.login
|
|
18722
|
-
};
|
|
18723
|
-
const metadata = {
|
|
18724
|
-
source: "github",
|
|
18725
|
-
event: "issue_comment",
|
|
18726
|
-
action: data.action
|
|
18727
|
-
};
|
|
18728
|
-
const chat = await findOrCreateDirectChat(app.db, senderId, targetAgentId);
|
|
18729
|
-
const { message: msg, recipients } = await sendMessage(app.db, chat.id, senderId, {
|
|
18730
|
-
format: "card",
|
|
18731
|
-
content,
|
|
18732
|
-
metadata
|
|
18733
|
-
});
|
|
18734
|
-
notifyRecipients(app.notifier, recipients, msg.id);
|
|
18735
|
-
return reply.status(200).send({
|
|
18736
|
-
ok: true,
|
|
18737
|
-
event: "issue_comment",
|
|
18738
|
-
action: data.action,
|
|
18739
|
-
routed: true
|
|
18740
|
-
});
|
|
18741
|
-
}
|
|
18742
18450
|
var schema_exports = /* @__PURE__ */ __exportAll({
|
|
18743
18451
|
adapterAgentMappings: () => adapterAgentMappings,
|
|
18744
18452
|
adapterChatMappings: () => adapterChatMappings,
|
|
@@ -18753,6 +18461,7 @@ var schema_exports = /* @__PURE__ */ __exportAll({
|
|
|
18753
18461
|
chatSubscriptions: () => chatSubscriptions,
|
|
18754
18462
|
chats: () => chats,
|
|
18755
18463
|
clients: () => clients,
|
|
18464
|
+
githubEntityChatMappings: () => githubEntityChatMappings,
|
|
18756
18465
|
inboxEntries: () => inboxEntries,
|
|
18757
18466
|
invitationRedemptions: () => invitationRedemptions,
|
|
18758
18467
|
invitations: () => invitations,
|
|
@@ -18764,8 +18473,6 @@ var schema_exports = /* @__PURE__ */ __exportAll({
|
|
|
18764
18473
|
pendingQuestions: () => pendingQuestions,
|
|
18765
18474
|
serverInstances: () => serverInstances,
|
|
18766
18475
|
sessionEvents: () => sessionEvents,
|
|
18767
|
-
taskChats: () => taskChats,
|
|
18768
|
-
tasks: () => tasks,
|
|
18769
18476
|
users: () => users
|
|
18770
18477
|
});
|
|
18771
18478
|
function connectDatabase(url) {
|
|
@@ -20258,7 +19965,6 @@ async function buildApp(config) {
|
|
|
20258
19965
|
await scope.register(orgAdapterStatusRoutes, { prefix: "/adapters/status" });
|
|
20259
19966
|
await scope.register(orgOverviewRoutes, { prefix: "/overview" });
|
|
20260
19967
|
await scope.register(orgActivityRoutes, { prefix: "/activity" });
|
|
20261
|
-
await scope.register(orgTaskRoutes, { prefix: "/tasks" });
|
|
20262
19968
|
await scope.register(orgSessionRoutes, { prefix: "/sessions" });
|
|
20263
19969
|
await scope.register(orgNotificationRoutes, { prefix: "/notifications" });
|
|
20264
19970
|
await scope.register(orgClientRoutes, { prefix: "/clients" });
|
|
@@ -20274,7 +19980,6 @@ async function buildApp(config) {
|
|
|
20274
19980
|
await scope.register(agentActivityRoutes, { prefix: "/agents" });
|
|
20275
19981
|
await scope.register(sessionRoutes, { prefix: "/agents" });
|
|
20276
19982
|
await scope.register(chatRoutes, { prefix: "/chats" });
|
|
20277
|
-
await scope.register(taskRoutes, { prefix: "/tasks" });
|
|
20278
19983
|
await scope.register(adapterRoutes, { prefix: "/adapters" });
|
|
20279
19984
|
await scope.register(adapterMappingRoutes, { prefix: "/adapter-mappings" });
|
|
20280
19985
|
await scope.register(clientRoutes, { prefix: "/clients" });
|
|
@@ -20286,7 +19991,6 @@ async function buildApp(config) {
|
|
|
20286
19991
|
await scope.register(agentSendToAgentRoutes, { prefix: "/agents" });
|
|
20287
19992
|
await scope.register(agentInboxRoutes, { prefix: "/inbox" });
|
|
20288
19993
|
await scope.register(agentConfigRoutes$1);
|
|
20289
|
-
await scope.register(agentTaskRoutes, { prefix: "/tasks" });
|
|
20290
19994
|
await scope.register(agentFeishuBotRoutes);
|
|
20291
19995
|
await scope.register(agentFeishuUserRoutes, { prefix: "/delegated" });
|
|
20292
19996
|
}), { prefix: "/agent" });
|
|
@@ -20897,4 +20601,4 @@ function registerSaaSConnectCommand(program) {
|
|
|
20897
20601
|
});
|
|
20898
20602
|
}
|
|
20899
20603
|
//#endregion
|
|
20900
|
-
export { formatStaleReason as $, checkDocker as A, isServiceSupported as B, createApiNameResolver as C, checkBackgroundService as D, checkAgentConfigs as E, checkWebSocket as F, uninstallClientService as G, restartClientService as H, printResults as I, stopPostgres as J, ensurePostgres as K, reconcileAgentConfigs as L, checkServerConfig as M, checkServerHealth as N, checkClientConfig as O, checkServerReachable as P, findStaleAliases as Q, getClientServiceStatus as R, runHomeMigration as S, runMigrations as T, startClientService as U, resolveCliInvocation as V, stopClientService as W, handleClientOrgMismatch as X, ClientRuntime as Y, rotateClientIdWithBackup as Z, formatCheckReport as _, declineUpdate as a,
|
|
20604
|
+
export { formatStaleReason as $, checkDocker as A, isServiceSupported as B, createApiNameResolver as C, checkBackgroundService as D, checkAgentConfigs as E, checkWebSocket as F, uninstallClientService as G, restartClientService as H, printResults as I, stopPostgres as J, ensurePostgres as K, reconcileAgentConfigs as L, checkServerConfig as M, checkServerHealth as N, checkClientConfig as O, checkServerReachable as P, findStaleAliases as Q, getClientServiceStatus as R, runHomeMigration as S, runMigrations as T, startClientService as U, resolveCliInvocation as V, stopClientService as W, handleClientOrgMismatch as X, ClientRuntime as Y, rotateClientIdWithBackup as Z, formatCheckReport as _, declineUpdate as a, fail as at, onboardCreate as b, detectInstallMode as c, ClientUserMismatchError as ct, startServer as d, SessionRegistry as dt, removeLocalAgent as et, reconcileLocalRuntimeProviders as f, cleanWorkspaces as ft, promptMissingFields as g, promptAddAgent as h, configureClientLoggerForService as ht, createExecuteUpdate as i, resolveSenderName as it, checkNodeVersion as j, checkDatabase as k, fetchLatestVersion as l, FirstTreeHubSDK as lt, isInteractive as m, applyClientLoggerConfig as mt, deriveHubUrlFromToken as n, hasUser as nt, promptUpdate as o, success as ot, uploadClientCapabilities as p, probeCapabilities as pt, isDockerAvailable as q, registerSaaSConnectCommand as r, resolveReplyToFromEnv as rt, PACKAGE_NAME as s, ClientOrgMismatchError as st, HubUrlDerivationError as t, createOwner as tt, installGlobalLatest as u, SdkError as ut, loadOnboardState as v, migrateLocalAgentDirs as w, saveOnboardState as x, onboardCheck as y, installClientService as z };
|