diff --git a/dist/fabric/src/inbound.js b/dist/fabric/src/inbound.js index ac716e7..e163985 100644 --- a/dist/fabric/src/inbound.js +++ b/dist/fabric/src/inbound.js @@ -12,6 +12,35 @@ export class FabricInbound { accounts; sockets = []; seen = new Set(); + // Guild access tokens are short-lived (~15 min). The socket survives via + // socket.io reconnect, but the token captured at connect time goes stale, + // so HTTP calls (attachment download, posting the reply) start 401ing. + // Re-login per agent on a short TTL to keep a fresh token. + tokenCache = new Map(); + static TOKEN_TTL_MS = 8 * 60 * 1000; + // Return a fresh guild access token for the agent, re-authenticating with + // the agent's Fabric API key when the cached session is stale. Falls back + // to the connect-time session token if re-login fails. + async freshGuildToken(agentId, guildNodeId, fallback) { + const pick = (s) => s.guildAccessTokens.find((t) => t.guildNodeId === guildNodeId)?.token; + const now = Date.now(); + const cached = this.tokenCache.get(agentId); + if (cached && now - cached.at < FabricInbound.TOKEN_TTL_MS) { + return pick(cached.session) ?? pick(fallback); + } + const apiKey = this.identity.findByAgentId(agentId)?.fabricApiKey; + if (apiKey) { + try { + const s = await this.client.agentLogin(apiKey); + this.tokenCache.set(agentId, { session: s, at: now }); + return pick(s) ?? pick(fallback); + } + catch (err) { + this.log.warn(`fabric: token refresh failed agent=${agentId}: ${String(err)}`); + } + } + return pick(fallback); + } constructor(core, // PluginRuntime cfg, // OpenClawConfig client, identity, log, accounts = []) { @@ -151,7 +180,7 @@ export class FabricInbound { const storePath = core.channel.session.resolveStorePath(cfg.session?.store, { agentId: route.agentId, }); - const gt = session.guildAccessTokens.find((t) => t.guildNodeId === guild.nodeId)?.token; + const gt = await this.freshGuildToken(agentId, guild.nodeId, session); // Fetch any uploaded files for the agent: download to a temp dir and // hand openclaw local MediaPaths (+types) so the model receives them. const media = await this.fetchAttachments(agentId, guild.endpoint, gt, m); diff --git a/src/inbound.ts b/src/inbound.ts index 911e783..3f5fc18 100644 --- a/src/inbound.ts +++ b/src/inbound.ts @@ -38,6 +38,40 @@ type FabricMessage = { export class FabricInbound { private sockets: Socket[] = []; private seen = new Set(); + // Guild access tokens are short-lived (~15 min). The socket survives via + // socket.io reconnect, but the token captured at connect time goes stale, + // so HTTP calls (attachment download, posting the reply) start 401ing. + // Re-login per agent on a short TTL to keep a fresh token. + private tokenCache = new Map(); + private static readonly TOKEN_TTL_MS = 8 * 60 * 1000; + + // Return a fresh guild access token for the agent, re-authenticating with + // the agent's Fabric API key when the cached session is stale. Falls back + // to the connect-time session token if re-login fails. + private async freshGuildToken( + agentId: string, + guildNodeId: string, + fallback: FabricSession, + ): Promise { + const pick = (s: FabricSession) => + s.guildAccessTokens.find((t) => t.guildNodeId === guildNodeId)?.token; + const now = Date.now(); + const cached = this.tokenCache.get(agentId); + if (cached && now - cached.at < FabricInbound.TOKEN_TTL_MS) { + return pick(cached.session) ?? pick(fallback); + } + const apiKey = this.identity.findByAgentId(agentId)?.fabricApiKey; + if (apiKey) { + try { + const s = await this.client.agentLogin(apiKey); + this.tokenCache.set(agentId, { session: s, at: now }); + return pick(s) ?? pick(fallback); + } catch (err) { + this.log.warn(`fabric: token refresh failed agent=${agentId}: ${String(err)}`); + } + } + return pick(fallback); + } constructor( private readonly core: unknown, // PluginRuntime @@ -188,7 +222,7 @@ export class FabricInbound { agentId: route.agentId, }); - const gt = session.guildAccessTokens.find((t) => t.guildNodeId === guild.nodeId)?.token; + const gt = await this.freshGuildToken(agentId, guild.nodeId, session); // Fetch any uploaded files for the agent: download to a temp dir and // hand openclaw local MediaPaths (+types) so the model receives them.