diff --git a/hindsight-integrations/paperclip/src/bank.ts b/hindsight-integrations/paperclip/src/bank.ts index 99ebd40b9..8ba9344ef 100644 --- a/hindsight-integrations/paperclip/src/bank.ts +++ b/hindsight-integrations/paperclip/src/bank.ts @@ -3,18 +3,20 @@ * * Default format: "paperclip::{companyId}::{agentId}" * - * bankGranularity: ['company'] → "paperclip::{companyId}" - * bankGranularity: ['agent'] → "paperclip::{agentId}" - * bankGranularity: ['company','agent'] → "paperclip::{companyId}::{agentId}" + * bankGranularity: ['company'] → "paperclip::{companyId}" + * bankGranularity: ['agent'] → "paperclip::{agentId}" + * bankGranularity: ['company','agent'] → "paperclip::{companyId}::{agentId}" + * bankGranularity: ['company','agent','user'] → "paperclip::{companyId}::{agentId}::user::{userId}" */ export interface BankContext { companyId: string; agentId: string; + userId?: string; } export interface BankConfig { - bankGranularity?: Array<"company" | "agent">; + bankGranularity?: Array<"company" | "agent" | "user">; } export function deriveBankId(context: BankContext, config: BankConfig): string { @@ -24,6 +26,10 @@ export function deriveBankId(context: BankContext, config: BankConfig): string { for (const field of granularity) { if (field === "company") parts.push(context.companyId); if (field === "agent") parts.push(context.agentId); + if (field === "user" && context.userId) { + parts.push("user"); + parts.push(context.userId); + } } return parts.join("::"); diff --git a/hindsight-integrations/paperclip/src/manifest.ts b/hindsight-integrations/paperclip/src/manifest.ts index 21e99f49d..ae92641f7 100644 --- a/hindsight-integrations/paperclip/src/manifest.ts +++ b/hindsight-integrations/paperclip/src/manifest.ts @@ -17,6 +17,7 @@ const manifest: PaperclipPluginManifestV1 = { "http.outbound", "secrets.read-ref", "agents.read", + "issues.read", ], entrypoints: { worker: "./dist/worker.js", @@ -42,8 +43,8 @@ const manifest: PaperclipPluginManifestV1 = { type: "array", title: "Bank Granularity", description: - "Controls memory isolation. Default ['company', 'agent'] gives each agent its own bank per company.", - items: { type: "string", enum: ["company", "agent"] }, + "Controls memory isolation. Default ['company', 'agent'] gives each agent its own bank per company. Add 'user' for per-user memory isolation (GDPR compliance).", + items: { type: "string", enum: ["company", "agent", "user"] }, default: ["company", "agent"], }, recallBudget: { diff --git a/hindsight-integrations/paperclip/src/worker.ts b/hindsight-integrations/paperclip/src/worker.ts index 85e41e0cd..2b04fb357 100644 --- a/hindsight-integrations/paperclip/src/worker.ts +++ b/hindsight-integrations/paperclip/src/worker.ts @@ -13,14 +13,14 @@ */ import { definePlugin, runWorker } from "@paperclipai/plugin-sdk"; -import type { ToolRunContext } from "@paperclipai/plugin-sdk"; +import type { PluginContext, ToolRunContext } from "@paperclipai/plugin-sdk"; import { HindsightClient, formatMemories } from "./client.js"; import { deriveBankId } from "./bank.js"; interface PluginConfig { hindsightApiUrl: string; hindsightApiKeyRef?: string; - bankGranularity?: Array<"company" | "agent">; + bankGranularity?: Array<"company" | "agent" | "user">; recallBudget?: "low" | "mid" | "high"; autoRetain?: boolean; } @@ -54,6 +54,41 @@ async function resolveApiKey( return resolved ?? undefined; } +async function resolveUserIdFromActiveIssue( + ctx: PluginContext, + companyId: string, + agentId: string, + config: PluginConfig +): Promise { + // Only resolve user ID if user granularity is enabled + if (!config.bankGranularity?.includes("user")) return undefined; + + try { + const issues = await ctx.issues.list({ + companyId, + assigneeAgentId: agentId, + status: "in_progress", + limit: 1, + }); + if (!issues || issues.length === 0) return undefined; + + const originId = issues[0]?.originId; + if (!originId) return undefined; + + // Format: "channel-key::user-email" — extract the email + // Search backwards for a part that looks like an email (contains "@") + const parts = originId.split("::"); + for (let i = parts.length - 1; i >= 0; i--) { + if (parts[i].includes("@")) { + return parts[i]; + } + } + return undefined; + } catch (err) { + return undefined; + } +} + const plugin = definePlugin({ async setup(ctx) { ctx.logger.info("Hindsight memory plugin starting"); @@ -66,6 +101,7 @@ const plugin = definePlugin({ const config = await getConfig(ctx); const { agentId, runId, issueTitle, issueDescription } = payload; const companyId = event.companyId; + const userId = await resolveUserIdFromActiveIssue(ctx, companyId, agentId, config); const query = [issueTitle, issueDescription].filter(Boolean).join("\n"); if (!query.trim()) return; @@ -73,7 +109,7 @@ const plugin = definePlugin({ try { const apiKey = await resolveApiKey(ctx, config); const client = new HindsightClient(config.hindsightApiUrl, apiKey); - const bankId = deriveBankId({ companyId, agentId }, config); + const bankId = deriveBankId({ companyId, agentId, userId }, config); const response = await client.recall(bankId, query, config.recallBudget ?? "mid"); @@ -109,6 +145,7 @@ const plugin = definePlugin({ const { agentId, runId, output, result } = payload; const companyId = event.companyId; + const userId = await resolveUserIdFromActiveIssue(ctx, companyId, agentId, config); const content = output ?? result; if (!content?.trim()) return; @@ -116,7 +153,7 @@ const plugin = definePlugin({ try { const apiKey = await resolveApiKey(ctx, config); const client = new HindsightClient(config.hindsightApiUrl, apiKey); - const bankId = deriveBankId({ companyId, agentId }, config); + const bankId = deriveBankId({ companyId, agentId, userId }, config); await client.retain(bankId, content, runId, { agentId, companyId, runId }); ctx.logger.info("Retained run output to memory", { runId, bankId }); @@ -147,8 +184,9 @@ const plugin = definePlugin({ async (params: unknown, runCtx: ToolRunContext) => { const { query } = params as { query: string }; const config = await getConfig(ctx); + const userId = await resolveUserIdFromActiveIssue(ctx, runCtx.companyId, runCtx.agentId, config); const bankId = deriveBankId( - { companyId: runCtx.companyId, agentId: runCtx.agentId }, + { companyId: runCtx.companyId, agentId: runCtx.agentId, userId }, config ); @@ -198,8 +236,9 @@ const plugin = definePlugin({ async (params: unknown, runCtx: ToolRunContext) => { const { content } = params as { content: string }; const config = await getConfig(ctx); + const userId = await resolveUserIdFromActiveIssue(ctx, runCtx.companyId, runCtx.agentId, config); const bankId = deriveBankId( - { companyId: runCtx.companyId, agentId: runCtx.agentId }, + { companyId: runCtx.companyId, agentId: runCtx.agentId, userId }, config );