@agent-team-foundation/first-tree-hub 0.12.2 → 0.12.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.mjs +4 -4
- package/dist/{client-WubcgX-W-B2bOvgJ1.mjs → client-0RrgrMjR-CylTJGEb.mjs} +2 -2
- package/dist/{client-DHCSQ8kg-DjlSmE9q.mjs → client-D1TDiik_-NV_lkhfI.mjs} +13 -11
- package/dist/{dist-DHHd2dar.mjs → dist-CMhywpXB.mjs} +2 -141
- package/dist/drizzle/0035_drop_hub_tasks.sql +7 -0
- package/dist/drizzle/meta/_journal.json +7 -0
- package/dist/{feishu-fLnwqCOs.mjs → feishu-tkZS0vvL.mjs} +1 -1
- package/dist/index.mjs +4 -4
- package/dist/{invitation-C299fxkP-B89eqDos.mjs → invitation-C299fxkP-CZRV665C.mjs} +1 -1
- package/dist/{saas-connect-_2M4kfPR.mjs → saas-connect-S71rG182.mjs} +285 -744
- package/dist/web/assets/index-BG9RRx2e.js +401 -0
- package/dist/web/assets/index-CbOOQaWp.css +1 -0
- package/dist/web/assets/{index-D5RJDuFw.js → index-RNegidl2.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-C1DBMrHD.js +0 -391
- package/dist/web/assets/index-CwC0zzF5.css +0 -1
|
@@ -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 runtimeStateMessageSchema, B as isReservedAgentName$1, C as createChatSchema, D as defaultRuntimeConfigPayload, E as createOrgFromMeSchema, F as inboxAckFrameSchema, G as notificationQuerySchema, H as listMeChatsQuerySchema, I as inboxDeliverFrameSchema$1, J as patchOnboardingSchema, K as onboardingEventSchema, L as inboxPollQuerySchema, M as githubDevCallbackQuerySchema, N as githubStartQuerySchema, O as delegateFeishuUserSchema, P as imageInlineContentSchema, Q as refreshTokenSchema, R as isOrgSettingNamespace, S as createAgentSchema, T as createMemberSchema, U as loginSchema, V as joinByInvitationSchema, W as messageSourceSchema$1, Z as rebindAgentSchema, _ as clientRegisterSchema, _t as updateMemberSchema, a as AGENT_VISIBILITY, at as sessionCompletionMessageSchema, b as createAdapterConfigSchema, c as ORG_SETTINGS_NAMESPACES$1, ct as sessionReconcileRequestSchema, d as addParticipantSchema, dt as submitQuestionAnswerSchema, et as safeRedirectPath, f as agentBindRequestSchema, ft as updateAdapterConfigSchema, gt as updateClientCapabilitiesSchema, h as agentTypeSchema$1, ht as updateChatSchema, i as AGENT_STATUSES, it as sendToAgentSchema, j as githubCallbackQuerySchema, k as dryRunAgentRuntimeConfigSchema, l as WS_AUTH_FRAME_TIMEOUT_MS, lt as sessionStateMessageSchema, m as agentRuntimeConfigPayloadSchema$1, mt as updateAgentSchema, n as AGENT_NAME_REGEX$1, nt as selfServiceFeishuBotSchema, ot as sessionEventMessageSchema, p as agentPinnedMessageSchema$1, pt as updateAgentRuntimeConfigSchema, q as paginationQuerySchema, r as AGENT_SELECTOR_HEADER$1, rt as sendMessageSchema, s as MENTION_REGEX, st as sessionEventSchema$1, t as AGENT_BIND_REJECT_REASONS, u as addMeChatParticipantsSchema, ut as stripCode, v as connectTokenExchangeSchema, vt as updateOrganizationSchema, w as createMeChatSchema, x as createAdapterMappingSchema, y as contextTreeSnapshotSchema, yt as wsAuthFrameSchema, z as isRedactedEnvValue } from "./dist-CMhywpXB.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-D1TDiik_-NV_lkhfI.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";
|
|
@@ -1015,7 +1015,6 @@ const messageFormatSchema = z.enum([
|
|
|
1015
1015
|
"card",
|
|
1016
1016
|
"reference",
|
|
1017
1017
|
"file",
|
|
1018
|
-
"task",
|
|
1019
1018
|
"question",
|
|
1020
1019
|
"question_answer"
|
|
1021
1020
|
]);
|
|
@@ -1199,9 +1198,7 @@ const meChatRowSchema = z.object({
|
|
|
1199
1198
|
lastMessageAt: z.string().nullable(),
|
|
1200
1199
|
lastMessagePreview: z.string().nullable(),
|
|
1201
1200
|
unreadMentionCount: z.number().int(),
|
|
1202
|
-
canReply: z.boolean()
|
|
1203
|
-
taskId: z.string().nullable(),
|
|
1204
|
-
taskStatus: z.string().nullable()
|
|
1201
|
+
canReply: z.boolean()
|
|
1205
1202
|
});
|
|
1206
1203
|
z.object({
|
|
1207
1204
|
rows: z.array(meChatRowSchema),
|
|
@@ -1677,94 +1674,6 @@ z.object({
|
|
|
1677
1674
|
totalMessages: z.number(),
|
|
1678
1675
|
byOrganization: z.array(orgStatsSchema)
|
|
1679
1676
|
});
|
|
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
1677
|
const userStatusSchema = z.enum(["active", "suspended"]);
|
|
1769
1678
|
z.object({
|
|
1770
1679
|
id: z.string(),
|
|
@@ -2876,7 +2785,126 @@ function getHandlerFactory(type) {
|
|
|
2876
2785
|
}
|
|
2877
2786
|
return factory;
|
|
2878
2787
|
}
|
|
2879
|
-
join(DEFAULT_DATA_DIR, "context-tree");
|
|
2788
|
+
const CONTEXT_TREE_DIR = join(DEFAULT_DATA_DIR, "context-tree");
|
|
2789
|
+
/**
|
|
2790
|
+
* Sync the shared Context Tree git clone.
|
|
2791
|
+
*
|
|
2792
|
+
* Clones on first run, pulls on subsequent runs.
|
|
2793
|
+
* Returns the binding on success, null on failure (graceful degradation).
|
|
2794
|
+
*/
|
|
2795
|
+
async function syncContextTree(serverUrl, getAccessToken, log, userAgent) {
|
|
2796
|
+
try {
|
|
2797
|
+
execFileSync("git", ["--version"], { stdio: "ignore" });
|
|
2798
|
+
} catch {
|
|
2799
|
+
log("Context Tree sync skipped: git is not installed");
|
|
2800
|
+
return null;
|
|
2801
|
+
}
|
|
2802
|
+
let repo;
|
|
2803
|
+
let branch;
|
|
2804
|
+
try {
|
|
2805
|
+
const config = await new FirstTreeHubSDK({
|
|
2806
|
+
serverUrl,
|
|
2807
|
+
getAccessToken,
|
|
2808
|
+
userAgent
|
|
2809
|
+
}).getContextTreeConfig();
|
|
2810
|
+
if (!config.repo) {
|
|
2811
|
+
log("Context Tree sync skipped: not configured on server");
|
|
2812
|
+
return null;
|
|
2813
|
+
}
|
|
2814
|
+
repo = config.repo;
|
|
2815
|
+
branch = config.branch ?? "main";
|
|
2816
|
+
} catch (err) {
|
|
2817
|
+
log(`Context Tree sync skipped: failed to fetch config from server (${err instanceof Error ? err.message : String(err)})`);
|
|
2818
|
+
return null;
|
|
2819
|
+
}
|
|
2820
|
+
try {
|
|
2821
|
+
if (existsSync(join(CONTEXT_TREE_DIR, ".git"))) {
|
|
2822
|
+
if (execFileSync("git", [
|
|
2823
|
+
"rev-parse",
|
|
2824
|
+
"--abbrev-ref",
|
|
2825
|
+
"HEAD"
|
|
2826
|
+
], {
|
|
2827
|
+
cwd: CONTEXT_TREE_DIR,
|
|
2828
|
+
encoding: "utf-8",
|
|
2829
|
+
timeout: 5e3
|
|
2830
|
+
}).trim() !== branch) {
|
|
2831
|
+
execFileSync("git", ["checkout", branch], {
|
|
2832
|
+
cwd: CONTEXT_TREE_DIR,
|
|
2833
|
+
stdio: "pipe",
|
|
2834
|
+
timeout: 1e4
|
|
2835
|
+
});
|
|
2836
|
+
log(`Context Tree switched to branch ${branch}`);
|
|
2837
|
+
}
|
|
2838
|
+
execFileSync("git", ["pull", "--ff-only"], {
|
|
2839
|
+
cwd: CONTEXT_TREE_DIR,
|
|
2840
|
+
stdio: "pipe",
|
|
2841
|
+
timeout: 3e4
|
|
2842
|
+
});
|
|
2843
|
+
log(`Context Tree updated (pull)`);
|
|
2844
|
+
} else {
|
|
2845
|
+
mkdirSync(CONTEXT_TREE_DIR, { recursive: true });
|
|
2846
|
+
execFileSync("git", [
|
|
2847
|
+
"clone",
|
|
2848
|
+
"--branch",
|
|
2849
|
+
branch,
|
|
2850
|
+
"--single-branch",
|
|
2851
|
+
repo,
|
|
2852
|
+
CONTEXT_TREE_DIR
|
|
2853
|
+
], {
|
|
2854
|
+
stdio: "pipe",
|
|
2855
|
+
timeout: 6e4
|
|
2856
|
+
});
|
|
2857
|
+
log(`Context Tree cloned from ${repo} (branch: ${branch})`);
|
|
2858
|
+
}
|
|
2859
|
+
return {
|
|
2860
|
+
path: CONTEXT_TREE_DIR,
|
|
2861
|
+
repoUrl: repo,
|
|
2862
|
+
branch
|
|
2863
|
+
};
|
|
2864
|
+
} catch (err) {
|
|
2865
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2866
|
+
log(`Context Tree sync failed: ${msg}`);
|
|
2867
|
+
log("Check that git credentials (SSH key or credential helper) are configured for this repo");
|
|
2868
|
+
if ((msg.includes("cannot fast-forward") || msg.includes("not possible to fast-forward") || msg.includes("CONFLICT")) && existsSync(join(CONTEXT_TREE_DIR, ".git"))) {
|
|
2869
|
+
log("Diverged history detected, attempting fresh clone...");
|
|
2870
|
+
try {
|
|
2871
|
+
rmSync(CONTEXT_TREE_DIR, {
|
|
2872
|
+
recursive: true,
|
|
2873
|
+
force: true
|
|
2874
|
+
});
|
|
2875
|
+
mkdirSync(CONTEXT_TREE_DIR, { recursive: true });
|
|
2876
|
+
execFileSync("git", [
|
|
2877
|
+
"clone",
|
|
2878
|
+
"--branch",
|
|
2879
|
+
branch,
|
|
2880
|
+
"--single-branch",
|
|
2881
|
+
repo,
|
|
2882
|
+
CONTEXT_TREE_DIR
|
|
2883
|
+
], {
|
|
2884
|
+
stdio: "pipe",
|
|
2885
|
+
timeout: 6e4
|
|
2886
|
+
});
|
|
2887
|
+
log("Context Tree re-cloned successfully");
|
|
2888
|
+
return {
|
|
2889
|
+
path: CONTEXT_TREE_DIR,
|
|
2890
|
+
repoUrl: repo,
|
|
2891
|
+
branch
|
|
2892
|
+
};
|
|
2893
|
+
} catch {
|
|
2894
|
+
log("Context Tree re-clone also failed, continuing without context");
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
if (existsSync(join(CONTEXT_TREE_DIR, ".git"))) {
|
|
2898
|
+
log("Using existing Context Tree clone despite sync failure");
|
|
2899
|
+
return {
|
|
2900
|
+
path: CONTEXT_TREE_DIR,
|
|
2901
|
+
repoUrl: repo,
|
|
2902
|
+
branch
|
|
2903
|
+
};
|
|
2904
|
+
}
|
|
2905
|
+
return null;
|
|
2906
|
+
}
|
|
2907
|
+
}
|
|
2880
2908
|
/**
|
|
2881
2909
|
* Marker file written into every workspace so the Codex CLI's project-root
|
|
2882
2910
|
* detection (configured via `project_root_markers: ["first-tree-workspace"]`)
|
|
@@ -2948,8 +2976,6 @@ function installFirstTreeIntegration(options) {
|
|
|
2948
2976
|
const integrateArgs = [
|
|
2949
2977
|
"tree",
|
|
2950
2978
|
"integrate",
|
|
2951
|
-
"--source-path",
|
|
2952
|
-
workspacePath,
|
|
2953
2979
|
"--tree-path",
|
|
2954
2980
|
contextTreePath,
|
|
2955
2981
|
"--mode",
|
|
@@ -2984,8 +3010,13 @@ function installFirstTreeIntegration(options) {
|
|
|
2984
3010
|
} catch (err) {
|
|
2985
3011
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2986
3012
|
const binaryMissing = /ENOENT|not found|command not found/i.test(msg);
|
|
3013
|
+
const unsupportedByThisCli = /unknown (?:option|command|argument)|unrecognized option/i.test(msg);
|
|
3014
|
+
const shouldRetry = binaryMissing || unsupportedByThisCli;
|
|
2987
3015
|
const isLastAttempt = index === attempts.length - 1;
|
|
2988
|
-
if (
|
|
3016
|
+
if (shouldRetry && !isLastAttempt) {
|
|
3017
|
+
log(`First-tree integration via ${attempt.label} unusable; falling back: ${msg.slice(0, 200)}`);
|
|
3018
|
+
continue;
|
|
3019
|
+
}
|
|
2989
3020
|
log(`First-tree integration skipped (${attempt.label}): ${msg.slice(0, 200)}`);
|
|
2990
3021
|
return false;
|
|
2991
3022
|
}
|
|
@@ -4608,6 +4639,8 @@ const createClaudeCodeHandler = (config) => {
|
|
|
4608
4639
|
}
|
|
4609
4640
|
}
|
|
4610
4641
|
const contextTreePath = config.contextTreePath ?? null;
|
|
4642
|
+
const contextTreeRepoUrl = config.contextTreeRepoUrl ?? null;
|
|
4643
|
+
const agentName = config.agentName ?? null;
|
|
4611
4644
|
/**
|
|
4612
4645
|
* Materialise the runtime config's `gitRepos` into worktrees under `cwd`.
|
|
4613
4646
|
* Idempotent across resumes: reuses an existing Hub-managed worktree if
|
|
@@ -4675,7 +4708,8 @@ const createClaudeCodeHandler = (config) => {
|
|
|
4675
4708
|
if (contextTreePath) installFirstTreeIntegration({
|
|
4676
4709
|
workspacePath: workspace,
|
|
4677
4710
|
contextTreePath,
|
|
4678
|
-
workspaceId: sessionCtx.chatId,
|
|
4711
|
+
workspaceId: agentName ?? sessionCtx.chatId,
|
|
4712
|
+
treeRepoUrl: contextTreeRepoUrl ?? void 0,
|
|
4679
4713
|
log: (msg) => sessionCtx.log(msg)
|
|
4680
4714
|
});
|
|
4681
4715
|
}
|
|
@@ -4864,6 +4898,8 @@ const createCodexHandler = (config) => {
|
|
|
4864
4898
|
const agentConfigCache = config.agentConfigCache ?? null;
|
|
4865
4899
|
const gitMirrorManager = config.gitMirrorManager ?? null;
|
|
4866
4900
|
const contextTreePath = config.contextTreePath ?? null;
|
|
4901
|
+
const contextTreeRepoUrl = config.contextTreeRepoUrl ?? null;
|
|
4902
|
+
const agentName = config.agentName ?? null;
|
|
4867
4903
|
let cwd = null;
|
|
4868
4904
|
let codex = null;
|
|
4869
4905
|
let thread = null;
|
|
@@ -5135,6 +5171,17 @@ const createCodexHandler = (config) => {
|
|
|
5135
5171
|
if (inputs.length === 0) return;
|
|
5136
5172
|
await runTurn(inputs.join("\n\n"), sessionCtx);
|
|
5137
5173
|
}
|
|
5174
|
+
/** Install the first-tree skill + binding block; no-op when context tree is unconfigured. */
|
|
5175
|
+
function ensureFirstTreeBinding(workspace, sessionCtx) {
|
|
5176
|
+
if (!contextTreePath) return;
|
|
5177
|
+
installFirstTreeIntegration({
|
|
5178
|
+
workspacePath: workspace,
|
|
5179
|
+
contextTreePath,
|
|
5180
|
+
workspaceId: agentName ?? sessionCtx.chatId,
|
|
5181
|
+
treeRepoUrl: contextTreeRepoUrl ?? void 0,
|
|
5182
|
+
log: (msg) => sessionCtx.log(msg)
|
|
5183
|
+
});
|
|
5184
|
+
}
|
|
5138
5185
|
return {
|
|
5139
5186
|
async start(message, sessionCtx) {
|
|
5140
5187
|
ctx = sessionCtx;
|
|
@@ -5160,6 +5207,7 @@ const createCodexHandler = (config) => {
|
|
|
5160
5207
|
content: buildAgentBriefing(payload)
|
|
5161
5208
|
}
|
|
5162
5209
|
});
|
|
5210
|
+
ensureFirstTreeBinding(cwd, sessionCtx);
|
|
5163
5211
|
await prepareGitWorktrees(payload, cwd, sessionCtx.chatId);
|
|
5164
5212
|
codex = new Codex({
|
|
5165
5213
|
env: buildEnv(sessionCtx),
|
|
@@ -5184,17 +5232,20 @@ const createCodexHandler = (config) => {
|
|
|
5184
5232
|
env: [],
|
|
5185
5233
|
gitRepos: []
|
|
5186
5234
|
};
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5235
|
+
if (!existsSync(join(cwd, ".agent", "identity.json"))) {
|
|
5236
|
+
bootstrapWorkspace({
|
|
5237
|
+
workspacePath: cwd,
|
|
5238
|
+
identity: sessionCtx.agent,
|
|
5239
|
+
contextTreePath,
|
|
5240
|
+
serverUrl: sessionCtx.sdk.serverUrl,
|
|
5241
|
+
chatId: sessionCtx.chatId,
|
|
5242
|
+
briefing: {
|
|
5243
|
+
format: "agents-md",
|
|
5244
|
+
content: buildAgentBriefing(payload)
|
|
5245
|
+
}
|
|
5246
|
+
});
|
|
5247
|
+
ensureFirstTreeBinding(cwd, sessionCtx);
|
|
5248
|
+
}
|
|
5198
5249
|
await prepareGitWorktrees(payload, cwd, sessionCtx.chatId);
|
|
5199
5250
|
codex = new Codex({
|
|
5200
5251
|
env: buildEnv(sessionCtx),
|
|
@@ -6227,7 +6278,7 @@ var AgentSlot = class {
|
|
|
6227
6278
|
lastActivityMs: 0
|
|
6228
6279
|
};
|
|
6229
6280
|
}
|
|
6230
|
-
async start(
|
|
6281
|
+
async start(contextTreeBinding) {
|
|
6231
6282
|
const sdk = (await this.clientConnection.bindAgent(this.config.agentId, this.config.runtimeType ?? this.config.type, this.config.runtimeVersion)).sdk;
|
|
6232
6283
|
this.sdk = sdk;
|
|
6233
6284
|
const agent = await sdk.register();
|
|
@@ -6305,7 +6356,9 @@ var AgentSlot = class {
|
|
|
6305
6356
|
handlerFactory: this.config.handlerFactory,
|
|
6306
6357
|
handlerConfig: {
|
|
6307
6358
|
workspaceRoot: join(DEFAULT_DATA_DIR, "workspaces", this.config.name),
|
|
6308
|
-
|
|
6359
|
+
agentName: this.config.name,
|
|
6360
|
+
contextTreePath: contextTreeBinding?.path,
|
|
6361
|
+
contextTreeRepoUrl: contextTreeBinding?.repoUrl,
|
|
6309
6362
|
gitMirrorManager
|
|
6310
6363
|
},
|
|
6311
6364
|
agentIdentity: {
|
|
@@ -7188,6 +7241,14 @@ var ClientRuntime = class {
|
|
|
7188
7241
|
watcher = null;
|
|
7189
7242
|
debounceTimer = null;
|
|
7190
7243
|
/**
|
|
7244
|
+
* Per-org Context Tree binding resolved at `start()`. Threaded through every
|
|
7245
|
+
* `slot.start()` so handlers can copy `AGENT.md` / root `NODE.md` into the
|
|
7246
|
+
* agent workspace's `.agent/context/` and install the first-tree skill.
|
|
7247
|
+
* `null` when the user has no primary org, the org has no tree configured,
|
|
7248
|
+
* or git sync failed — handlers degrade gracefully (empty context dir).
|
|
7249
|
+
*/
|
|
7250
|
+
contextTreeBinding = null;
|
|
7251
|
+
/**
|
|
7191
7252
|
* Directory we write auto-registered agent configs into (same path that
|
|
7192
7253
|
* `first-tree-hub agent add` uses). Set by `watchAgentsDir` so the
|
|
7193
7254
|
* `agent:pinned` handler knows where to materialise new configs.
|
|
@@ -7247,6 +7308,7 @@ var ClientRuntime = class {
|
|
|
7247
7308
|
this.agentIds.add(config.agentId);
|
|
7248
7309
|
}
|
|
7249
7310
|
async start() {
|
|
7311
|
+
this.contextTreeBinding = await syncContextTree(this.serverUrl, (opts) => ensureFreshAccessToken(opts), (msg) => print.status("[context-tree]", msg), CLI_USER_AGENT);
|
|
7250
7312
|
if (this.options.currentVersion && this.options.update) this.updateManager = UpdateManager.attach(this.connection, {
|
|
7251
7313
|
currentVersion: this.options.currentVersion,
|
|
7252
7314
|
...this.options.update,
|
|
@@ -7265,7 +7327,7 @@ var ClientRuntime = class {
|
|
|
7265
7327
|
}
|
|
7266
7328
|
await Promise.allSettled(this.agents.map(async (agent) => {
|
|
7267
7329
|
try {
|
|
7268
|
-
const identity = await agent.slot.start();
|
|
7330
|
+
const identity = await agent.slot.start(this.contextTreeBinding);
|
|
7269
7331
|
print.check(true, `${agent.name}: connected`, `agent: ${identity.displayName ?? identity.agentId}`);
|
|
7270
7332
|
} catch (error) {
|
|
7271
7333
|
const msg = error instanceof Error ? error.message : String(error);
|
|
@@ -7392,7 +7454,7 @@ var ClientRuntime = class {
|
|
|
7392
7454
|
startAgent(name) {
|
|
7393
7455
|
const entry = this.agents.find((a) => a.name === name);
|
|
7394
7456
|
if (!entry) return;
|
|
7395
|
-
entry.slot.start().then((identity) => {
|
|
7457
|
+
entry.slot.start(this.contextTreeBinding).then((identity) => {
|
|
7396
7458
|
print.check(true, `${name}: connected`, `agent: ${identity.displayName ?? identity.agentId}`);
|
|
7397
7459
|
}).catch((err) => {
|
|
7398
7460
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -9029,7 +9091,7 @@ async function onboardCreate(args) {
|
|
|
9029
9091
|
}
|
|
9030
9092
|
const runtimeAgent = args.type === "human" ? args.assistant : args.id;
|
|
9031
9093
|
if (args.feishuBotAppId && args.feishuBotAppSecret) {
|
|
9032
|
-
const { bindFeishuBot } = await import("./feishu-
|
|
9094
|
+
const { bindFeishuBot } = await import("./feishu-tkZS0vvL.mjs").then((n) => n.r);
|
|
9033
9095
|
const targetAgentUuid = args.type === "human" ? assistantUuid : primary.uuid;
|
|
9034
9096
|
if (!targetAgentUuid) print.line(`Warning: Cannot bind Feishu bot — no runtime agent available for "${args.id}".\n`);
|
|
9035
9097
|
else {
|
|
@@ -10242,7 +10304,7 @@ function createFeedbackHandler(config) {
|
|
|
10242
10304
|
return { handle };
|
|
10243
10305
|
}
|
|
10244
10306
|
//#endregion
|
|
10245
|
-
//#region ../server/dist/app-
|
|
10307
|
+
//#region ../server/dist/app-D4vCx0C0.mjs
|
|
10246
10308
|
var import_fastify_opentelemetry = /* @__PURE__ */ __toESM(require_fastify_opentelemetry(), 1);
|
|
10247
10309
|
init_esm();
|
|
10248
10310
|
var __defProp = Object.defineProperty;
|
|
@@ -10267,50 +10329,6 @@ const adapterAgentMappings = pgTable("adapter_agent_mappings", {
|
|
|
10267
10329
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
|
|
10268
10330
|
}, (table) => [unique("uq_adapter_agent_mapping").on(table.platform, table.externalUserId)]);
|
|
10269
10331
|
/**
|
|
10270
|
-
* Tasks — lightweight work units. Process descriptors, not tickets.
|
|
10271
|
-
* Immutable status state machine: pending → assigned → working → (completed | failed | cancelled).
|
|
10272
|
-
* Sub-tasks (parent_task_id) are deferred to a later phase.
|
|
10273
|
-
*
|
|
10274
|
-
* Referential integrity (org / assignee / chat) is enforced at the service layer,
|
|
10275
|
-
* not via DB foreign keys — see `services/task.ts`.
|
|
10276
|
-
*/
|
|
10277
|
-
const tasks = pgTable("tasks", {
|
|
10278
|
-
id: text("id").primaryKey(),
|
|
10279
|
-
organizationId: text("organization_id").notNull(),
|
|
10280
|
-
title: text("title").notNull(),
|
|
10281
|
-
body: text("body").notNull().default(""),
|
|
10282
|
-
status: text("status").$type().notNull(),
|
|
10283
|
-
assigneeAgentId: text("assignee_agent_id"),
|
|
10284
|
-
createdByType: text("created_by_type").$type().notNull(),
|
|
10285
|
-
createdById: text("created_by_id").notNull(),
|
|
10286
|
-
originRef: text("origin_ref"),
|
|
10287
|
-
result: text("result"),
|
|
10288
|
-
metadata: jsonb("metadata").$type().notNull().default({}),
|
|
10289
|
-
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
10290
|
-
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
|
10291
|
-
cancelledAt: timestamp("cancelled_at", { withTimezone: true }),
|
|
10292
|
-
cancelledByType: text("cancelled_by_type").$type(),
|
|
10293
|
-
cancelledById: text("cancelled_by_id")
|
|
10294
|
-
}, (table) => [
|
|
10295
|
-
index("idx_tasks_org_status").on(table.organizationId, table.status),
|
|
10296
|
-
index("idx_tasks_assignee_status").on(table.assigneeAgentId, table.status),
|
|
10297
|
-
index("idx_tasks_origin_ref").on(table.originRef),
|
|
10298
|
-
index("idx_tasks_org_created_at").on(table.organizationId, table.createdAt)
|
|
10299
|
-
]);
|
|
10300
|
-
/**
|
|
10301
|
-
* Task ↔ Chat association (M:N). A task may be executed across multiple chats;
|
|
10302
|
-
* a chat may host work for multiple tasks over its lifetime.
|
|
10303
|
-
*
|
|
10304
|
-
* No FK constraints — when a task or chat is deleted, the service layer is
|
|
10305
|
-
* responsible for deleting linked rows here first.
|
|
10306
|
-
*/
|
|
10307
|
-
const taskChats = pgTable("task_chats", {
|
|
10308
|
-
taskId: text("task_id").notNull(),
|
|
10309
|
-
chatId: text("chat_id").notNull(),
|
|
10310
|
-
linkedByAgentId: text("linked_by_agent_id"),
|
|
10311
|
-
linkedAt: timestamp("linked_at", { withTimezone: true }).notNull().defaultNow()
|
|
10312
|
-
}, (table) => [primaryKey({ columns: [table.taskId, table.chatId] }), index("idx_task_chats_chat").on(table.chatId)]);
|
|
10313
|
-
/**
|
|
10314
10332
|
* Pull the JWT-verified `UserScope` off the request. The `userAuthHook`
|
|
10315
10333
|
* middleware populates `request.user` synchronously before any handler
|
|
10316
10334
|
* runs; this helper just narrows the optional and throws a clean 401 if
|
|
@@ -10445,31 +10463,6 @@ async function assertAgentManageableByUser(db, userId, agentUuid) {
|
|
|
10445
10463
|
return scope;
|
|
10446
10464
|
}
|
|
10447
10465
|
/**
|
|
10448
|
-
* Gate access to a task. Allowed for any active member of the task's org —
|
|
10449
|
-
* mirrors the original inline gate in `api/tasks.ts` that this helper
|
|
10450
|
-
* replaces. Returns both the task's org row and the caller's resolved
|
|
10451
|
-
* `OrgScope`, so handlers can read `scope.memberId` for audit fields.
|
|
10452
|
-
*/
|
|
10453
|
-
async function requireTaskAccess(request, db) {
|
|
10454
|
-
const { userId } = requireUser(request);
|
|
10455
|
-
const { taskId } = request.params;
|
|
10456
|
-
const [task] = await db.select({ organizationId: tasks.organizationId }).from(tasks).where(eq(tasks.id, taskId)).limit(1);
|
|
10457
|
-
if (!task) throw new NotFoundError(`Task "${taskId}" not found`);
|
|
10458
|
-
const caller = await resolveCallerInOrg(db, userId, task.organizationId);
|
|
10459
|
-
const scope = {
|
|
10460
|
-
userId,
|
|
10461
|
-
organizationId: task.organizationId,
|
|
10462
|
-
memberId: caller.memberId,
|
|
10463
|
-
role: caller.role,
|
|
10464
|
-
humanAgentId: caller.humanAgentId
|
|
10465
|
-
};
|
|
10466
|
-
stampOrgScope(request, scope);
|
|
10467
|
-
return {
|
|
10468
|
-
task,
|
|
10469
|
-
scope
|
|
10470
|
-
};
|
|
10471
|
-
}
|
|
10472
|
-
/**
|
|
10473
10466
|
* Assert every agent in `agentIds` is visible to `scope` and lives in
|
|
10474
10467
|
* `scope.organizationId`. Used by chat-create to keep visibility rules out of
|
|
10475
10468
|
* the service layer's signature.
|
|
@@ -10905,10 +10898,9 @@ async function ensureDefaultOrganization(db) {
|
|
|
10905
10898
|
return org ?? existing;
|
|
10906
10899
|
}
|
|
10907
10900
|
/**
|
|
10908
|
-
* Names beginning with `__` are reserved for Hub-internal pseudo agents
|
|
10909
|
-
*
|
|
10910
|
-
*
|
|
10911
|
-
* real account.
|
|
10901
|
+
* Names beginning with `__` are reserved for Hub-internal pseudo agents.
|
|
10902
|
+
* User-facing creation must not be able to squat on them, otherwise
|
|
10903
|
+
* internal traffic could be routed through a real account.
|
|
10912
10904
|
*/
|
|
10913
10905
|
const RESERVED_AGENT_NAME_PREFIX = "__";
|
|
10914
10906
|
/**
|
|
@@ -11007,6 +10999,28 @@ async function resolveAgentClient(db, data) {
|
|
|
11007
10999
|
return client.id;
|
|
11008
11000
|
}
|
|
11009
11001
|
/**
|
|
11002
|
+
* Validate a `delegateMention` write at the service layer. Two checks:
|
|
11003
|
+
* 1. Target uuid must resolve to an existing agent — dangling references
|
|
11004
|
+
* would silently break webhook delegation at runtime.
|
|
11005
|
+
* 2. Target must belong to the same organization as the source agent —
|
|
11006
|
+
* cross-org delegate links are rejected here at the source so the
|
|
11007
|
+
* database never accumulates dirty rows. The webhook router has a
|
|
11008
|
+
* defense-in-depth check that filters them at fan-out time, but this
|
|
11009
|
+
* keeps the data clean and gives the admin UI an immediate 422 instead
|
|
11010
|
+
* of a silent runtime drop.
|
|
11011
|
+
*
|
|
11012
|
+
* `null` clears the field — handled by the caller; we are only invoked when
|
|
11013
|
+
* the caller wrote a non-null uuid.
|
|
11014
|
+
*/
|
|
11015
|
+
async function validateDelegateMentionTarget(db, targetUuid, sourceOrgId) {
|
|
11016
|
+
const [target] = await db.select({
|
|
11017
|
+
uuid: agents.uuid,
|
|
11018
|
+
organizationId: agents.organizationId
|
|
11019
|
+
}).from(agents).where(eq(agents.uuid, targetUuid)).limit(1);
|
|
11020
|
+
if (!target) throw new BadRequestError(`delegateMention target "${targetUuid}" not found`);
|
|
11021
|
+
if (target.organizationId !== sourceOrgId) throw new BadRequestError("delegateMention target must belong to the same organization as the agent");
|
|
11022
|
+
}
|
|
11023
|
+
/**
|
|
11010
11024
|
* Pick the first admin member in the org for internal system agents. Throws
|
|
11011
11025
|
* if the org has no admin — the caller should surface the error so an admin
|
|
11012
11026
|
* is created before the system tries to register more agents.
|
|
@@ -11046,6 +11060,7 @@ async function createAgent(db, data, options = {}) {
|
|
|
11046
11060
|
type: data.type
|
|
11047
11061
|
});
|
|
11048
11062
|
await ensureClientSupportsRuntimeProvider(db, clientId, runtimeProvider, { force: options.force });
|
|
11063
|
+
if (data.delegateMention) await validateDelegateMentionTarget(db, data.delegateMention, orgId);
|
|
11049
11064
|
const [org] = await db.select({ maxAgents: organizations.maxAgents }).from(organizations).where(eq(organizations.id, orgId)).limit(1);
|
|
11050
11065
|
if (org && org.maxAgents > 0) {
|
|
11051
11066
|
if (((await db.select({ value: count() }).from(agents).where(and(eq(agents.organizationId, orgId), ne(agents.status, AGENT_STATUSES.DELETED))))[0]?.value ?? 0) >= org.maxAgents) throw new ForbiddenError(`Organization "${orgId}" has reached its agent limit (${org.maxAgents}). Upgrade your plan or delete unused agents.`);
|
|
@@ -11192,7 +11207,10 @@ async function updateAgent(db, uuid, data) {
|
|
|
11192
11207
|
const updates = { updatedAt: /* @__PURE__ */ new Date() };
|
|
11193
11208
|
if (data.type !== void 0) updates.type = data.type;
|
|
11194
11209
|
if (data.displayName !== void 0) updates.displayName = data.displayName;
|
|
11195
|
-
if (data.delegateMention !== void 0)
|
|
11210
|
+
if (data.delegateMention !== void 0) {
|
|
11211
|
+
if (data.delegateMention !== null) await validateDelegateMentionTarget(db, data.delegateMention, agent.organizationId);
|
|
11212
|
+
updates.delegateMention = data.delegateMention;
|
|
11213
|
+
}
|
|
11196
11214
|
if (data.visibility !== void 0) updates.visibility = data.visibility;
|
|
11197
11215
|
if (data.metadata !== void 0) updates.metadata = data.metadata;
|
|
11198
11216
|
if (data.managerId !== void 0) {
|
|
@@ -12092,462 +12110,6 @@ async function agentSendToAgentRoutes(app) {
|
|
|
12092
12110
|
});
|
|
12093
12111
|
});
|
|
12094
12112
|
}
|
|
12095
|
-
/** Legal status transitions. Service enforces; API maps violations to 400. */
|
|
12096
|
-
const STATUS_TRANSITIONS = {
|
|
12097
|
-
pending: ["assigned", "cancelled"],
|
|
12098
|
-
assigned: ["working", "cancelled"],
|
|
12099
|
-
working: [
|
|
12100
|
-
"completed",
|
|
12101
|
-
"failed",
|
|
12102
|
-
"cancelled"
|
|
12103
|
-
],
|
|
12104
|
-
completed: [],
|
|
12105
|
-
failed: [],
|
|
12106
|
-
cancelled: []
|
|
12107
|
-
};
|
|
12108
|
-
function isLegalTransition(from, to) {
|
|
12109
|
-
return STATUS_TRANSITIONS[from]?.includes(to) ?? false;
|
|
12110
|
-
}
|
|
12111
|
-
function isTerminal(status) {
|
|
12112
|
-
return TASK_TERMINAL_STATUSES.includes(status);
|
|
12113
|
-
}
|
|
12114
|
-
/**
|
|
12115
|
-
* Reserved name for the hub-owned task notifier pseudo agent. The `__` prefix
|
|
12116
|
-
* is rejected by `createAgent`, so real users cannot squat on this identity.
|
|
12117
|
-
*/
|
|
12118
|
-
const SYSTEM_TASKS_AGENT_NAME = "__hub_system_tasks";
|
|
12119
|
-
/**
|
|
12120
|
-
* Ensure a task-notifier pseudo agent exists in the given organization and
|
|
12121
|
-
* return its UUID. Used as the sender for task notification messages so they
|
|
12122
|
-
* flow through the normal chat/inbox pipeline. Idempotent under concurrent
|
|
12123
|
-
* creation via the unique `(organization_id, name)` constraint.
|
|
12124
|
-
*/
|
|
12125
|
-
async function ensureSystemTasksAgent(db, organizationId) {
|
|
12126
|
-
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);
|
|
12127
|
-
if (existing) return existing.uuid;
|
|
12128
|
-
const uuid = uuidv7();
|
|
12129
|
-
const inboxId = `inbox_${uuid}`;
|
|
12130
|
-
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);
|
|
12131
|
-
if (!adminMember) throw new ConflictError(`Cannot create system tasks agent in organization "${organizationId}" — no admin member exists.`);
|
|
12132
|
-
try {
|
|
12133
|
-
const [created] = await db.insert(agents).values({
|
|
12134
|
-
uuid,
|
|
12135
|
-
name: SYSTEM_TASKS_AGENT_NAME,
|
|
12136
|
-
organizationId,
|
|
12137
|
-
type: AGENT_TYPES.AUTONOMOUS_AGENT,
|
|
12138
|
-
displayName: "System · Tasks",
|
|
12139
|
-
inboxId,
|
|
12140
|
-
status: AGENT_STATUSES.ACTIVE,
|
|
12141
|
-
source: AGENT_SOURCES.ADMIN_API,
|
|
12142
|
-
metadata: {
|
|
12143
|
-
system: true,
|
|
12144
|
-
role: "task-notifier"
|
|
12145
|
-
},
|
|
12146
|
-
managerId: adminMember.id
|
|
12147
|
-
}).returning({ uuid: agents.uuid });
|
|
12148
|
-
if (created) return created.uuid;
|
|
12149
|
-
} catch (err) {
|
|
12150
|
-
if ((err?.code ?? err?.cause?.code ?? "") !== "23505") throw err;
|
|
12151
|
-
}
|
|
12152
|
-
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);
|
|
12153
|
-
if (!row) throw new Error("ensureSystemTasksAgent: agent missing after conflict");
|
|
12154
|
-
return row.uuid;
|
|
12155
|
-
}
|
|
12156
|
-
function resolveCreator(actor) {
|
|
12157
|
-
if (actor.type === "agent") return {
|
|
12158
|
-
type: TASK_CREATOR_TYPES.AGENT,
|
|
12159
|
-
id: actor.agentId
|
|
12160
|
-
};
|
|
12161
|
-
return {
|
|
12162
|
-
type: TASK_CREATOR_TYPES.ADMIN,
|
|
12163
|
-
id: actor.adminId
|
|
12164
|
-
};
|
|
12165
|
-
}
|
|
12166
|
-
/**
|
|
12167
|
-
* Assert the task allows the given agent actor to mutate its chat associations.
|
|
12168
|
-
* Only the creator or assignee (for agents) or any admin may do so.
|
|
12169
|
-
*/
|
|
12170
|
-
function assertCanMutateTaskChats(task, actor) {
|
|
12171
|
-
if (actor.type === "admin") return;
|
|
12172
|
-
const isAssignee = task.assigneeAgentId === actor.agentId;
|
|
12173
|
-
const isCreator = task.createdByType === TASK_CREATOR_TYPES.AGENT && task.createdById === actor.agentId;
|
|
12174
|
-
if (!isAssignee && !isCreator) throw new ForbiddenError("Only the task creator or assignee may modify its chat associations");
|
|
12175
|
-
}
|
|
12176
|
-
async function loadAssigneeOrThrow(db, assigneeAgentId, expectedOrgId) {
|
|
12177
|
-
const [assignee] = await db.select({
|
|
12178
|
-
uuid: agents.uuid,
|
|
12179
|
-
organizationId: agents.organizationId,
|
|
12180
|
-
status: agents.status
|
|
12181
|
-
}).from(agents).where(eq(agents.uuid, assigneeAgentId)).limit(1);
|
|
12182
|
-
if (!assignee || assignee.status === AGENT_STATUSES.DELETED) throw new BadRequestError(`Assignee agent "${assigneeAgentId}" not found`);
|
|
12183
|
-
if (assignee.organizationId !== expectedOrgId) throw new BadRequestError("Assignee agent belongs to a different organization");
|
|
12184
|
-
if (assignee.status === AGENT_STATUSES.SUSPENDED) throw new BadRequestError(`Assignee agent "${assigneeAgentId}" is suspended`);
|
|
12185
|
-
return assignee;
|
|
12186
|
-
}
|
|
12187
|
-
/**
|
|
12188
|
-
* Create a task.
|
|
12189
|
-
*
|
|
12190
|
-
* Initial status is determined by assignee:
|
|
12191
|
-
* - no assignee → "pending"
|
|
12192
|
-
* - assignee is an agent and equals the creator → "working" (work-first; no notification)
|
|
12193
|
-
* - assignee set and differs from creator → "assigned" (task-first; notification dispatched)
|
|
12194
|
-
*
|
|
12195
|
-
* Task-first notifications go through the regular message+inbox pipeline via a per-org
|
|
12196
|
-
* task-notifier pseudo agent. The caller is responsible for triggering notifier fan-out
|
|
12197
|
-
* using the returned notification recipients.
|
|
12198
|
-
*/
|
|
12199
|
-
async function createTask(db, actor, input) {
|
|
12200
|
-
const [org] = await db.select({ id: organizations.id }).from(organizations).where(eq(organizations.id, input.organizationId)).limit(1);
|
|
12201
|
-
if (!org) throw new NotFoundError(`Organization "${input.organizationId}" not found`);
|
|
12202
|
-
if (input.assigneeAgentId) await loadAssigneeOrThrow(db, input.assigneeAgentId, input.organizationId);
|
|
12203
|
-
if (actor.type === "agent" && actor.organizationId !== input.organizationId) throw new ForbiddenError("Cannot create tasks in a different organization");
|
|
12204
|
-
const creator = resolveCreator(actor);
|
|
12205
|
-
const selfAssigned = input.assigneeAgentId !== void 0 && actor.type === "agent" && input.assigneeAgentId === actor.agentId;
|
|
12206
|
-
let initialStatus;
|
|
12207
|
-
if (!input.assigneeAgentId) initialStatus = TASK_STATUSES.PENDING;
|
|
12208
|
-
else if (selfAssigned) initialStatus = TASK_STATUSES.WORKING;
|
|
12209
|
-
else initialStatus = TASK_STATUSES.ASSIGNED;
|
|
12210
|
-
const taskId = uuidv7();
|
|
12211
|
-
const [task] = await db.insert(tasks).values({
|
|
12212
|
-
id: taskId,
|
|
12213
|
-
organizationId: input.organizationId,
|
|
12214
|
-
title: input.title,
|
|
12215
|
-
body: input.body ?? "",
|
|
12216
|
-
status: initialStatus,
|
|
12217
|
-
assigneeAgentId: input.assigneeAgentId ?? null,
|
|
12218
|
-
createdByType: creator.type,
|
|
12219
|
-
createdById: creator.id,
|
|
12220
|
-
originRef: input.originRef ?? null,
|
|
12221
|
-
metadata: input.metadata ?? {}
|
|
12222
|
-
}).returning();
|
|
12223
|
-
if (!task) throw new Error("Unexpected: INSERT RETURNING produced no row");
|
|
12224
|
-
let notification;
|
|
12225
|
-
if (initialStatus === TASK_STATUSES.ASSIGNED && task.assigneeAgentId) notification = await dispatchTaskSystemMessage(db, task, "assigned");
|
|
12226
|
-
return {
|
|
12227
|
-
task,
|
|
12228
|
-
notification
|
|
12229
|
-
};
|
|
12230
|
-
}
|
|
12231
|
-
/** Compose and send a system message describing a task state change to the assignee's chat. */
|
|
12232
|
-
async function dispatchTaskSystemMessage(db, task, event, fromStatus) {
|
|
12233
|
-
if (!task.assigneeAgentId) return void 0;
|
|
12234
|
-
const systemAgentId = await ensureSystemTasksAgent(db, task.organizationId);
|
|
12235
|
-
if (systemAgentId === task.assigneeAgentId) return void 0;
|
|
12236
|
-
const chat = await findOrCreateDirectChat(db, systemAgentId, task.assigneeAgentId);
|
|
12237
|
-
const content = {
|
|
12238
|
-
taskId: task.id,
|
|
12239
|
-
event,
|
|
12240
|
-
title: task.title,
|
|
12241
|
-
body: task.body,
|
|
12242
|
-
status: task.status,
|
|
12243
|
-
...fromStatus ? { fromStatus } : {},
|
|
12244
|
-
originRef: task.originRef
|
|
12245
|
-
};
|
|
12246
|
-
return sendMessage(db, chat.id, systemAgentId, {
|
|
12247
|
-
format: "task",
|
|
12248
|
-
content,
|
|
12249
|
-
metadata: {
|
|
12250
|
-
taskId: task.id,
|
|
12251
|
-
event,
|
|
12252
|
-
mentions: [task.assigneeAgentId]
|
|
12253
|
-
}
|
|
12254
|
-
});
|
|
12255
|
-
}
|
|
12256
|
-
/**
|
|
12257
|
-
* Fetch a task, optionally asserting it belongs to `expectedOrgId`. Cross-org
|
|
12258
|
-
* access is reported as NotFound so we don't leak existence across tenants.
|
|
12259
|
-
*/
|
|
12260
|
-
async function getTask(db, taskId, expectedOrgId) {
|
|
12261
|
-
const [task] = await db.select().from(tasks).where(eq(tasks.id, taskId)).limit(1);
|
|
12262
|
-
if (!task) throw new NotFoundError(`Task "${taskId}" not found`);
|
|
12263
|
-
if (expectedOrgId !== void 0 && task.organizationId !== expectedOrgId) throw new NotFoundError(`Task "${taskId}" not found`);
|
|
12264
|
-
return task;
|
|
12265
|
-
}
|
|
12266
|
-
async function getTaskDetail(db, taskId, expectedOrgId) {
|
|
12267
|
-
const task = await getTask(db, taskId, expectedOrgId);
|
|
12268
|
-
const links = await db.select().from(taskChats).where(eq(taskChats.taskId, taskId));
|
|
12269
|
-
return {
|
|
12270
|
-
...task,
|
|
12271
|
-
chats: links.map((c) => ({
|
|
12272
|
-
taskId: c.taskId,
|
|
12273
|
-
chatId: c.chatId,
|
|
12274
|
-
linkedByAgentId: c.linkedByAgentId,
|
|
12275
|
-
linkedAt: c.linkedAt.toISOString()
|
|
12276
|
-
}))
|
|
12277
|
-
};
|
|
12278
|
-
}
|
|
12279
|
-
async function listTasks(db, organizationId, query) {
|
|
12280
|
-
const conditions = [eq(tasks.organizationId, organizationId)];
|
|
12281
|
-
if (query.status) conditions.push(eq(tasks.status, query.status));
|
|
12282
|
-
if (query.assigneeAgentId) conditions.push(eq(tasks.assigneeAgentId, query.assigneeAgentId));
|
|
12283
|
-
if (query.originRef) conditions.push(eq(tasks.originRef, query.originRef));
|
|
12284
|
-
if (query.cursor) conditions.push(lt(tasks.createdAt, new Date(query.cursor)));
|
|
12285
|
-
const rows = await db.select().from(tasks).where(and(...conditions)).orderBy(desc(tasks.createdAt)).limit(query.limit + 1);
|
|
12286
|
-
const hasMore = rows.length > query.limit;
|
|
12287
|
-
const items = hasMore ? rows.slice(0, query.limit) : rows;
|
|
12288
|
-
const last = items[items.length - 1];
|
|
12289
|
-
return {
|
|
12290
|
-
items,
|
|
12291
|
-
nextCursor: hasMore && last ? last.createdAt.toISOString() : null
|
|
12292
|
-
};
|
|
12293
|
-
}
|
|
12294
|
-
/** Agent self-report: working / completed / failed. */
|
|
12295
|
-
async function updateTaskStatus(db, taskId, actor, data) {
|
|
12296
|
-
const existing = await getTask(db, taskId);
|
|
12297
|
-
if (actor.type !== "agent") throw new ForbiddenError("updateTaskStatus is for agent self-report; use adminUpdateTask for admin actions");
|
|
12298
|
-
if (existing.assigneeAgentId !== actor.agentId) throw new ForbiddenError("Only the assignee may update this task");
|
|
12299
|
-
const from = existing.status;
|
|
12300
|
-
const to = data.status;
|
|
12301
|
-
if (!isLegalTransition(from, to)) throw new BadRequestError(`Illegal status transition: ${from} → ${to}`);
|
|
12302
|
-
if (to === TASK_STATUSES.COMPLETED && data.result === void 0) throw new BadRequestError("Completion requires a result (may be an empty string)");
|
|
12303
|
-
const updates = {
|
|
12304
|
-
status: to,
|
|
12305
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
12306
|
-
};
|
|
12307
|
-
if (data.result !== void 0) updates.result = data.result;
|
|
12308
|
-
const [updated] = await db.update(tasks).set(updates).where(eq(tasks.id, taskId)).returning();
|
|
12309
|
-
if (!updated) throw new Error("Unexpected: UPDATE RETURNING produced no row");
|
|
12310
|
-
return { task: updated };
|
|
12311
|
-
}
|
|
12312
|
-
/** Admin-facing update: may re-assign while pending, or force a status transition (still gated by state machine). */
|
|
12313
|
-
async function adminUpdateTask(db, taskId, actor, data) {
|
|
12314
|
-
if (actor.type !== "admin") throw new ForbiddenError("adminUpdateTask requires admin actor");
|
|
12315
|
-
const existing = await getTask(db, taskId);
|
|
12316
|
-
if (data.status === TASK_STATUSES.CANCELLED) {
|
|
12317
|
-
if (isTerminal(existing.status)) throw new ConflictError(`Task is already in terminal state "${existing.status}"`);
|
|
12318
|
-
return cancelTask(db, taskId, actor);
|
|
12319
|
-
}
|
|
12320
|
-
const updates = { updatedAt: /* @__PURE__ */ new Date() };
|
|
12321
|
-
let notify = false;
|
|
12322
|
-
if (data.assigneeAgentId !== void 0) {
|
|
12323
|
-
if (existing.status !== TASK_STATUSES.PENDING && data.assigneeAgentId !== existing.assigneeAgentId) throw new BadRequestError("Cannot reassign a task that is not pending");
|
|
12324
|
-
if (data.assigneeAgentId !== null) {
|
|
12325
|
-
await loadAssigneeOrThrow(db, data.assigneeAgentId, existing.organizationId);
|
|
12326
|
-
updates.assigneeAgentId = data.assigneeAgentId;
|
|
12327
|
-
updates.status = TASK_STATUSES.ASSIGNED;
|
|
12328
|
-
notify = true;
|
|
12329
|
-
} else {
|
|
12330
|
-
updates.assigneeAgentId = null;
|
|
12331
|
-
updates.status = TASK_STATUSES.PENDING;
|
|
12332
|
-
}
|
|
12333
|
-
}
|
|
12334
|
-
if (data.status !== void 0 && data.status !== existing.status) {
|
|
12335
|
-
const from = updates.status ?? existing.status;
|
|
12336
|
-
if (!isLegalTransition(from, data.status)) throw new BadRequestError(`Illegal status transition: ${from} → ${data.status}`);
|
|
12337
|
-
updates.status = data.status;
|
|
12338
|
-
}
|
|
12339
|
-
if (data.result !== void 0) updates.result = data.result;
|
|
12340
|
-
const resolvedStatus = updates.status ?? existing.status;
|
|
12341
|
-
const resolvedAssignee = updates.assigneeAgentId === void 0 ? existing.assigneeAgentId : updates.assigneeAgentId;
|
|
12342
|
-
if (resolvedStatus === TASK_STATUSES.ASSIGNED && !resolvedAssignee) throw new BadRequestError("Cannot set status to \"assigned\" without an assignee");
|
|
12343
|
-
const [updated] = await db.update(tasks).set(updates).where(eq(tasks.id, taskId)).returning();
|
|
12344
|
-
if (!updated) throw new Error("Unexpected: UPDATE RETURNING produced no row");
|
|
12345
|
-
let notification;
|
|
12346
|
-
if (notify && updated.assigneeAgentId) notification = await dispatchTaskSystemMessage(db, updated, "assigned");
|
|
12347
|
-
return {
|
|
12348
|
-
task: updated,
|
|
12349
|
-
notification
|
|
12350
|
-
};
|
|
12351
|
-
}
|
|
12352
|
-
async function cancelTask(db, taskId, actor) {
|
|
12353
|
-
const existing = await getTask(db, taskId);
|
|
12354
|
-
if (isTerminal(existing.status)) throw new ConflictError(`Task is already in terminal state "${existing.status}"`);
|
|
12355
|
-
if (actor.type === "agent") {
|
|
12356
|
-
const isAssignee = existing.assigneeAgentId === actor.agentId;
|
|
12357
|
-
const isCreator = existing.createdByType === TASK_CREATOR_TYPES.AGENT && existing.createdById === actor.agentId;
|
|
12358
|
-
if (!isAssignee && !isCreator) throw new ForbiddenError("Only the assignee or creator may cancel this task");
|
|
12359
|
-
}
|
|
12360
|
-
const now = /* @__PURE__ */ new Date();
|
|
12361
|
-
const { type: cancelType, id: cancelId } = resolveCreator(actor);
|
|
12362
|
-
const [updated] = await db.update(tasks).set({
|
|
12363
|
-
status: TASK_STATUSES.CANCELLED,
|
|
12364
|
-
cancelledAt: now,
|
|
12365
|
-
cancelledByType: cancelType,
|
|
12366
|
-
cancelledById: cancelId,
|
|
12367
|
-
updatedAt: now
|
|
12368
|
-
}).where(eq(tasks.id, taskId)).returning();
|
|
12369
|
-
if (!updated) throw new Error("Unexpected: UPDATE RETURNING produced no row");
|
|
12370
|
-
let notification;
|
|
12371
|
-
if (updated.assigneeAgentId && !(actor.type === "agent" && actor.agentId === updated.assigneeAgentId)) notification = await dispatchTaskSystemMessage(db, updated, "cancelled", existing.status);
|
|
12372
|
-
return {
|
|
12373
|
-
task: updated,
|
|
12374
|
-
notification
|
|
12375
|
-
};
|
|
12376
|
-
}
|
|
12377
|
-
async function linkChatToTask(db, taskId, chatId, actor) {
|
|
12378
|
-
const task = await getTask(db, taskId);
|
|
12379
|
-
if (actor.type === "agent" && task.organizationId !== actor.organizationId) throw new NotFoundError(`Task "${taskId}" not found`);
|
|
12380
|
-
assertCanMutateTaskChats(task, actor);
|
|
12381
|
-
const [chat] = await db.select({ organizationId: chats.organizationId }).from(chats).where(eq(chats.id, chatId)).limit(1);
|
|
12382
|
-
if (!chat) throw new NotFoundError(`Chat "${chatId}" not found`);
|
|
12383
|
-
if (chat.organizationId !== task.organizationId) throw new BadRequestError("Chat belongs to a different organization");
|
|
12384
|
-
if (actor.type === "agent") await assertParticipant(db, chatId, actor.agentId);
|
|
12385
|
-
const linkedBy = actor.type === "agent" ? actor.agentId : null;
|
|
12386
|
-
await db.insert(taskChats).values({
|
|
12387
|
-
taskId,
|
|
12388
|
-
chatId,
|
|
12389
|
-
linkedByAgentId: linkedBy
|
|
12390
|
-
}).onConflictDoNothing();
|
|
12391
|
-
}
|
|
12392
|
-
async function unlinkChatFromTask(db, taskId, chatId, actor) {
|
|
12393
|
-
const task = await getTask(db, taskId);
|
|
12394
|
-
if (actor.type === "agent" && task.organizationId !== actor.organizationId) throw new NotFoundError(`Task "${taskId}" not found`);
|
|
12395
|
-
assertCanMutateTaskChats(task, actor);
|
|
12396
|
-
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}"`);
|
|
12397
|
-
}
|
|
12398
|
-
/**
|
|
12399
|
-
* Derive a health signal for a task. Only meaningful for `working` tasks.
|
|
12400
|
-
* See hub-task-design Section 9 for the rules this implements.
|
|
12401
|
-
*
|
|
12402
|
-
* Algorithm (per linked chat for the assignee):
|
|
12403
|
-
* 1. No session row OR state != 'active' → idle_island candidate
|
|
12404
|
-
* 2. Session active, last message from assignee → awaiting_reply candidate
|
|
12405
|
-
* 3. Session active, last message from other → normal candidate
|
|
12406
|
-
* Across all linked chats, normal wins over awaiting_reply, which wins over idle_island.
|
|
12407
|
-
*/
|
|
12408
|
-
async function getTaskHealth(db, taskId, expectedOrgId) {
|
|
12409
|
-
const task = await getTask(db, taskId, expectedOrgId);
|
|
12410
|
-
if (task.status !== TASK_STATUSES.WORKING) return {
|
|
12411
|
-
taskId,
|
|
12412
|
-
signal: TASK_HEALTH_SIGNALS.NOT_APPLICABLE,
|
|
12413
|
-
reason: `Task status is "${task.status}" — health is only computed for working tasks`
|
|
12414
|
-
};
|
|
12415
|
-
if (!task.assigneeAgentId) return {
|
|
12416
|
-
taskId,
|
|
12417
|
-
signal: TASK_HEALTH_SIGNALS.NO_CHAT,
|
|
12418
|
-
reason: "Task has no assignee"
|
|
12419
|
-
};
|
|
12420
|
-
const linked = await db.select({
|
|
12421
|
-
chatId: taskChats.chatId,
|
|
12422
|
-
sessionState: agentChatSessions.state
|
|
12423
|
-
}).from(taskChats).leftJoin(agentChatSessions, and(eq(agentChatSessions.chatId, taskChats.chatId), eq(agentChatSessions.agentId, task.assigneeAgentId))).where(eq(taskChats.taskId, taskId));
|
|
12424
|
-
if (linked.length === 0) return {
|
|
12425
|
-
taskId,
|
|
12426
|
-
signal: TASK_HEALTH_SIGNALS.NO_CHAT,
|
|
12427
|
-
reason: "Task has no linked chats"
|
|
12428
|
-
};
|
|
12429
|
-
const chatSignals = [];
|
|
12430
|
-
for (const row of linked) {
|
|
12431
|
-
if (row.sessionState !== "active") {
|
|
12432
|
-
chatSignals.push(TASK_HEALTH_SIGNALS.IDLE_ISLAND);
|
|
12433
|
-
continue;
|
|
12434
|
-
}
|
|
12435
|
-
const [last] = await db.select({ senderId: messages.senderId }).from(messages).where(eq(messages.chatId, row.chatId)).orderBy(desc(messages.createdAt)).limit(1);
|
|
12436
|
-
if (!last) {
|
|
12437
|
-
chatSignals.push(TASK_HEALTH_SIGNALS.IDLE_ISLAND);
|
|
12438
|
-
continue;
|
|
12439
|
-
}
|
|
12440
|
-
if (last.senderId === task.assigneeAgentId) chatSignals.push(TASK_HEALTH_SIGNALS.AWAITING_REPLY);
|
|
12441
|
-
else chatSignals.push(TASK_HEALTH_SIGNALS.NORMAL);
|
|
12442
|
-
}
|
|
12443
|
-
if (chatSignals.includes(TASK_HEALTH_SIGNALS.NORMAL)) return {
|
|
12444
|
-
taskId,
|
|
12445
|
-
signal: TASK_HEALTH_SIGNALS.NORMAL,
|
|
12446
|
-
reason: "At least one linked chat is actively progressing"
|
|
12447
|
-
};
|
|
12448
|
-
if (chatSignals.includes(TASK_HEALTH_SIGNALS.AWAITING_REPLY)) return {
|
|
12449
|
-
taskId,
|
|
12450
|
-
signal: TASK_HEALTH_SIGNALS.AWAITING_REPLY,
|
|
12451
|
-
reason: "Assignee sent the last message and is waiting for a reply"
|
|
12452
|
-
};
|
|
12453
|
-
return {
|
|
12454
|
-
taskId,
|
|
12455
|
-
signal: TASK_HEALTH_SIGNALS.IDLE_ISLAND,
|
|
12456
|
-
reason: "No active session found for the assignee in any linked chat"
|
|
12457
|
-
};
|
|
12458
|
-
}
|
|
12459
|
-
/** Serialize a task row for API output. */
|
|
12460
|
-
function serializeTask(task) {
|
|
12461
|
-
return {
|
|
12462
|
-
...task,
|
|
12463
|
-
createdAt: task.createdAt.toISOString(),
|
|
12464
|
-
updatedAt: task.updatedAt.toISOString(),
|
|
12465
|
-
cancelledAt: task.cancelledAt ? task.cancelledAt.toISOString() : null
|
|
12466
|
-
};
|
|
12467
|
-
}
|
|
12468
|
-
function dispatch$2(notifier, result) {
|
|
12469
|
-
if (!result) return;
|
|
12470
|
-
notifyRecipients(notifier, result.recipients, result.message.id);
|
|
12471
|
-
}
|
|
12472
|
-
async function agentTaskRoutes(app) {
|
|
12473
|
-
/** Create a task. Agent creator; assignee defaults to self (work-first) if omitted. */
|
|
12474
|
-
app.post("/", async (request, reply) => {
|
|
12475
|
-
const identity = requireAgent(request);
|
|
12476
|
-
const body = createTaskSchema.parse(request.body);
|
|
12477
|
-
const { task, notification } = await createTask(app.db, {
|
|
12478
|
-
type: "agent",
|
|
12479
|
-
agentId: identity.uuid,
|
|
12480
|
-
organizationId: identity.organizationId
|
|
12481
|
-
}, {
|
|
12482
|
-
...body,
|
|
12483
|
-
organizationId: identity.organizationId
|
|
12484
|
-
});
|
|
12485
|
-
dispatch$2(app.notifier, notification);
|
|
12486
|
-
return reply.status(201).send(serializeTask(task));
|
|
12487
|
-
});
|
|
12488
|
-
app.get("/", async (request) => {
|
|
12489
|
-
const identity = requireAgent(request);
|
|
12490
|
-
const query = taskListQuerySchema.parse(request.query);
|
|
12491
|
-
const result = await listTasks(app.db, identity.organizationId, query);
|
|
12492
|
-
return {
|
|
12493
|
-
items: result.items.map((t) => serializeTask(t)),
|
|
12494
|
-
nextCursor: result.nextCursor
|
|
12495
|
-
};
|
|
12496
|
-
});
|
|
12497
|
-
app.get("/:taskId", async (request) => {
|
|
12498
|
-
const identity = requireAgent(request);
|
|
12499
|
-
const detail = await getTaskDetail(app.db, request.params.taskId, identity.organizationId);
|
|
12500
|
-
return {
|
|
12501
|
-
...serializeTask(detail),
|
|
12502
|
-
chats: detail.chats
|
|
12503
|
-
};
|
|
12504
|
-
});
|
|
12505
|
-
/** Agent self-report: working / completed / failed. */
|
|
12506
|
-
app.patch("/:taskId", async (request) => {
|
|
12507
|
-
const identity = requireAgent(request);
|
|
12508
|
-
const body = updateTaskStatusSchema.parse(request.body);
|
|
12509
|
-
const { task } = await updateTaskStatus(app.db, request.params.taskId, {
|
|
12510
|
-
type: "agent",
|
|
12511
|
-
agentId: identity.uuid,
|
|
12512
|
-
organizationId: identity.organizationId
|
|
12513
|
-
}, body);
|
|
12514
|
-
return serializeTask(task);
|
|
12515
|
-
});
|
|
12516
|
-
app.post("/:taskId/cancel", async (request) => {
|
|
12517
|
-
const identity = requireAgent(request);
|
|
12518
|
-
const { task, notification } = await cancelTask(app.db, request.params.taskId, {
|
|
12519
|
-
type: "agent",
|
|
12520
|
-
agentId: identity.uuid,
|
|
12521
|
-
organizationId: identity.organizationId
|
|
12522
|
-
});
|
|
12523
|
-
dispatch$2(app.notifier, notification);
|
|
12524
|
-
return serializeTask(task);
|
|
12525
|
-
});
|
|
12526
|
-
app.post("/:taskId/chats", async (request, reply) => {
|
|
12527
|
-
const identity = requireAgent(request);
|
|
12528
|
-
const body = linkTaskChatSchema.parse(request.body);
|
|
12529
|
-
await linkChatToTask(app.db, request.params.taskId, body.chatId, {
|
|
12530
|
-
type: "agent",
|
|
12531
|
-
agentId: identity.uuid,
|
|
12532
|
-
organizationId: identity.organizationId
|
|
12533
|
-
});
|
|
12534
|
-
return reply.status(204).send();
|
|
12535
|
-
});
|
|
12536
|
-
app.delete("/:taskId/chats/:chatId", async (request, reply) => {
|
|
12537
|
-
const identity = requireAgent(request);
|
|
12538
|
-
await unlinkChatFromTask(app.db, request.params.taskId, request.params.chatId, {
|
|
12539
|
-
type: "agent",
|
|
12540
|
-
agentId: identity.uuid,
|
|
12541
|
-
organizationId: identity.organizationId
|
|
12542
|
-
});
|
|
12543
|
-
return reply.status(204).send();
|
|
12544
|
-
});
|
|
12545
|
-
/** Task health signal — only meaningful while task.status === "working". */
|
|
12546
|
-
app.get("/:taskId/health", async (request) => {
|
|
12547
|
-
const identity = requireAgent(request);
|
|
12548
|
-
return getTaskHealth(app.db, request.params.taskId, identity.organizationId);
|
|
12549
|
-
});
|
|
12550
|
-
}
|
|
12551
12113
|
/** WS close code: agent already connected from another client. */
|
|
12552
12114
|
const WS_CLOSE_ALREADY_CONNECTED = 4009;
|
|
12553
12115
|
/** Track active WS connections per agentId. At most one entry per agent. */
|
|
@@ -13779,7 +13341,7 @@ async function agentRoutes(app) {
|
|
|
13779
13341
|
});
|
|
13780
13342
|
app.post("/:uuid/test", async (request, reply) => {
|
|
13781
13343
|
const { uuid } = request.params;
|
|
13782
|
-
await requireAgentAccess(request, app.db, "manage");
|
|
13344
|
+
const { agent: targetAgent } = await requireAgentAccess(request, app.db, "manage");
|
|
13783
13345
|
const presence = await getPresence(app.db, uuid);
|
|
13784
13346
|
const wsConnected = hasActiveConnection(uuid);
|
|
13785
13347
|
const clientId = getAgentClientId(uuid) ?? presence?.clientId ?? null;
|
|
@@ -13816,15 +13378,15 @@ async function agentRoutes(app) {
|
|
|
13816
13378
|
message: "Agent connection is stale — heartbeat lost. The client process may have crashed.",
|
|
13817
13379
|
connection
|
|
13818
13380
|
});
|
|
13819
|
-
const [owner] = await app.db.select({ uuid: agents.uuid }).from(agents).where(and(eq(agents.delegateMention, uuid), eq(agents.status, "active"))).limit(1);
|
|
13381
|
+
const [owner] = await app.db.select({ uuid: agents.uuid }).from(agents).where(and(eq(agents.delegateMention, uuid), eq(agents.status, "active"), eq(agents.organizationId, targetAgent.organizationId))).limit(1);
|
|
13820
13382
|
let senderId = owner?.uuid ?? null;
|
|
13821
13383
|
if (!senderId) {
|
|
13822
|
-
const [other] = await app.db.select({ uuid: agents.uuid }).from(agents).where(and(ne(agents.uuid, uuid), eq(agents.status, "active"))).limit(1);
|
|
13384
|
+
const [other] = await app.db.select({ uuid: agents.uuid }).from(agents).where(and(ne(agents.uuid, uuid), eq(agents.status, "active"), eq(agents.organizationId, targetAgent.organizationId))).limit(1);
|
|
13823
13385
|
senderId = other?.uuid ?? null;
|
|
13824
13386
|
}
|
|
13825
13387
|
if (!senderId) return reply.status(200).send({
|
|
13826
13388
|
status: "error",
|
|
13827
|
-
message: "No suitable sender found. Need at least one other active agent.",
|
|
13389
|
+
message: "No suitable sender found. Need at least one other active agent in the same organization.",
|
|
13828
13390
|
connection
|
|
13829
13391
|
});
|
|
13830
13392
|
const chat = await findOrCreateDirectChat(app.db, senderId, uuid);
|
|
@@ -15017,7 +14579,7 @@ function decodeCursor(cursor) {
|
|
|
15017
14579
|
* - Cursor narrows the result to rows STRICTLY before `(cursor.ts, cursor.id)`.
|
|
15018
14580
|
* - Followed by a small participant-list lookup for the page only.
|
|
15019
14581
|
*/
|
|
15020
|
-
async function listMeChats(db, humanAgentId, query) {
|
|
14582
|
+
async function listMeChats(db, humanAgentId, organizationId, query) {
|
|
15021
14583
|
const limit = query.limit;
|
|
15022
14584
|
const cursor = query.cursor ? decodeCursor(query.cursor) : null;
|
|
15023
14585
|
if (query.cursor && !cursor) throw new BadRequestError("Invalid cursor");
|
|
@@ -15058,6 +14620,11 @@ async function listMeChats(db, humanAgentId, query) {
|
|
|
15058
14620
|
FROM chats c
|
|
15059
14621
|
JOIN deduped d ON d.chat_id = c.id
|
|
15060
14622
|
WHERE c.parent_chat_id IS NULL
|
|
14623
|
+
/* Scope to the caller's org. Without this, cross-org dirty chats
|
|
14624
|
+
whose chat_participants still reference the caller's human agent
|
|
14625
|
+
(historical pollution — see fix/cross-org-direct-chat-pollution)
|
|
14626
|
+
would leak into the list and 404 on click via requireChatAccess. */
|
|
14627
|
+
AND c.organization_id = ${organizationId}
|
|
15061
14628
|
/* Filter: unread / watching */
|
|
15062
14629
|
AND (${!filterUnreadOnly}::bool OR d.unread_mention_count > 0)
|
|
15063
14630
|
AND (${!filterWatchingOnly}::bool OR d.membership_kind = 'watching')
|
|
@@ -15118,9 +14685,7 @@ async function listMeChats(db, humanAgentId, query) {
|
|
|
15118
14685
|
lastMessageAt: toDate(r.last_message_at)?.toISOString() ?? null,
|
|
15119
14686
|
lastMessagePreview: r.last_message_preview,
|
|
15120
14687
|
unreadMentionCount: r.unread_mention_count,
|
|
15121
|
-
canReply: r.membership_kind === "participant"
|
|
15122
|
-
taskId: null,
|
|
15123
|
-
taskStatus: null
|
|
14688
|
+
canReply: r.membership_kind === "participant"
|
|
15124
14689
|
};
|
|
15125
14690
|
}),
|
|
15126
14691
|
nextCursor
|
|
@@ -16077,17 +15642,17 @@ function isGithubHttpsRepo(repoUrl) {
|
|
|
16077
15642
|
}
|
|
16078
15643
|
function contextStatus(warning) {
|
|
16079
15644
|
if (warning?.stale) return {
|
|
16080
|
-
label: "
|
|
15645
|
+
label: "Context Tree may be stale",
|
|
16081
15646
|
detail: warning.detail,
|
|
16082
15647
|
severity: "warning"
|
|
16083
15648
|
};
|
|
16084
15649
|
if (warning) return {
|
|
16085
|
-
label: "
|
|
15650
|
+
label: "Context Tree needs attention",
|
|
16086
15651
|
detail: warning.detail,
|
|
16087
15652
|
severity: "warning"
|
|
16088
15653
|
};
|
|
16089
15654
|
return {
|
|
16090
|
-
label: "
|
|
15655
|
+
label: "Context Tree is up to date",
|
|
16091
15656
|
detail: "Agents have a synced team context snapshot available.",
|
|
16092
15657
|
severity: "ok"
|
|
16093
15658
|
};
|
|
@@ -16838,7 +16403,7 @@ async function healthzRoutes(app) {
|
|
|
16838
16403
|
* `api/orgs/invitations.ts` (Class B, admin-gated).
|
|
16839
16404
|
*/
|
|
16840
16405
|
async function publicInvitationRoutes(app) {
|
|
16841
|
-
const { previewInvitation } = await import("./invitation-C299fxkP-
|
|
16406
|
+
const { previewInvitation } = await import("./invitation-C299fxkP-CZRV665C.mjs");
|
|
16842
16407
|
app.get("/:token/preview", async (request, reply) => {
|
|
16843
16408
|
if (!request.params.token) throw new UnauthorizedError("Token required");
|
|
16844
16409
|
const preview = await previewInvitation(app.db, request.params.token);
|
|
@@ -17018,7 +16583,7 @@ async function meRoutes(app) {
|
|
|
17018
16583
|
*/
|
|
17019
16584
|
app.get("/me/pinned-agents", async (request) => {
|
|
17020
16585
|
const { userId } = requireUser(request);
|
|
17021
|
-
const { listMyPinnedAgents } = await import("./client-
|
|
16586
|
+
const { listMyPinnedAgents } = await import("./client-0RrgrMjR-CylTJGEb.mjs");
|
|
17022
16587
|
return listMyPinnedAgents(app.db, { userId });
|
|
17023
16588
|
});
|
|
17024
16589
|
/**
|
|
@@ -17451,7 +17016,7 @@ async function orgChatRoutes(app) {
|
|
|
17451
17016
|
}
|
|
17452
17017
|
if (view === "grouped") return listChatsForMember(app.db, scope.memberId, scope.humanAgentId);
|
|
17453
17018
|
const query = listMeChatsQuerySchema.parse(request.query);
|
|
17454
|
-
return listMeChats(app.db, scope.humanAgentId, query);
|
|
17019
|
+
return listMeChats(app.db, scope.humanAgentId, scope.organizationId, query);
|
|
17455
17020
|
});
|
|
17456
17021
|
/**
|
|
17457
17022
|
* POST /orgs/:orgId/chats — create a new chat. The :orgId path param
|
|
@@ -17831,39 +17396,6 @@ function enrichOutput(namespace, out, orgId, publicUrl) {
|
|
|
17831
17396
|
}
|
|
17832
17397
|
return out;
|
|
17833
17398
|
}
|
|
17834
|
-
function dispatch$1(notifier, result) {
|
|
17835
|
-
if (!result) return;
|
|
17836
|
-
notifyRecipients(notifier, result.recipients, result.message.id);
|
|
17837
|
-
}
|
|
17838
|
-
/** Class B — `/api/v1/orgs/:orgId/tasks`. Per-task ops live in api/tasks.ts. */
|
|
17839
|
-
async function orgTaskRoutes(app) {
|
|
17840
|
-
app.get("/", async (request) => {
|
|
17841
|
-
const scope = await requireOrgMembership(request, app.db);
|
|
17842
|
-
const query = taskListQuerySchema.parse(request.query);
|
|
17843
|
-
const result = await listTasks(app.db, scope.organizationId, query);
|
|
17844
|
-
return {
|
|
17845
|
-
items: result.items.map((t) => serializeTask(t)),
|
|
17846
|
-
nextCursor: result.nextCursor
|
|
17847
|
-
};
|
|
17848
|
-
});
|
|
17849
|
-
app.post("/", async (request, reply) => {
|
|
17850
|
-
const scope = await requireOrgMembership(request, app.db);
|
|
17851
|
-
const body = adminCreateTaskSchema.parse(request.body);
|
|
17852
|
-
const { task, notification } = await createTask(app.db, {
|
|
17853
|
-
type: "admin",
|
|
17854
|
-
adminId: scope.memberId
|
|
17855
|
-
}, {
|
|
17856
|
-
title: body.title,
|
|
17857
|
-
body: body.body,
|
|
17858
|
-
...body.assigneeAgentId !== void 0 ? { assigneeAgentId: body.assigneeAgentId } : {},
|
|
17859
|
-
...body.originRef !== void 0 ? { originRef: body.originRef } : {},
|
|
17860
|
-
...body.metadata !== void 0 ? { metadata: body.metadata } : {},
|
|
17861
|
-
organizationId: scope.organizationId
|
|
17862
|
-
});
|
|
17863
|
-
dispatch$1(app.notifier, notification);
|
|
17864
|
-
return reply.status(201).send(serializeTask(task));
|
|
17865
|
-
});
|
|
17866
|
-
}
|
|
17867
17399
|
async function loadVisibleAgentIds(db, organizationId, memberId) {
|
|
17868
17400
|
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))));
|
|
17869
17401
|
return new Set(rows.map((r) => r.id));
|
|
@@ -18084,44 +17616,6 @@ async function sessionRoutes(app) {
|
|
|
18084
17616
|
});
|
|
18085
17617
|
});
|
|
18086
17618
|
}
|
|
18087
|
-
function dispatch(notifier, result) {
|
|
18088
|
-
if (!result) return;
|
|
18089
|
-
notifyRecipients(notifier, result.recipients, result.message.id);
|
|
18090
|
-
}
|
|
18091
|
-
/** Class C — `/api/v1/tasks/:taskId`. The task's `organizationId` locates the org. */
|
|
18092
|
-
async function taskRoutes(app) {
|
|
18093
|
-
app.get("/:taskId", async (request) => {
|
|
18094
|
-
await requireTaskAccess(request, app.db);
|
|
18095
|
-
const detail = await getTaskDetail(app.db, request.params.taskId);
|
|
18096
|
-
return {
|
|
18097
|
-
...serializeTask(detail),
|
|
18098
|
-
chats: detail.chats
|
|
18099
|
-
};
|
|
18100
|
-
});
|
|
18101
|
-
app.patch("/:taskId", async (request) => {
|
|
18102
|
-
const { scope } = await requireTaskAccess(request, app.db);
|
|
18103
|
-
const body = adminUpdateTaskSchema.parse(request.body);
|
|
18104
|
-
const { task, notification } = await adminUpdateTask(app.db, request.params.taskId, {
|
|
18105
|
-
type: "admin",
|
|
18106
|
-
adminId: scope.memberId
|
|
18107
|
-
}, body);
|
|
18108
|
-
dispatch(app.notifier, notification);
|
|
18109
|
-
return serializeTask(task);
|
|
18110
|
-
});
|
|
18111
|
-
app.post("/:taskId/cancel", async (request) => {
|
|
18112
|
-
const { scope } = await requireTaskAccess(request, app.db);
|
|
18113
|
-
const { task, notification } = await cancelTask(app.db, request.params.taskId, {
|
|
18114
|
-
type: "admin",
|
|
18115
|
-
adminId: scope.memberId
|
|
18116
|
-
});
|
|
18117
|
-
dispatch(app.notifier, notification);
|
|
18118
|
-
return serializeTask(task);
|
|
18119
|
-
});
|
|
18120
|
-
app.get("/:taskId/health", async (request) => {
|
|
18121
|
-
await requireTaskAccess(request, app.db);
|
|
18122
|
-
return getTaskHealth(app.db, request.params.taskId);
|
|
18123
|
-
});
|
|
18124
|
-
}
|
|
18125
17619
|
const log$1 = createLogger$1("GithubWebhook");
|
|
18126
17620
|
const GITHUB_ADAPTER_ID = "github-adapter";
|
|
18127
17621
|
function verifySignature(secret, rawBody, signatureHeader) {
|
|
@@ -18187,6 +17681,31 @@ function extractMentions$1(text) {
|
|
|
18187
17681
|
}
|
|
18188
17682
|
return [...names];
|
|
18189
17683
|
}
|
|
17684
|
+
/** Extract mentions from structural payload fields (not free-form text).
|
|
17685
|
+
* GitHub's `pull_request.review_requested` puts the targeted reviewer in
|
|
17686
|
+
* `requested_reviewer.login`, not in any text body — `extractMentions` would
|
|
17687
|
+
* miss it. Team requests use `requested_team` instead, which we deliberately
|
|
17688
|
+
* skip to stay consistent with `extractMentions` ignoring `@org/team`. */
|
|
17689
|
+
function extractStructuralMentions(eventType, payload) {
|
|
17690
|
+
if (!isRecord(payload)) return [];
|
|
17691
|
+
if (eventType !== "pull_request") return [];
|
|
17692
|
+
if (payload.action !== "review_requested") return [];
|
|
17693
|
+
const reviewer = isRecord(payload.requested_reviewer) ? payload.requested_reviewer : null;
|
|
17694
|
+
const login = typeof reviewer?.login === "string" ? reviewer.login : null;
|
|
17695
|
+
return login ? [login.toLowerCase()] : [];
|
|
17696
|
+
}
|
|
17697
|
+
const DELEGATE_VERDICT_MESSAGES = {
|
|
17698
|
+
ok: "delegate_mention target eligible",
|
|
17699
|
+
not_found: "delegate_mention target not found, skipping",
|
|
17700
|
+
cross_org: "delegate_mention target belongs to another org, skipping",
|
|
17701
|
+
inactive: "delegate_mention target not active, skipping"
|
|
17702
|
+
};
|
|
17703
|
+
function evaluateDelegateTarget(target, sourceOrgId) {
|
|
17704
|
+
if (!target) return "not_found";
|
|
17705
|
+
if (target.organizationId !== sourceOrgId) return "cross_org";
|
|
17706
|
+
if (target.status !== "active") return "inactive";
|
|
17707
|
+
return "ok";
|
|
17708
|
+
}
|
|
18190
17709
|
/**
|
|
18191
17710
|
* Route @mentions to delegate agents.
|
|
18192
17711
|
* For each mentioned user who has delegate_mention configured,
|
|
@@ -18205,13 +17724,19 @@ async function routeMentionDelegations(app, organizationId, mentionedNames, ctx)
|
|
|
18205
17724
|
if (agent.status !== "active" || !agent.delegateMention) continue;
|
|
18206
17725
|
const [target] = await app.db.select({
|
|
18207
17726
|
id: agents.uuid,
|
|
18208
|
-
status: agents.status
|
|
17727
|
+
status: agents.status,
|
|
17728
|
+
organizationId: agents.organizationId
|
|
18209
17729
|
}).from(agents).where(eq(agents.uuid, agent.delegateMention)).limit(1);
|
|
18210
|
-
|
|
17730
|
+
const verdict = evaluateDelegateTarget(target, organizationId);
|
|
17731
|
+
if (verdict !== "ok") {
|
|
18211
17732
|
log$1.warn({
|
|
18212
17733
|
targetAgent: agent.delegateMention,
|
|
18213
|
-
sourceAgent: agent.name
|
|
18214
|
-
|
|
17734
|
+
sourceAgent: agent.name,
|
|
17735
|
+
sourceOrg: organizationId,
|
|
17736
|
+
targetOrg: target?.organizationId,
|
|
17737
|
+
targetStatus: target?.status,
|
|
17738
|
+
verdict
|
|
17739
|
+
}, DELEGATE_VERDICT_MESSAGES[verdict]);
|
|
18215
17740
|
continue;
|
|
18216
17741
|
}
|
|
18217
17742
|
try {
|
|
@@ -18222,6 +17747,7 @@ async function routeMentionDelegations(app, organizationId, mentionedNames, ctx)
|
|
|
18222
17747
|
type: "github_mention",
|
|
18223
17748
|
mentionedUser: agent.name,
|
|
18224
17749
|
event: ctx.event,
|
|
17750
|
+
action: ctx.action,
|
|
18225
17751
|
repository: ctx.repository,
|
|
18226
17752
|
sender: ctx.sender,
|
|
18227
17753
|
title: ctx.title,
|
|
@@ -18231,7 +17757,8 @@ async function routeMentionDelegations(app, organizationId, mentionedNames, ctx)
|
|
|
18231
17757
|
metadata: {
|
|
18232
17758
|
source: "github",
|
|
18233
17759
|
event: "mention_delegation",
|
|
18234
|
-
mentionedUser: agent.name
|
|
17760
|
+
mentionedUser: agent.name,
|
|
17761
|
+
action: ctx.action
|
|
18235
17762
|
}
|
|
18236
17763
|
});
|
|
18237
17764
|
notifyRecipients(app.notifier, recipients, msg.id);
|
|
@@ -18388,12 +17915,14 @@ function extractEventContext(eventType, payload) {
|
|
|
18388
17915
|
const sender = isRecord(payload.sender) ? payload.sender : null;
|
|
18389
17916
|
const repository = typeof repo?.full_name === "string" ? repo.full_name : "";
|
|
18390
17917
|
const senderLogin = typeof sender?.login === "string" ? sender.login : "";
|
|
17918
|
+
const action = typeof payload.action === "string" ? payload.action : void 0;
|
|
18391
17919
|
switch (eventType) {
|
|
18392
17920
|
case "issues": {
|
|
18393
17921
|
const issue = isRecord(payload.issue) ? payload.issue : null;
|
|
18394
17922
|
if (!issue) return null;
|
|
18395
17923
|
return {
|
|
18396
17924
|
event: "issues",
|
|
17925
|
+
action,
|
|
18397
17926
|
repository,
|
|
18398
17927
|
sender: senderLogin,
|
|
18399
17928
|
title: `Issue #${issue.number}: ${issue.title}`,
|
|
@@ -18407,6 +17936,7 @@ function extractEventContext(eventType, payload) {
|
|
|
18407
17936
|
if (!issue || !comment) return null;
|
|
18408
17937
|
return {
|
|
18409
17938
|
event: "issue_comment",
|
|
17939
|
+
action,
|
|
18410
17940
|
repository,
|
|
18411
17941
|
sender: senderLogin,
|
|
18412
17942
|
title: `Issue #${issue.number}: ${issue.title}`,
|
|
@@ -18419,6 +17949,7 @@ function extractEventContext(eventType, payload) {
|
|
|
18419
17949
|
if (!pr) return null;
|
|
18420
17950
|
return {
|
|
18421
17951
|
event: "pull_request",
|
|
17952
|
+
action,
|
|
18422
17953
|
repository,
|
|
18423
17954
|
sender: senderLogin,
|
|
18424
17955
|
title: `PR #${pr.number}: ${pr.title}`,
|
|
@@ -18432,6 +17963,7 @@ function extractEventContext(eventType, payload) {
|
|
|
18432
17963
|
if (!pr || !review) return null;
|
|
18433
17964
|
return {
|
|
18434
17965
|
event: "pull_request_review",
|
|
17966
|
+
action,
|
|
18435
17967
|
repository,
|
|
18436
17968
|
sender: senderLogin,
|
|
18437
17969
|
title: `PR #${pr.number}: ${pr.title}`,
|
|
@@ -18445,6 +17977,7 @@ function extractEventContext(eventType, payload) {
|
|
|
18445
17977
|
if (!pr || !comment) return null;
|
|
18446
17978
|
return {
|
|
18447
17979
|
event: "pull_request_review_comment",
|
|
17980
|
+
action,
|
|
18448
17981
|
repository,
|
|
18449
17982
|
sender: senderLogin,
|
|
18450
17983
|
title: `PR #${pr.number}: ${pr.title}`,
|
|
@@ -18457,6 +17990,7 @@ function extractEventContext(eventType, payload) {
|
|
|
18457
17990
|
if (!disc) return null;
|
|
18458
17991
|
return {
|
|
18459
17992
|
event: "discussion",
|
|
17993
|
+
action,
|
|
18460
17994
|
repository,
|
|
18461
17995
|
sender: senderLogin,
|
|
18462
17996
|
title: typeof disc.title === "string" ? disc.title : "",
|
|
@@ -18470,6 +18004,7 @@ function extractEventContext(eventType, payload) {
|
|
|
18470
18004
|
if (!disc || !comment) return null;
|
|
18471
18005
|
return {
|
|
18472
18006
|
event: "discussion_comment",
|
|
18007
|
+
action,
|
|
18473
18008
|
repository,
|
|
18474
18009
|
sender: senderLogin,
|
|
18475
18010
|
title: typeof disc.title === "string" ? disc.title : "",
|
|
@@ -18482,6 +18017,7 @@ function extractEventContext(eventType, payload) {
|
|
|
18482
18017
|
if (!comment) return null;
|
|
18483
18018
|
return {
|
|
18484
18019
|
event: "commit_comment",
|
|
18020
|
+
action,
|
|
18485
18021
|
repository,
|
|
18486
18022
|
sender: senderLogin,
|
|
18487
18023
|
title: "Commit comment",
|
|
@@ -18497,16 +18033,26 @@ function extractEventContext(eventType, payload) {
|
|
|
18497
18033
|
* Only called after action gating confirms this is a "new content" event.
|
|
18498
18034
|
*/
|
|
18499
18035
|
async function handleMentionDelegation(app, organizationId, eventType, payload) {
|
|
18500
|
-
const
|
|
18036
|
+
const textMentions = extractMentions$1(extractEventText(eventType, payload));
|
|
18037
|
+
const structuralMentions = extractStructuralMentions(eventType, payload);
|
|
18038
|
+
const mentions = [...new Set([...textMentions, ...structuralMentions])];
|
|
18501
18039
|
const mentionCtx = extractEventContext(eventType, payload);
|
|
18502
18040
|
if (mentions.length > 0 && mentionCtx) return routeMentionDelegations(app, organizationId, mentions, mentionCtx);
|
|
18503
18041
|
return 0;
|
|
18504
18042
|
}
|
|
18505
|
-
/** Actions that represent new/changed content (worth scanning for @mentions).
|
|
18043
|
+
/** Actions that represent new/changed content (worth scanning for @mentions).
|
|
18044
|
+
* Note: `pull_request.review_requested` doesn't carry an @mention in any
|
|
18045
|
+
* text body — the reviewer is in `requested_reviewer.login`. We pick it up
|
|
18046
|
+
* via `extractStructuralMentions`. The complementary `review_request_removed`
|
|
18047
|
+
* is intentionally omitted to avoid notifying the reviewer twice. */
|
|
18506
18048
|
const MENTION_ACTIONS = {
|
|
18507
18049
|
issues: ["opened", "edited"],
|
|
18508
18050
|
issue_comment: ["created"],
|
|
18509
|
-
pull_request: [
|
|
18051
|
+
pull_request: [
|
|
18052
|
+
"opened",
|
|
18053
|
+
"edited",
|
|
18054
|
+
"review_requested"
|
|
18055
|
+
],
|
|
18510
18056
|
pull_request_review: ["submitted"],
|
|
18511
18057
|
pull_request_review_comment: ["created"],
|
|
18512
18058
|
discussion: ["created", "edited"],
|
|
@@ -18656,8 +18202,6 @@ var schema_exports = /* @__PURE__ */ __exportAll({
|
|
|
18656
18202
|
pendingQuestions: () => pendingQuestions,
|
|
18657
18203
|
serverInstances: () => serverInstances,
|
|
18658
18204
|
sessionEvents: () => sessionEvents,
|
|
18659
|
-
taskChats: () => taskChats,
|
|
18660
|
-
tasks: () => tasks,
|
|
18661
18205
|
users: () => users
|
|
18662
18206
|
});
|
|
18663
18207
|
function connectDatabase(url) {
|
|
@@ -20150,7 +19694,6 @@ async function buildApp(config) {
|
|
|
20150
19694
|
await scope.register(orgAdapterStatusRoutes, { prefix: "/adapters/status" });
|
|
20151
19695
|
await scope.register(orgOverviewRoutes, { prefix: "/overview" });
|
|
20152
19696
|
await scope.register(orgActivityRoutes, { prefix: "/activity" });
|
|
20153
|
-
await scope.register(orgTaskRoutes, { prefix: "/tasks" });
|
|
20154
19697
|
await scope.register(orgSessionRoutes, { prefix: "/sessions" });
|
|
20155
19698
|
await scope.register(orgNotificationRoutes, { prefix: "/notifications" });
|
|
20156
19699
|
await scope.register(orgClientRoutes, { prefix: "/clients" });
|
|
@@ -20166,7 +19709,6 @@ async function buildApp(config) {
|
|
|
20166
19709
|
await scope.register(agentActivityRoutes, { prefix: "/agents" });
|
|
20167
19710
|
await scope.register(sessionRoutes, { prefix: "/agents" });
|
|
20168
19711
|
await scope.register(chatRoutes, { prefix: "/chats" });
|
|
20169
|
-
await scope.register(taskRoutes, { prefix: "/tasks" });
|
|
20170
19712
|
await scope.register(adapterRoutes, { prefix: "/adapters" });
|
|
20171
19713
|
await scope.register(adapterMappingRoutes, { prefix: "/adapter-mappings" });
|
|
20172
19714
|
await scope.register(clientRoutes, { prefix: "/clients" });
|
|
@@ -20178,7 +19720,6 @@ async function buildApp(config) {
|
|
|
20178
19720
|
await scope.register(agentSendToAgentRoutes, { prefix: "/agents" });
|
|
20179
19721
|
await scope.register(agentInboxRoutes, { prefix: "/inbox" });
|
|
20180
19722
|
await scope.register(agentConfigRoutes$1);
|
|
20181
|
-
await scope.register(agentTaskRoutes, { prefix: "/tasks" });
|
|
20182
19723
|
await scope.register(agentFeishuBotRoutes);
|
|
20183
19724
|
await scope.register(agentFeishuUserRoutes, { prefix: "/delegated" });
|
|
20184
19725
|
}), { prefix: "/agent" });
|