{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "customer-memory-agent",
  "title": "Memory & Persistence Agent",
  "description": "A support workspace with in-memory default persistence, optional Postgres storage, explicit saved memories, retrieval, and compaction checkpoints.",
  "dependencies": [
    "@ai-sdk/react",
    "@base-ui/react",
    "@neondatabase/serverless",
    "@phosphor-icons/react",
    "ai",
    "class-variance-authority",
    "drizzle-orm",
    "lucide-react",
    "zod"
  ],
  "registryDependencies": [
    "utils",
    "badge",
    "breadcrumb",
    "button",
    "skeleton",
    "textarea",
    "https://elements.ai-sdk.dev/api/registry/checkpoint.json",
    "https://elements.ai-sdk.dev/api/registry/conversation.json",
    "https://elements.ai-sdk.dev/api/registry/message.json",
    "https://elements.ai-sdk.dev/api/registry/shimmer.json",
    "https://elements.ai-sdk.dev/api/registry/tool.json"
  ],
  "files": [
    {
      "path": "registry/customer-memory-agent/app/demos/customer-memory-agent/page.tsx",
      "content": "import { CustomerMemoryAgentScreen } from \"@/components/customer-memory-agent/customer-memory-agent-screen\";\n\nexport default function CustomerMemoryAgentPage() {\n  return <CustomerMemoryAgentScreen />;\n}\n",
      "type": "registry:page",
      "target": "app/demos/customer-memory-agent/page.tsx"
    },
    {
      "path": "registry/customer-memory-agent/app/api/demos/customer-memory-agent/route.ts",
      "content": "import { getCustomerMemoryAgentEnv } from \"@/lib/customer-memory-agent/env\";\nimport { handleCustomerMemoryChatRequest } from \"@/lib/customer-memory-agent/runtime\";\nimport {\n  buildCustomerMemoryVisitorCookie,\n  getOrCreateCustomerMemoryVisitorId,\n} from \"@/lib/customer-memory-agent/viewer-context\";\n\nexport const maxDuration = 30;\n\nexport async function POST(request: Request) {\n  const visitor = getOrCreateCustomerMemoryVisitorId(request);\n  const response = await handleCustomerMemoryChatRequest(\n    request,\n    {\n      isReadonly: false,\n      visitorId: visitor.visitorId,\n    },\n    getCustomerMemoryAgentEnv(),\n    undefined\n  );\n\n  if (visitor.shouldSetCookie) {\n    response.headers.append(\n      \"Set-Cookie\",\n      buildCustomerMemoryVisitorCookie(visitor.visitorId)\n    );\n  }\n\n  return response;\n}\n",
      "type": "registry:page",
      "target": "app/api/demos/customer-memory-agent/route.ts"
    },
    {
      "path": "registry/customer-memory-agent/app/api/demos/customer-memory-agent/session/route.ts",
      "content": "import { getCustomerMemoryAgentEnv } from \"@/lib/customer-memory-agent/env\";\nimport { handleCustomerMemorySessionRequest } from \"@/lib/customer-memory-agent/session-runtime\";\nimport {\n  buildCustomerMemoryVisitorCookie,\n  getOrCreateCustomerMemoryVisitorId,\n} from \"@/lib/customer-memory-agent/viewer-context\";\n\nexport async function GET(request: Request) {\n  const visitor = getOrCreateCustomerMemoryVisitorId(request);\n  const response = await handleCustomerMemorySessionRequest(\n    request,\n    {\n      isReadonly: false,\n      visitorId: visitor.visitorId,\n    },\n    getCustomerMemoryAgentEnv(),\n    {}\n  );\n\n  if (visitor.shouldSetCookie) {\n    response.headers.append(\n      \"Set-Cookie\",\n      buildCustomerMemoryVisitorCookie(visitor.visitorId)\n    );\n  }\n\n  return response;\n}\n",
      "type": "registry:page",
      "target": "app/api/demos/customer-memory-agent/session/route.ts"
    },
    {
      "path": "registry/customer-memory-agent/app/api/demos/customer-memory-agent/threads/route.ts",
      "content": "import { getCustomerMemoryAgentEnv } from \"@/lib/customer-memory-agent/env\";\nimport { handleCustomerMemoryThreadCreateRequest } from \"@/lib/customer-memory-agent/session-runtime\";\nimport {\n  buildCustomerMemoryVisitorCookie,\n  getOrCreateCustomerMemoryVisitorId,\n} from \"@/lib/customer-memory-agent/viewer-context\";\n\nexport async function POST(request: Request) {\n  const visitor = getOrCreateCustomerMemoryVisitorId(request);\n  const response = await handleCustomerMemoryThreadCreateRequest(\n    request,\n    {\n      isReadonly: false,\n      visitorId: visitor.visitorId,\n    },\n    getCustomerMemoryAgentEnv(),\n    {}\n  );\n\n  if (visitor.shouldSetCookie) {\n    response.headers.append(\n      \"Set-Cookie\",\n      buildCustomerMemoryVisitorCookie(visitor.visitorId)\n    );\n  }\n\n  return response;\n}\n",
      "type": "registry:page",
      "target": "app/api/demos/customer-memory-agent/threads/route.ts"
    },
    {
      "path": "registry/customer-memory-agent/components/customer-memory-agent/customer-memory-agent-screen.tsx",
      "content": "import { Badge } from \"@/components/ui/badge\";\nimport {\n  Breadcrumb,\n  BreadcrumbItem,\n  BreadcrumbLink,\n  BreadcrumbList,\n  BreadcrumbPage,\n  BreadcrumbSeparator,\n} from \"@/components/ui/breadcrumb\";\nimport { ArrowLeft } from \"lucide-react\";\n\nimport { customerMemoryProfiles } from \"@/lib/customer-memory-agent/customer-profiles\";\nimport { getCustomerMemoryRuntimeState } from \"@/lib/customer-memory-agent/runtime\";\nimport { CustomerMemoryAgentWorkspace } from \"./customer-memory-agent-workspace\";\n\nexport function CustomerMemoryAgentScreen() {\n  const runtimeState = getCustomerMemoryRuntimeState();\n\n  return (\n    <main className=\"min-h-svh bg-background text-foreground\">\n      <div className=\"mx-auto flex w-full max-w-[92rem] flex-col gap-6 px-4 py-6 md:px-6\">\n        <header className=\"grid gap-4 border border-foreground/10 bg-background px-4 py-5 md:grid-cols-[minmax(0,1fr)_auto] md:items-end\">\n          <div className=\"space-y-2\">\n            <Breadcrumb>\n              <BreadcrumbList className=\"text-[11px] text-muted-foreground uppercase tracking-[0.2em]\">\n                <BreadcrumbItem>\n                  <BreadcrumbLink\n                    aria-label=\"Back to demos\"\n                    className=\"-ml-1 inline-flex items-center gap-1.5 text-muted-foreground transition-colors hover:text-foreground\"\n                    href=\"/\"\n                  >\n                    <ArrowLeft aria-hidden=\"true\" className=\"size-3.5 shrink-0\" />\n                    <span>Demo</span>\n                  </BreadcrumbLink>\n                </BreadcrumbItem>\n                <BreadcrumbSeparator className=\"text-muted-foreground\">\n                  /\n                </BreadcrumbSeparator>\n                <BreadcrumbItem>\n                  <BreadcrumbPage className=\"font-normal text-muted-foreground\">\n                    Memory & Persistence Agent\n                  </BreadcrumbPage>\n                </BreadcrumbItem>\n              </BreadcrumbList>\n            </Breadcrumb>\n            <h1 className=\"max-w-4xl font-medium text-2xl tracking-tight\">\n              Persist threads, explicit memories, and handoff compactions in a\n              portable workspace\n            </h1>\n            <p className=\"max-w-4xl text-muted-foreground text-sm/relaxed\">\n              This Batch 6 workspace shows the long-lived agent layer: the chat\n              thread is restored from in-memory storage by default, the agent\n              explicitly saves durable memories through a tool call, and older\n              context is compacted into a handoff checkpoint once the message\n              threshold is crossed. Add Postgres when you want durable storage\n              across server restarts.\n            </p>\n          </div>\n\n          <div className=\"flex flex-wrap items-center gap-2\">\n            <Badge variant=\"outline\">{runtimeState.statusLabel}</Badge>\n            <Badge variant=\"outline\">{runtimeState.persistenceLabel}</Badge>\n            <Badge variant=\"outline\">{runtimeState.chatModel}</Badge>\n            <Badge variant=\"outline\">\n              {runtimeState.compactionThreshold} messages to compact\n            </Badge>\n          </div>\n        </header>\n\n        <div className=\"xl:h-svh\">\n          <CustomerMemoryAgentWorkspace\n            chatModel={runtimeState.chatModel}\n            compactionThreshold={runtimeState.compactionThreshold}\n            customers={customerMemoryProfiles}\n            isChatAvailable={runtimeState.isChatAvailable}\n            nodeVersion={runtimeState.nodeVersion}\n            setupMessage={runtimeState.setupMessage}\n          />\n        </div>\n      </div>\n    </main>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/customer-memory-agent/customer-memory-agent-screen.tsx"
    },
    {
      "path": "registry/customer-memory-agent/components/customer-memory-agent/customer-memory-agent-workspace.tsx",
      "content": "\"use client\";\n\nimport {\n  ArrowClockwiseIcon,\n  DatabaseIcon,\n  PlusIcon,\n  RobotIcon,\n  ScrollIcon,\n  StopIcon,\n} from \"@phosphor-icons/react\";\nimport { Checkpoint } from \"@/components/ai-elements/checkpoint\";\nimport {\n  Conversation,\n  ConversationContent,\n  ConversationEmptyState,\n  ConversationScrollButton,\n} from \"@/components/ai-elements/conversation\";\nimport {\n  Message,\n  MessageContent,\n  MessageResponse,\n} from \"@/components/ai-elements/message\";\nimport {\n  PromptInput,\n  PromptInputBody,\n  PromptInputFooter,\n  PromptInputSubmit,\n  PromptInputTextarea,\n} from \"@/components/ai-elements/prompt-input\";\nimport { Shimmer } from \"@/components/ai-elements/shimmer\";\nimport {\n  Tool,\n  ToolContent,\n  ToolHeader,\n  ToolInput,\n  ToolOutput,\n  type ToolPart,\n} from \"@/components/ai-elements/tool\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\nimport { cn } from \"@/lib/utils\";\nimport type { ChatStatus, UIMessage } from \"ai\";\nimport { useMemo } from \"react\";\n\nimport type { CustomerMemoryProfile } from \"@/lib/customer-memory-agent/customer-profiles\";\nimport type { CustomerMemorySessionData } from \"@/lib/customer-memory-agent/session-data\";\nimport {\n  buildCustomerMemoryThreadLabel,\n  formatCustomerMemoryCategory,\n  getCustomerMemoryMessageText,\n  getCustomerMemoryPendingCompaction,\n  getCustomerMemorySamplePrompts,\n  getCustomerMemoryToolParts,\n  hasCustomerMemoryMessageContent,\n} from \"./customer-memory-session\";\nimport { useCustomerMemorySession } from \"./use-customer-memory-session\";\n\nconst NODE_VERSION_PREFIX_PATTERN = /^v/;\nconst THREAD_SKELETON_KEYS = [\n  \"thread-skeleton-primary\",\n  \"thread-skeleton-secondary\",\n  \"thread-skeleton-tertiary\",\n] as const;\n\nfunction formatShortDate(value: string) {\n  return new Intl.DateTimeFormat(\"en-US\", {\n    day: \"numeric\",\n    hour: \"numeric\",\n    minute: \"2-digit\",\n    month: \"short\",\n  }).format(new Date(value));\n}\n\ninterface CustomerMemoryAgentWorkspaceProps {\n  chatModel: string;\n  compactionThreshold: number;\n  customers: CustomerMemoryProfile[];\n  isChatAvailable: boolean;\n  nodeVersion: string;\n  setupMessage: string | null;\n}\n\ninterface AssistantMessageTraceProps {\n  isStreaming: boolean;\n  message: UIMessage;\n  onRetryTurn: () => Promise<void>;\n}\n\nfunction AssistantMessageTrace({\n  isStreaming,\n  message,\n  onRetryTurn,\n}: AssistantMessageTraceProps) {\n  const text = getCustomerMemoryMessageText(message);\n  const toolParts = getCustomerMemoryToolParts(message);\n\n  return (\n    <>\n      {toolParts.map((part) => (\n        <AssistantToolTrace key={part.toolCallId} part={part} />\n      ))}\n\n      <AssistantMessageText\n        isStreaming={isStreaming}\n        onRetryTurn={onRetryTurn}\n        text={text}\n      />\n    </>\n  );\n}\n\nfunction getCustomerMemoryToolName(part: ToolPart) {\n  return part.type === \"dynamic-tool\"\n    ? part.toolName\n    : part.type.split(\"-\").slice(1).join(\"-\");\n}\n\nfunction AssistantToolTrace({ part }: { part: ToolPart }) {\n  const toolName = getCustomerMemoryToolName(part);\n\n  return (\n    <Tool>\n      {part.type === \"dynamic-tool\" ? (\n        <ToolHeader\n          state={part.state}\n          title={toolName}\n          toolName={toolName}\n          type={part.type}\n        />\n      ) : (\n        <ToolHeader state={part.state} title={toolName} type={part.type} />\n      )}\n      <ToolContent>\n        {part.input ? <ToolInput input={part.input} /> : null}\n        <ToolOutput errorText={part.errorText} output={part.output} />\n      </ToolContent>\n    </Tool>\n  );\n}\n\nfunction AssistantMessageText({\n  isStreaming,\n  onRetryTurn,\n  text,\n}: {\n  isStreaming: boolean;\n  onRetryTurn: () => Promise<void>;\n  text: string;\n}) {\n  if (text.length > 0) {\n    return <MessageResponse>{text}</MessageResponse>;\n  }\n\n  if (isStreaming) {\n    return <Shimmer className=\"text-sm\">Thinking...</Shimmer>;\n  }\n\n  return (\n    <div className=\"flex flex-wrap items-center gap-3 text-muted-foreground text-sm\">\n      <span>Something went wrong while the agent was working.</span>\n      <Button\n        onClick={() => {\n          onRetryTurn();\n        }}\n        size=\"sm\"\n        type=\"button\"\n        variant=\"outline\"\n      >\n        <ArrowClockwiseIcon className=\"size-3.5\" />\n        Retry turn\n      </Button>\n    </div>\n  );\n}\n\nfunction CustomerMemoryThreadListSkeleton() {\n  return (\n    <div className=\"grid gap-2\">\n      {THREAD_SKELETON_KEYS.map((key) => (\n        <div\n          className=\"space-y-2 border border-foreground/10 px-3 py-3\"\n          key={key}\n        >\n          <div className=\"flex items-center justify-between gap-2\">\n            <Skeleton className=\"h-4 w-28\" />\n            <Skeleton className=\"h-5 w-8\" />\n          </div>\n          <Skeleton className=\"h-3 w-24\" />\n        </div>\n      ))}\n    </div>\n  );\n}\n\nfunction CustomerMemoryAccountContextSkeleton() {\n  return (\n    <div className=\"space-y-2\">\n      <p className=\"text-[11px] text-muted-foreground uppercase tracking-[0.18em]\">\n        Account context\n      </p>\n      <div className=\"space-y-2\">\n        <Skeleton className=\"h-4 w-full\" />\n        <Skeleton className=\"h-4 w-11/12\" />\n        <Skeleton className=\"h-4 w-4/5\" />\n      </div>\n      <div className=\"space-y-2 pt-1\">\n        <Skeleton className=\"h-4 w-10/12\" />\n        <Skeleton className=\"h-4 w-9/12\" />\n      </div>\n    </div>\n  );\n}\n\nfunction CustomerMemoryConversationSkeleton() {\n  return (\n    <div className=\"grid gap-6\">\n      <div className=\"ml-auto max-w-2xl space-y-3\">\n        <Skeleton className=\"h-4 w-40\" />\n        <Skeleton className=\"h-16 w-[28rem] max-w-full\" />\n      </div>\n      <div className=\"max-w-3xl space-y-3\">\n        <Skeleton className=\"h-4 w-56\" />\n        <Skeleton className=\"h-20 w-[34rem] max-w-full\" />\n        <Skeleton className=\"h-28 w-[30rem] max-w-full\" />\n      </div>\n    </div>\n  );\n}\n\nfunction buildSkeletonLineKeys(title: string, lineCount: number) {\n  return Array.from(\n    { length: lineCount },\n    (_, index) => `${title}-line-${index + 1}`\n  );\n}\n\nfunction CustomerMemoryPanelSkeleton(props: {\n  title: string;\n  lines?: number;\n  withBadgeRow?: boolean;\n}) {\n  const lineCount = props.lines ?? 3;\n\n  return (\n    <div className=\"space-y-2\">\n      <p className=\"text-[11px] text-muted-foreground uppercase tracking-[0.18em]\">\n        {props.title}\n      </p>\n      <div className=\"space-y-2 border border-foreground/10 px-3 py-3\">\n        {props.withBadgeRow ? (\n          <div className=\"flex flex-wrap gap-2\">\n            <Skeleton className=\"h-5 w-28\" />\n            <Skeleton className=\"h-5 w-20\" />\n          </div>\n        ) : null}\n        {buildSkeletonLineKeys(props.title, lineCount).map((key, index) => (\n          <Skeleton\n            className={cn(\"h-4\", index === lineCount - 1 ? \"w-4/5\" : \"w-full\")}\n            key={key}\n          />\n        ))}\n      </div>\n    </div>\n  );\n}\n\ninterface CustomerMemoryNavigationSidebarProps {\n  activeThreadId: string | null;\n  customerId: string;\n  customers: CustomerMemoryProfile[];\n  isBusy: boolean;\n  isReadonlyAccount: boolean;\n  isReady: boolean;\n  isSessionLoading: boolean;\n  onCreateThread: () => Promise<void>;\n  onSelectCustomer: (customerId: string) => Promise<void>;\n  onSelectThread: (threadId: string) => Promise<void>;\n  session: CustomerMemorySessionData | null;\n}\n\nfunction CustomerMemoryNavigationSidebar({\n  activeThreadId,\n  customerId,\n  customers,\n  isBusy,\n  isReadonlyAccount,\n  isReady,\n  isSessionLoading,\n  session,\n  onCreateThread,\n  onSelectCustomer,\n  onSelectThread,\n}: CustomerMemoryNavigationSidebarProps) {\n  return (\n    <aside className=\"grid content-start gap-4 border border-foreground/10 bg-background px-4 py-4 xl:min-h-0 xl:overflow-y-auto\">\n      <CustomerMemoryCustomerList\n        customerId={customerId}\n        customers={customers}\n        isBusy={isBusy}\n        isSessionLoading={isSessionLoading}\n        onSelectCustomer={onSelectCustomer}\n      />\n      <CustomerMemoryThreadList\n        activeThreadId={activeThreadId}\n        isBusy={isBusy}\n        isReadonlyAccount={isReadonlyAccount}\n        isReady={isReady}\n        isSessionLoading={isSessionLoading}\n        onCreateThread={onCreateThread}\n        onSelectThread={onSelectThread}\n        session={session}\n      />\n      <CustomerMemoryAccountContext\n        isSessionLoading={isSessionLoading}\n        session={session}\n      />\n    </aside>\n  );\n}\n\nfunction CustomerMemoryCustomerList({\n  customerId,\n  customers,\n  isBusy,\n  isSessionLoading,\n  onSelectCustomer,\n}: {\n  customerId: string;\n  customers: CustomerMemoryProfile[];\n  isBusy: boolean;\n  isSessionLoading: boolean;\n  onSelectCustomer: (customerId: string) => Promise<void>;\n}) {\n  return (\n    <div className=\"space-y-2\">\n      <p className=\"text-[11px] text-muted-foreground uppercase tracking-[0.18em]\">\n        Customers\n      </p>\n      <div className=\"grid gap-2\">\n        {customers.map((customer) => {\n          const isActiveCustomer = customer.id === customerId;\n\n          return (\n            <button\n              className={cn(\n                \"space-y-1 border px-3 py-3 text-left transition-colors\",\n                isActiveCustomer\n                  ? \"border-foreground bg-foreground text-background\"\n                  : \"border-foreground/10 hover:border-foreground/30\"\n              )}\n              disabled={isBusy || isSessionLoading}\n              key={customer.id}\n              onClick={() => {\n                onSelectCustomer(customer.id);\n              }}\n              type=\"button\"\n            >\n              <p className=\"font-medium text-sm\">{customer.name}</p>\n              <div\n                className={cn(\n                  \"flex items-center justify-between gap-2 text-xs/relaxed\",\n                  isActiveCustomer\n                    ? \"text-background/80\"\n                    : \"text-muted-foreground\"\n                )}\n              >\n                <span>{customer.industry}</span>\n                <span>\n                  {customer.accessMode === \"shared_readonly\"\n                    ? \"Read only\"\n                    : \"Writable\"}\n                </span>\n              </div>\n            </button>\n          );\n        })}\n      </div>\n    </div>\n  );\n}\n\nfunction CustomerMemoryThreadList({\n  activeThreadId,\n  isBusy,\n  isReadonlyAccount,\n  isReady,\n  isSessionLoading,\n  session,\n  onCreateThread,\n  onSelectThread,\n}: {\n  activeThreadId: string | null;\n  isBusy: boolean;\n  isReadonlyAccount: boolean;\n  isReady: boolean;\n  isSessionLoading: boolean;\n  session: CustomerMemorySessionData | null;\n  onCreateThread: () => Promise<void>;\n  onSelectThread: (threadId: string) => Promise<void>;\n}) {\n  return (\n    <div className=\"space-y-2\">\n      <div className=\"flex items-center justify-between gap-3\">\n        <p className=\"text-[11px] text-muted-foreground uppercase tracking-[0.18em]\">\n          Threads\n        </p>\n        <Button\n          disabled={!isReady || isBusy || isSessionLoading || isReadonlyAccount}\n          onClick={() => {\n            onCreateThread();\n          }}\n          size=\"sm\"\n          type=\"button\"\n          variant=\"outline\"\n        >\n          <PlusIcon className=\"size-3.5\" />\n          New\n        </Button>\n      </div>\n\n      <CustomerMemoryThreadListContent\n        activeThreadId={activeThreadId}\n        isBusy={isBusy}\n        isSessionLoading={isSessionLoading}\n        onSelectThread={onSelectThread}\n        session={session}\n      />\n    </div>\n  );\n}\n\nfunction CustomerMemoryThreadListContent({\n  activeThreadId,\n  isBusy,\n  isSessionLoading,\n  session,\n  onSelectThread,\n}: {\n  activeThreadId: string | null;\n  isBusy: boolean;\n  isSessionLoading: boolean;\n  session: CustomerMemorySessionData | null;\n  onSelectThread: (threadId: string) => Promise<void>;\n}) {\n  if (isSessionLoading && !session) {\n    return <CustomerMemoryThreadListSkeleton />;\n  }\n\n  if (!session?.threads.length) {\n    return (\n      <p className=\"text-muted-foreground text-sm/relaxed\">\n        No threads loaded yet.\n      </p>\n    );\n  }\n\n  return (\n    <div className=\"grid gap-2\">\n      {session.threads.map((thread, index) => {\n        const isActiveThread = thread.id === activeThreadId;\n\n        return (\n          <button\n            className={cn(\n              \"space-y-1 border px-3 py-3 text-left transition-colors\",\n              isActiveThread\n                ? \"border-foreground bg-muted/50\"\n                : \"border-foreground/10 hover:border-foreground/30\"\n            )}\n            disabled={isBusy || isSessionLoading}\n            key={thread.id}\n            onClick={() => {\n              onSelectThread(thread.id);\n            }}\n            type=\"button\"\n          >\n            <div className=\"flex items-center justify-between gap-2\">\n              <p className=\"font-medium text-sm\">\n                {buildCustomerMemoryThreadLabel({\n                  fallbackIndex: index,\n                  title: thread.title,\n                })}\n              </p>\n              <Badge variant=\"outline\">{thread.messageCount}</Badge>\n            </div>\n            <p className=\"text-muted-foreground text-xs/relaxed\">\n              Updated {formatShortDate(thread.updatedAt)}\n            </p>\n          </button>\n        );\n      })}\n    </div>\n  );\n}\n\nfunction CustomerMemoryAccountContext({\n  isSessionLoading,\n  session,\n}: {\n  isSessionLoading: boolean;\n  session: CustomerMemorySessionData | null;\n}) {\n  if (isSessionLoading && !session) {\n    return <CustomerMemoryAccountContextSkeleton />;\n  }\n\n  if (!session?.customer) {\n    return null;\n  }\n\n  return (\n    <div className=\"space-y-2\">\n      <p className=\"text-[11px] text-muted-foreground uppercase tracking-[0.18em]\">\n        Account context\n      </p>\n      <p className=\"text-sm/relaxed\">{session.customer.accountSummary}</p>\n      <ul className=\"space-y-2 text-muted-foreground text-sm/relaxed\">\n        {session.customer.operatingNotes.map((note) => (\n          <li key={note}>• {note}</li>\n        ))}\n      </ul>\n    </div>\n  );\n}\n\ninterface CustomerMemoryChatPanelProps {\n  chatErrorMessage: string | null;\n  chatModel: string;\n  compactionThreshold: number;\n  isBusy: boolean;\n  isChatAvailable: boolean;\n  isReadonlyAccount: boolean;\n  isReady: boolean;\n  isSessionLoading: boolean;\n  messages: UIMessage[];\n  onRegenerateLastTurn: () => Promise<void>;\n  onSendPrompt: (text: string) => Promise<void>;\n  onStopChat: () => void;\n  samplePrompts: string[];\n  session: CustomerMemorySessionData | null;\n  sessionErrorMessage: string | null;\n  setupMessage: string | null;\n  status: ChatStatus;\n}\n\nfunction CustomerMemoryChatPanel({\n  chatErrorMessage,\n  chatModel,\n  compactionThreshold,\n  isBusy,\n  isChatAvailable,\n  isReadonlyAccount,\n  isReady,\n  isSessionLoading,\n  messages,\n  samplePrompts,\n  session,\n  sessionErrorMessage,\n  setupMessage,\n  status,\n  onRegenerateLastTurn,\n  onSendPrompt,\n  onStopChat,\n}: CustomerMemoryChatPanelProps) {\n  return (\n    <section className=\"flex min-h-[74svh] flex-col border border-foreground/10 bg-background xl:h-full xl:min-h-0\">\n      {isChatAvailable ? null : (\n        <div className=\"border-foreground/10 border-b px-4 py-3 text-muted-foreground text-xs/relaxed\">\n          {setupMessage}\n        </div>\n      )}\n      <CustomerMemoryErrorBanner message={sessionErrorMessage} />\n      <CustomerMemoryErrorBanner message={chatErrorMessage} />\n      <CustomerMemoryConversation\n        compactionThreshold={compactionThreshold}\n        isBusy={isBusy}\n        isSessionLoading={isSessionLoading}\n        messages={messages}\n        onRegenerateLastTurn={onRegenerateLastTurn}\n        session={session}\n      />\n      <CustomerMemoryComposer\n        chatModel={chatModel}\n        isBusy={isBusy}\n        isReadonlyAccount={isReadonlyAccount}\n        isReady={isReady}\n        isSessionLoading={isSessionLoading}\n        messages={messages}\n        onRegenerateLastTurn={onRegenerateLastTurn}\n        onSendPrompt={onSendPrompt}\n        onStopChat={onStopChat}\n        samplePrompts={samplePrompts}\n        status={status}\n      />\n    </section>\n  );\n}\n\nfunction CustomerMemoryErrorBanner({ message }: { message: string | null }) {\n  if (!message) {\n    return null;\n  }\n\n  return (\n    <div className=\"border-foreground/10 border-b px-4 py-3 text-destructive text-xs/relaxed\">\n      {message}\n    </div>\n  );\n}\n\nfunction CustomerMemoryConversation({\n  compactionThreshold,\n  isBusy,\n  isSessionLoading,\n  messages,\n  onRegenerateLastTurn,\n  session,\n}: {\n  compactionThreshold: number;\n  isBusy: boolean;\n  isSessionLoading: boolean;\n  messages: UIMessage[];\n  onRegenerateLastTurn: () => Promise<void>;\n  session: CustomerMemorySessionData | null;\n}) {\n  return (\n    <Conversation className=\"min-h-0\">\n      <ConversationContent className=\"mx-auto flex w-full max-w-3xl flex-1 gap-6 px-4 py-6\">\n        <CustomerMemoryConversationContent\n          compactionThreshold={compactionThreshold}\n          isBusy={isBusy}\n          isSessionLoading={isSessionLoading}\n          messages={messages}\n          onRegenerateLastTurn={onRegenerateLastTurn}\n          session={session}\n        />\n      </ConversationContent>\n      <ConversationScrollButton />\n    </Conversation>\n  );\n}\n\nfunction CustomerMemoryConversationContent({\n  compactionThreshold,\n  isBusy,\n  isSessionLoading,\n  messages,\n  onRegenerateLastTurn,\n  session,\n}: {\n  compactionThreshold: number;\n  isBusy: boolean;\n  isSessionLoading: boolean;\n  messages: UIMessage[];\n  onRegenerateLastTurn: () => Promise<void>;\n  session: CustomerMemorySessionData | null;\n}) {\n  if (messages.length === 0) {\n    if (isSessionLoading) {\n      return <CustomerMemoryConversationSkeleton />;\n    }\n\n    return (\n      <ConversationEmptyState\n        description=\"Talk to one customer thread, let the agent write durable memories through a tool call, then inspect the saved memories and handoff compaction on the right.\"\n        icon={<DatabaseIcon className=\"size-5\" />}\n        title=\"Customer memory workspace is ready\"\n      />\n    );\n  }\n\n  const completedCompactionMessageCount =\n    session?.latestCompaction?.messageCount ?? null;\n  const displayMessages = messages.filter(\n    (message, index) =>\n      message.role !== \"assistant\" ||\n      hasCustomerMemoryMessageContent(message) ||\n      index === messages.length - 1\n  );\n  const latestMessage = messages.at(-1);\n  const compactionMessageCount = displayMessages.filter(\n    hasCustomerMemoryMessageContent\n  ).length;\n  const pendingCompaction = getCustomerMemoryPendingCompaction({\n    compactionThreshold,\n    isSessionLoading,\n    latestCompactionMessageCount: completedCompactionMessageCount,\n    messageCount: compactionMessageCount,\n  });\n\n  return (\n    <>\n      {displayMessages.map((message, index) => {\n        const isLastAssistantMessage =\n          message.role === \"assistant\" && message === latestMessage && isBusy;\n        const shouldShowCompletedCompaction =\n          completedCompactionMessageCount === index + 1;\n\n        return (\n          <div className=\"contents\" key={message.id}>\n            <Message from={message.role}>\n              <MessageContent\n                className={cn(\n                  \"space-y-4\",\n                  message.role === \"assistant\" ? \"max-w-3xl\" : \"max-w-2xl\"\n                )}\n              >\n                {message.role === \"assistant\" ? (\n                  <AssistantMessageTrace\n                    isStreaming={isLastAssistantMessage}\n                    message={message}\n                    onRetryTurn={onRegenerateLastTurn}\n                  />\n                ) : (\n                  <MessageResponse>\n                    {getCustomerMemoryMessageText(message)}\n                  </MessageResponse>\n                )}\n              </MessageContent>\n            </Message>\n            {shouldShowCompletedCompaction ? (\n              <CustomerMemoryContextCheckpoint\n                messageCount={completedCompactionMessageCount}\n                state=\"completed\"\n              />\n            ) : null}\n          </div>\n        );\n      })}\n      {pendingCompaction ? (\n        <CustomerMemoryContextCheckpoint\n          messageCount={pendingCompaction.messageCount}\n          state=\"pending\"\n        />\n      ) : null}\n    </>\n  );\n}\n\nfunction CustomerMemoryContextCheckpoint({\n  messageCount,\n  state,\n}: {\n  messageCount: number;\n  state: \"completed\" | \"pending\";\n}) {\n  return (\n    <Checkpoint className=\"my-1 w-full gap-2 text-muted-foreground text-xs\">\n      <span className=\"h-px flex-1 bg-border\" />\n      <ScrollIcon className=\"size-4 shrink-0\" />\n      {state === \"pending\" ? (\n        <Shimmer className=\"shrink-0 font-medium\">\n          Compacting customer context…\n        </Shimmer>\n      ) : (\n        <span className=\"shrink-0 font-medium\">\n          Context compacted · {messageCount} messages summarized\n        </span>\n      )}\n    </Checkpoint>\n  );\n}\n\nfunction CustomerMemoryComposer({\n  chatModel,\n  isBusy,\n  isReadonlyAccount,\n  isReady,\n  isSessionLoading,\n  messages,\n  samplePrompts,\n  status,\n  onRegenerateLastTurn,\n  onSendPrompt,\n  onStopChat,\n}: {\n  chatModel: string;\n  isBusy: boolean;\n  isReadonlyAccount: boolean;\n  isReady: boolean;\n  isSessionLoading: boolean;\n  messages: UIMessage[];\n  samplePrompts: string[];\n  status: ChatStatus;\n  onRegenerateLastTurn: () => Promise<void>;\n  onSendPrompt: (text: string) => Promise<void>;\n  onStopChat: () => void;\n}) {\n  return (\n    <div className=\"border-foreground/10 border-t px-4 py-4\">\n      <div className=\"mx-auto w-full max-w-3xl space-y-3\">\n        <PromptInput\n          onSubmit={({ text }) => {\n            onSendPrompt(text);\n          }}\n        >\n          <PromptInputBody>\n            <PromptInputTextarea\n              disabled={\n                !isReady || isBusy || isSessionLoading || isReadonlyAccount\n              }\n              placeholder={\n                isReadonlyAccount\n                  ? \"This demo account is read-only. Switch to Demo Sandbox to create your own private thread.\"\n                  : \"Ask for an update, share a durable customer fact, or tell the agent about a promise that should be remembered later.\"\n              }\n            />\n          </PromptInputBody>\n          <PromptInputFooter className=\"flex items-center justify-between gap-3 border-foreground/10 border-t px-3 py-3\">\n            <div className=\"flex flex-wrap items-center gap-2\">\n              <Badge variant=\"outline\">Memory thread</Badge>\n              <Badge variant=\"outline\">Agent memory tool</Badge>\n              <Badge variant=\"outline\">{chatModel}</Badge>\n            </div>\n            <div className=\"flex items-center gap-2\">\n              {isBusy ? (\n                <Button\n                  onClick={onStopChat}\n                  size=\"sm\"\n                  type=\"button\"\n                  variant=\"outline\"\n                >\n                  <StopIcon className=\"size-3.5\" />\n                  Stop\n                </Button>\n              ) : null}\n              {messages.length > 0 ? (\n                <Button\n                  disabled={isReadonlyAccount}\n                  onClick={() => {\n                    onRegenerateLastTurn();\n                  }}\n                  size=\"sm\"\n                  type=\"button\"\n                  variant=\"outline\"\n                >\n                  <ArrowClockwiseIcon className=\"size-3.5\" />\n                  Retry\n                </Button>\n              ) : null}\n              <PromptInputSubmit\n                disabled={!isReady || isBusy || isReadonlyAccount}\n                status={status}\n              />\n            </div>\n          </PromptInputFooter>\n        </PromptInput>\n\n        {messages.length === 0 ? (\n          <div className=\"flex flex-wrap gap-2\">\n            {samplePrompts.map((prompt) => (\n              <Button\n                className=\"max-w-full justify-start text-left\"\n                disabled={\n                  !isReady || isBusy || isSessionLoading || isReadonlyAccount\n                }\n                key={prompt}\n                onClick={() => {\n                  onSendPrompt(prompt);\n                }}\n                size=\"sm\"\n                type=\"button\"\n                variant=\"outline\"\n              >\n                {prompt}\n              </Button>\n            ))}\n          </div>\n        ) : null}\n      </div>\n    </div>\n  );\n}\n\ninterface CustomerMemoryInsightsSidebarProps {\n  compactionThreshold: number;\n  isChatAvailable: boolean;\n  isSessionLoading: boolean;\n  latestPrompt: string;\n  nodeVersion: string;\n  onRefreshSession: (query?: string) => Promise<void>;\n  session: CustomerMemorySessionData | null;\n  viewState: {\n    memoryEventCount: number;\n    memoryCount: number;\n    messageCount: number;\n    relevantMemoryCount: number;\n    threadCount: number;\n  };\n}\n\nfunction CustomerMemoryInsightsSidebar({\n  compactionThreshold,\n  isChatAvailable,\n  isSessionLoading,\n  latestPrompt,\n  nodeVersion,\n  session,\n  viewState,\n  onRefreshSession,\n}: CustomerMemoryInsightsSidebarProps) {\n  return (\n    <aside className=\"grid content-start gap-4 border border-foreground/10 bg-background px-4 py-4 xl:min-h-0 xl:overflow-y-auto\">\n      <CustomerMemoryRuntimePanel\n        compactionThreshold={compactionThreshold}\n        isChatAvailable={isChatAvailable}\n        nodeVersion={nodeVersion}\n      />\n      <CustomerMemorySessionStatePanel viewState={viewState} />\n      <CustomerMemoryMemoriesPanel\n        isSessionLoading={isSessionLoading}\n        latestPrompt={latestPrompt}\n        onRefreshSession={onRefreshSession}\n        session={session}\n      />\n      <CustomerMemoryMemoryEventsPanel\n        isSessionLoading={isSessionLoading}\n        session={session}\n      />\n      <CustomerMemoryCompactionPanel\n        isSessionLoading={isSessionLoading}\n        session={session}\n      />\n      <CustomerMemoryPromptContextPanel\n        isSessionLoading={isSessionLoading}\n        latestPrompt={latestPrompt}\n        session={session}\n        viewState={viewState}\n      />\n    </aside>\n  );\n}\n\nfunction CustomerMemoryRuntimePanel({\n  compactionThreshold,\n  isChatAvailable,\n  nodeVersion,\n}: {\n  compactionThreshold: number;\n  isChatAvailable: boolean;\n  nodeVersion: string;\n}) {\n  return (\n    <div className=\"space-y-2\">\n      <p className=\"text-[11px] text-muted-foreground uppercase tracking-[0.18em]\">\n        Runtime\n      </p>\n      <div className=\"flex flex-wrap gap-2\">\n        <Badge variant=\"outline\">\n          {isChatAvailable ? \"Ready\" : \"Setup required\"}\n        </Badge>\n        <Badge variant=\"outline\">\n          Node {nodeVersion.replace(NODE_VERSION_PREFIX_PATTERN, \"\")}\n        </Badge>\n        <Badge variant=\"outline\">{compactionThreshold} message threshold</Badge>\n      </div>\n    </div>\n  );\n}\n\nfunction CustomerMemorySessionStatePanel({\n  viewState,\n}: {\n  viewState: CustomerMemoryInsightsSidebarProps[\"viewState\"];\n}) {\n  return (\n    <div className=\"space-y-2\">\n      <p className=\"text-[11px] text-muted-foreground uppercase tracking-[0.18em]\">\n        Session state\n      </p>\n      <div className=\"flex flex-wrap gap-2\">\n        <Badge variant=\"outline\">{viewState.threadCount} threads</Badge>\n        <Badge variant=\"outline\">{viewState.messageCount} messages</Badge>\n        <Badge variant=\"outline\">{viewState.memoryCount} memories</Badge>\n        <Badge variant=\"outline\">{viewState.memoryEventCount} events</Badge>\n      </div>\n      <p className=\"text-muted-foreground text-sm/relaxed\">\n        The agent manages durable customer facts explicitly through the memory\n        lifecycle tool. The chat thread is restored from the configured\n        persistence layer whenever you switch back to the account.\n      </p>\n    </div>\n  );\n}\n\nfunction CustomerMemoryMemoriesPanel({\n  isSessionLoading,\n  latestPrompt,\n  session,\n  onRefreshSession,\n}: {\n  isSessionLoading: boolean;\n  latestPrompt: string;\n  session: CustomerMemorySessionData | null;\n  onRefreshSession: (query?: string) => Promise<void>;\n}) {\n  return (\n    <div className=\"space-y-2\">\n      <div className=\"flex items-center justify-between gap-3\">\n        <p className=\"text-[11px] text-muted-foreground uppercase tracking-[0.18em]\">\n          Saved memories\n        </p>\n        <Button\n          onClick={() => {\n            onRefreshSession(latestPrompt);\n          }}\n          size=\"sm\"\n          type=\"button\"\n          variant=\"outline\"\n        >\n          <RobotIcon className=\"size-3.5\" />\n          Refresh\n        </Button>\n      </div>\n\n      <CustomerMemoryMemoriesContent\n        isSessionLoading={isSessionLoading}\n        session={session}\n      />\n    </div>\n  );\n}\n\nfunction CustomerMemoryMemoriesContent({\n  isSessionLoading,\n  session,\n}: {\n  isSessionLoading: boolean;\n  session: CustomerMemorySessionData | null;\n}) {\n  if (isSessionLoading && !session) {\n    return (\n      <CustomerMemoryPanelSkeleton\n        lines={4}\n        title=\"Saved memories\"\n        withBadgeRow\n      />\n    );\n  }\n\n  if (!session?.memories.length) {\n    return (\n      <p className=\"text-muted-foreground text-sm/relaxed\">\n        No customer memories are active yet. Share a durable preference,\n        promise, risk, or account fact and let the agent call its memory\n        lifecycle tool.\n      </p>\n    );\n  }\n\n  return (\n    <div className=\"grid gap-2\">\n      {session.memories.map((memory) => {\n        const isRelevant = session.relevantMemories.some(\n          (candidate) => candidate.id === memory.id\n        );\n\n        return (\n          <div\n            className={cn(\n              \"space-y-2 border px-3 py-3\",\n              isRelevant\n                ? \"border-foreground/30 bg-muted/30\"\n                : \"border-foreground/10\"\n            )}\n            key={memory.id}\n          >\n            <div className=\"flex flex-wrap items-center gap-2\">\n              <Badge variant=\"outline\">\n                {formatCustomerMemoryCategory(memory.category)}\n              </Badge>\n              {memory.status === \"updated\" ? (\n                <Badge variant=\"outline\">Updated</Badge>\n              ) : null}\n              {isRelevant ? <Badge variant=\"outline\">Recalled</Badge> : null}\n            </div>\n            <p className=\"font-medium text-sm\">\n              {memory.title?.trim() || \"Untitled memory\"}\n            </p>\n            <p className=\"text-sm/relaxed\">{memory.content}</p>\n            <p className=\"text-muted-foreground text-xs/relaxed\">\n              Updated {formatShortDate(memory.updatedAt)}\n            </p>\n          </div>\n        );\n      })}\n    </div>\n  );\n}\n\nfunction CustomerMemoryMemoryEventsPanel({\n  isSessionLoading,\n  session,\n}: {\n  isSessionLoading: boolean;\n  session: CustomerMemorySessionData | null;\n}) {\n  if (isSessionLoading && !session) {\n    return <CustomerMemoryPanelSkeleton lines={3} title=\"Memory lifecycle\" />;\n  }\n\n  if (!session?.memoryEvents.length) {\n    return null;\n  }\n\n  return (\n    <details className=\"group space-y-2 border border-foreground/10 px-3 py-3\">\n      <summary className=\"cursor-pointer list-none text-[11px] text-muted-foreground uppercase tracking-[0.18em]\">\n        Memory lifecycle\n      </summary>\n      <div className=\"grid gap-2 pt-2\">\n        {session.memoryEvents.slice(0, 8).map((event) => (\n          <div className=\"space-y-1 text-xs/relaxed\" key={event.id}>\n            <div className=\"flex flex-wrap items-center gap-2\">\n              <Badge variant=\"outline\">\n                {formatCustomerMemoryCategory(event.operation)}\n              </Badge>\n              <span className=\"text-muted-foreground\">\n                {formatShortDate(event.createdAt)}\n              </span>\n            </div>\n            {event.reason ? (\n              <p className=\"text-muted-foreground\">{event.reason}</p>\n            ) : null}\n            {event.afterContent ? <p>{event.afterContent}</p> : null}\n          </div>\n        ))}\n      </div>\n    </details>\n  );\n}\n\nfunction CustomerMemoryCompactionPanel({\n  isSessionLoading,\n  session,\n}: {\n  isSessionLoading: boolean;\n  session: CustomerMemorySessionData | null;\n}) {\n  return (\n    <div className=\"space-y-2\">\n      <p className=\"text-[11px] text-muted-foreground uppercase tracking-[0.18em]\">\n        Latest handoff\n      </p>\n      <CustomerMemoryCompactionContent\n        isSessionLoading={isSessionLoading}\n        session={session}\n      />\n    </div>\n  );\n}\n\nfunction CustomerMemoryCompactionContent({\n  isSessionLoading,\n  session,\n}: {\n  isSessionLoading: boolean;\n  session: CustomerMemorySessionData | null;\n}) {\n  if (isSessionLoading && !session) {\n    return (\n      <CustomerMemoryPanelSkeleton\n        lines={3}\n        title=\"Latest handoff\"\n        withBadgeRow\n      />\n    );\n  }\n\n  if (!session?.latestCompaction) {\n    return (\n      <p className=\"text-muted-foreground text-sm/relaxed\">\n        No handoff compaction exists yet. Once the thread reaches the threshold,\n        older turns will be replaced by one saved handoff.\n      </p>\n    );\n  }\n\n  return (\n    <div className=\"space-y-2 border border-foreground/10 px-3 py-3\">\n      <div className=\"flex flex-wrap items-center gap-2\">\n        <Badge variant=\"outline\">\n          {session.latestCompaction.messageCount} messages compacted\n        </Badge>\n        <Badge variant=\"outline\">\n          {formatShortDate(session.latestCompaction.createdAt)}\n        </Badge>\n      </div>\n      <p className=\"text-sm/relaxed\">{session.latestCompaction.summary}</p>\n    </div>\n  );\n}\n\nfunction CustomerMemoryPromptContextPanel({\n  isSessionLoading,\n  latestPrompt,\n  session,\n  viewState,\n}: {\n  isSessionLoading: boolean;\n  latestPrompt: string;\n  session: CustomerMemorySessionData | null;\n  viewState: CustomerMemoryInsightsSidebarProps[\"viewState\"];\n}) {\n  if (isSessionLoading && !session) {\n    return (\n      <CustomerMemoryPanelSkeleton lines={2} title=\"Current prompt context\" />\n    );\n  }\n\n  if (!latestPrompt) {\n    return null;\n  }\n\n  return (\n    <div className=\"space-y-2\">\n      <p className=\"text-[11px] text-muted-foreground uppercase tracking-[0.18em]\">\n        Current prompt context\n      </p>\n      <div className=\"space-y-2 border border-foreground/10 px-3 py-3\">\n        <p className=\"text-sm/relaxed\">{latestPrompt}</p>\n        <p className=\"text-muted-foreground text-xs/relaxed\">\n          {viewState.relevantMemoryCount} saved memories were retrieved for the\n          latest user turn.\n        </p>\n      </div>\n    </div>\n  );\n}\n\nexport function CustomerMemoryAgentWorkspace({\n  chatModel,\n  compactionThreshold,\n  customers,\n  isChatAvailable,\n  nodeVersion,\n  setupMessage,\n}: CustomerMemoryAgentWorkspaceProps) {\n  const controller = useCustomerMemorySession(isChatAvailable);\n  const samplePrompts = useMemo(\n    () => getCustomerMemorySamplePrompts(controller.customerId),\n    [controller.customerId]\n  );\n\n  return (\n    <div className=\"grid min-h-[74svh] gap-4 xl:h-full xl:min-h-0 xl:grid-cols-[16rem_minmax(0,1fr)_22rem]\">\n      <CustomerMemoryNavigationSidebar\n        activeThreadId={controller.activeThreadId}\n        customerId={controller.customerId}\n        customers={customers}\n        isBusy={controller.isBusy}\n        isReadonlyAccount={controller.isReadonlyAccount}\n        isReady={controller.isReady}\n        isSessionLoading={controller.isSessionLoading}\n        onCreateThread={controller.createThread}\n        onSelectCustomer={controller.selectCustomer}\n        onSelectThread={controller.selectThread}\n        session={controller.session}\n      />\n      <CustomerMemoryChatPanel\n        chatErrorMessage={controller.chatErrorMessage}\n        chatModel={chatModel}\n        compactionThreshold={compactionThreshold}\n        isBusy={controller.isBusy}\n        isChatAvailable={isChatAvailable}\n        isReadonlyAccount={controller.isReadonlyAccount}\n        isReady={controller.isReady}\n        isSessionLoading={controller.isSessionLoading}\n        messages={controller.messages}\n        onRegenerateLastTurn={controller.regenerateLastTurn}\n        onSendPrompt={controller.sendPrompt}\n        onStopChat={controller.stopChat}\n        samplePrompts={samplePrompts}\n        session={controller.session}\n        sessionErrorMessage={controller.sessionErrorMessage}\n        setupMessage={setupMessage}\n        status={controller.status}\n      />\n      <CustomerMemoryInsightsSidebar\n        compactionThreshold={compactionThreshold}\n        isChatAvailable={isChatAvailable}\n        isSessionLoading={controller.isSessionLoading}\n        latestPrompt={controller.latestPrompt}\n        nodeVersion={nodeVersion}\n        onRefreshSession={controller.refreshSession}\n        session={controller.session}\n        viewState={controller.viewState}\n      />\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/customer-memory-agent/customer-memory-agent-workspace.tsx"
    },
    {
      "path": "registry/customer-memory-agent/components/customer-memory-agent/customer-memory-session.ts",
      "content": "\"use client\";\n\nimport { isToolOrDynamicToolUIPart, type UIMessage } from \"ai\";\n\nimport type { CustomerMemoryProfile } from \"@/lib/customer-memory-agent/customer-profiles\";\nimport type { CustomerMemorySessionData } from \"@/lib/customer-memory-agent/session-data\";\n\nconst customerMemoryRecentMessageWindow = 2;\n\nexport interface CustomerMemorySessionViewState {\n  activeThreadId: string | null;\n  customer: CustomerMemoryProfile | null;\n  hasMessages: boolean;\n  latestPrompt: string;\n  latestSummary: string | null;\n  memoryCount: number;\n  memoryEventCount: number;\n  messageCount: number;\n  relevantMemoryCount: number;\n  threadCount: number;\n}\n\nexport function getCustomerMemoryPendingCompaction(input: {\n  compactionThreshold: number;\n  isSessionLoading: boolean;\n  latestCompactionMessageCount: number | null;\n  messageCount: number;\n  recentWindow?: number;\n}) {\n  if (\n    !input.isSessionLoading ||\n    input.messageCount < input.compactionThreshold\n  ) {\n    return null;\n  }\n\n  const recentWindow = input.recentWindow ?? customerMemoryRecentMessageWindow;\n  const targetMessageCount = input.messageCount - recentWindow;\n\n  if (targetMessageCount <= 0) {\n    return null;\n  }\n\n  if (\n    input.latestCompactionMessageCount !== null &&\n    input.latestCompactionMessageCount >= targetMessageCount\n  ) {\n    return null;\n  }\n\n  return {\n    messageCount: targetMessageCount,\n  };\n}\n\nexport function getCustomerMemoryMessageText(message: UIMessage) {\n  return message.parts\n    .filter((part) => part.type === \"text\")\n    .map((part) => part.text)\n    .join(\"\\n\")\n    .trim();\n}\n\nexport function getCustomerMemoryToolParts(message: UIMessage) {\n  return message.parts.filter(isToolOrDynamicToolUIPart);\n}\n\nexport function hasCustomerMemoryMessageContent(message: UIMessage) {\n  return (\n    getCustomerMemoryMessageText(message).length > 0 ||\n    getCustomerMemoryToolParts(message).length > 0\n  );\n}\n\nexport function removeEmptyCustomerMemoryAssistantMessages(\n  messages: UIMessage[]\n) {\n  return messages.filter(\n    (message) =>\n      message.role !== \"assistant\" || hasCustomerMemoryMessageContent(message)\n  );\n}\n\nexport function getLatestCustomerMemoryPrompt(messages: UIMessage[]) {\n  for (const message of [...messages].reverse()) {\n    if (message.role !== \"user\") {\n      continue;\n    }\n\n    const text = getCustomerMemoryMessageText(message);\n\n    if (text.length > 0) {\n      return text;\n    }\n  }\n\n  return \"\";\n}\n\nexport function buildCustomerMemoryThreadLabel(input: {\n  fallbackIndex: number;\n  title: string | null;\n}) {\n  const trimmedTitle = input.title?.trim();\n\n  if (trimmedTitle) {\n    return trimmedTitle;\n  }\n\n  return `Thread ${input.fallbackIndex + 1}`;\n}\n\nexport function formatCustomerMemoryCategory(category: string) {\n  return category\n    .split(\"_\")\n    .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))\n    .join(\" \");\n}\n\nexport function getCustomerMemorySamplePrompts(customerId: string) {\n  switch (customerId) {\n    case \"demo-sandbox\":\n      return [\n        \"Remember that Brightfield Health requires compliance-safe rollout language and wants every customer-facing update to stay plain and factual.\",\n        \"We promised Brightfield a revised launch update by Tuesday after the compliance review. Save that and draft the reply.\",\n        \"How should I answer if Brightfield asks whether a rollout claim is safe to send to customers today?\",\n      ];\n    case \"northstar-logistics\":\n      return [\n        \"Remember that Northstar needs incident updates with a clear owner and next checkpoint.\",\n        \"We promised the finance lead an outage summary by Friday. Save that and draft a response.\",\n        \"How should I answer if Northstar asks for an update on billing-impact investigations?\",\n      ];\n    case \"helio-dev\":\n      return [\n        \"Remember that Helio Dev wants root-cause detail and concrete follow-up dates.\",\n        \"We promised a patch ETA next Tuesday. Save that and draft the reply.\",\n        \"How should I respond if Helio asks for a concise technical status update?\",\n      ];\n    default:\n      return [\n        \"Remember that Acme Co needs executive-safe status language and legal review on marketing claims.\",\n        \"We promised Acme a rewritten launch note after legal review on Monday. Save that and reply.\",\n        \"How should I answer if Acme asks whether a campaign claim is safe to publish?\",\n      ];\n  }\n}\n\nexport function buildCustomerMemorySessionViewState(\n  session: CustomerMemorySessionData | null\n): CustomerMemorySessionViewState {\n  return {\n    activeThreadId: session?.thread.id ?? null,\n    customer: session?.customer ?? null,\n    hasMessages: (session?.messages.length ?? 0) > 0,\n    latestPrompt: getLatestCustomerMemoryPrompt(session?.messages ?? []),\n    latestSummary: session?.latestCompaction?.summary ?? null,\n    memoryEventCount: session?.memoryEvents.length ?? 0,\n    memoryCount: session?.memories.length ?? 0,\n    messageCount: session?.messages.length ?? 0,\n    relevantMemoryCount: session?.relevantMemories.length ?? 0,\n    threadCount: session?.threads.length ?? 0,\n  };\n}\n",
      "type": "registry:component",
      "target": "@components/customer-memory-agent/customer-memory-session.ts"
    },
    {
      "path": "registry/customer-memory-agent/components/customer-memory-agent/use-customer-memory-chat.ts",
      "content": "\"use client\";\n\nimport { Chat, useChat } from \"@ai-sdk/react\";\nimport { DefaultChatTransport, type UIMessage } from \"ai\";\nimport { useState } from \"react\";\n\ninterface UseCustomerMemoryChatWithFactoryOptions<TMessage extends UIMessage> {\n  createChat: () => Chat<TMessage>;\n}\n\nexport function useCustomerMemoryChat<TMessage extends UIMessage = UIMessage>(\n  options: UseCustomerMemoryChatWithFactoryOptions<TMessage>\n) {\n  const [chat] = useState(() => options.createChat());\n  const controller = useChat({ chat });\n  const hasMessages = controller.messages.length > 0;\n  const isBusy =\n    controller.status === \"submitted\" || controller.status === \"streaming\";\n\n  return {\n    ...controller,\n    chat,\n    hasMessages,\n    isBusy,\n  };\n}\n",
      "type": "registry:component",
      "target": "@components/customer-memory-agent/use-customer-memory-chat.ts"
    },
    {
      "path": "registry/customer-memory-agent/components/customer-memory-agent/use-customer-memory-session.ts",
      "content": "\"use client\";\n\nimport { Chat } from \"@ai-sdk/react\";\nimport { type ChatStatus, DefaultChatTransport, type UIMessage } from \"ai\";\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { customerMemoryProfiles } from \"@/lib/customer-memory-agent/customer-profiles\";\nimport type { CustomerMemorySessionData } from \"@/lib/customer-memory-agent/session-data\";\nimport {\n  buildCustomerMemorySessionViewState,\n  getLatestCustomerMemoryPrompt,\n  removeEmptyCustomerMemoryAssistantMessages,\n} from \"./customer-memory-session\";\nimport { useCustomerMemoryChat } from \"./use-customer-memory-chat\";\n\nasync function fetchCustomerMemorySession(input: {\n  customerId: string;\n  query?: string;\n  threadId?: string | null;\n}) {\n  const params = new URLSearchParams({\n    customerId: input.customerId,\n  });\n\n  if (input.query?.trim()) {\n    params.set(\"query\", input.query.trim());\n  }\n\n  if (input.threadId?.trim()) {\n    params.set(\"threadId\", input.threadId.trim());\n  }\n\n  const response = await fetch(\n    `/api/demos/customer-memory-agent/session?${params.toString()}`\n  );\n\n  if (!response.ok) {\n    throw new Error(\n      (await response.text()) || \"Failed to load the customer-memory session.\"\n    );\n  }\n\n  return (await response.json()) as CustomerMemorySessionData;\n}\n\nasync function createCustomerMemoryThread(customerId: string) {\n  const response = await fetch(\"/api/demos/customer-memory-agent/threads\", {\n    body: JSON.stringify({ customerId }),\n    headers: {\n      \"Content-Type\": \"application/json\",\n    },\n    method: \"POST\",\n  });\n\n  if (!response.ok) {\n    throw new Error(\n      (await response.text()) || \"Failed to create a customer-memory thread.\"\n    );\n  }\n\n  return (await response.json()) as CustomerMemorySessionData;\n}\n\nfunction getDefaultCustomerMemorySelection() {\n  return {\n    customerId: customerMemoryProfiles[0]?.id ?? \"acme-co\",\n    threadId: null as string | null,\n  };\n}\n\nfunction getStoredCustomerMemorySelection() {\n  if (typeof window === \"undefined\") {\n    return getDefaultCustomerMemorySelection();\n  }\n\n  return {\n    customerId:\n      window.localStorage.getItem(\"customer-memory-agent.customer-id\") ??\n      getDefaultCustomerMemorySelection().customerId,\n    threadId:\n      window.localStorage.getItem(\"customer-memory-agent.thread-id\") ?? null,\n  };\n}\n\nfunction persistCustomerMemorySelection(input: {\n  customerId: string;\n  threadId: string | null;\n}) {\n  if (typeof window === \"undefined\") {\n    return;\n  }\n\n  window.localStorage.setItem(\n    \"customer-memory-agent.customer-id\",\n    input.customerId\n  );\n\n  if (input.threadId) {\n    window.localStorage.setItem(\n      \"customer-memory-agent.thread-id\",\n      input.threadId\n    );\n    return;\n  }\n\n  window.localStorage.removeItem(\"customer-memory-agent.thread-id\");\n}\n\nfunction reportCustomerMemorySessionRefreshError(error: unknown) {\n  console.error(\"Failed to refresh customer-memory session.\", error);\n}\n\nexport interface CustomerMemorySessionController {\n  activeThreadId: string | null;\n  chatErrorMessage: string | null;\n  createThread: () => Promise<void>;\n  customerId: string;\n  isBusy: boolean;\n  isReadonlyAccount: boolean;\n  isReady: boolean;\n  isSessionLoading: boolean;\n  latestPrompt: string;\n  messages: UIMessage[];\n  refreshSession: (query?: string) => Promise<void>;\n  regenerateLastTurn: () => Promise<void>;\n  selectCustomer: (customerId: string) => Promise<void>;\n  selectThread: (threadId: string) => Promise<void>;\n  sendPrompt: (text: string) => Promise<void>;\n  session: CustomerMemorySessionData | null;\n  sessionErrorMessage: string | null;\n  status: ChatStatus;\n  stopChat: () => void;\n  viewState: ReturnType<typeof buildCustomerMemorySessionViewState>;\n}\n\nexport function resolveCustomerMemorySessionMessages(input: {\n  fallbackMessages?: UIMessage[];\n  nextSession: CustomerMemorySessionData;\n}) {\n  const fallbackMessages = removeEmptyCustomerMemoryAssistantMessages(\n    input.fallbackMessages ?? []\n  );\n\n  if (fallbackMessages.length <= input.nextSession.messages.length) {\n    return input.nextSession.messages;\n  }\n\n  return fallbackMessages;\n}\n\nexport function shouldApplyCustomerMemorySessionRefresh(input: {\n  latestRequestId: number;\n  requestId: number;\n}) {\n  return input.requestId === input.latestRequestId;\n}\n\nexport function useCustomerMemorySession(\n  isChatAvailable: boolean\n): CustomerMemorySessionController {\n  const initialSelection = useMemo(getDefaultCustomerMemorySelection, []);\n  const [customerId, setCustomerId] = useState(initialSelection.customerId);\n  const [activeThreadId, setActiveThreadId] = useState<string | null>(null);\n  const [session, setSession] = useState<CustomerMemorySessionData | null>(\n    null\n  );\n  const [sessionErrorMessage, setSessionErrorMessage] = useState<string | null>(\n    null\n  );\n  const [isSessionLoading, setIsSessionLoading] = useState(true);\n  const selectionRef = useRef({\n    customerId: initialSelection.customerId,\n    threadId: null as string | null,\n  });\n  const latestRefreshRequestIdRef = useRef(0);\n  const activeProfile = useMemo(\n    () =>\n      customerMemoryProfiles.find((profile) => profile.id === customerId) ??\n      customerMemoryProfiles[0],\n    [customerId]\n  );\n  const isReadonlyAccount = activeProfile?.accessMode === \"shared_readonly\";\n  const {\n    clearError,\n    error,\n    isBusy,\n    messages,\n    regenerate,\n    sendMessage,\n    setMessages,\n    status,\n    stop,\n  } = useCustomerMemoryChat<UIMessage>({\n    createChat: () =>\n      new Chat<UIMessage>({\n        onFinish: ({ messages }) => {\n          const latestPrompt = getLatestCustomerMemoryPrompt(messages);\n\n          refreshSessionRef\n            .current(latestPrompt, {\n              fallbackMessages: messages,\n            })\n            .catch(reportCustomerMemorySessionRefreshError);\n        },\n        transport: new DefaultChatTransport({\n          api: \"/api/demos/customer-memory-agent\",\n          prepareSendMessagesRequest({ body, messages }) {\n            const threadId = selectionRef.current.threadId;\n\n            if (!threadId) {\n              throw new Error(\n                \"No active customer-memory thread is selected for this request.\"\n              );\n            }\n\n            return {\n              body: {\n                ...body,\n                customerId: selectionRef.current.customerId,\n                messages,\n                threadId,\n              },\n            };\n          },\n        }),\n      }),\n  });\n  const latestPrompt = useMemo(\n    () => getLatestCustomerMemoryPrompt(messages),\n    [messages]\n  );\n  const refreshSessionRef = useRef<\n    (\n      query?: string,\n      options?: {\n        fallbackMessages?: UIMessage[];\n      }\n    ) => Promise<void>\n  >(async () => undefined);\n\n  const applySession = useCallback(\n    (\n      nextSession: CustomerMemorySessionData,\n      options?: { fallbackMessages?: UIMessage[] }\n    ) => {\n      const resolvedMessages = resolveCustomerMemorySessionMessages({\n        fallbackMessages: options?.fallbackMessages,\n        nextSession,\n      });\n\n      setSession({\n        ...nextSession,\n        messages: resolvedMessages,\n      });\n      setCustomerId(nextSession.customer.id);\n      setActiveThreadId(nextSession.thread.id);\n      selectionRef.current = {\n        customerId: nextSession.customer.id,\n        threadId: nextSession.thread.id,\n      };\n      persistCustomerMemorySelection({\n        customerId: nextSession.customer.id,\n        threadId: nextSession.thread.id,\n      });\n      setMessages(resolvedMessages);\n      clearError();\n    },\n    [clearError, setMessages]\n  );\n\n  const refreshSession = useCallback(\n    async (\n      query = \"\",\n      options?: {\n        fallbackMessages?: UIMessage[];\n      }\n    ) => {\n      const currentSelection = selectionRef.current;\n      const requestId = latestRefreshRequestIdRef.current + 1;\n      latestRefreshRequestIdRef.current = requestId;\n      setIsSessionLoading(true);\n      setSessionErrorMessage(null);\n\n      try {\n        const nextSession = await fetchCustomerMemorySession({\n          customerId: currentSelection.customerId,\n          query,\n          threadId: currentSelection.threadId,\n        });\n\n        if (\n          !shouldApplyCustomerMemorySessionRefresh({\n            latestRequestId: latestRefreshRequestIdRef.current,\n            requestId,\n          })\n        ) {\n          return;\n        }\n\n        applySession(nextSession, {\n          fallbackMessages: options?.fallbackMessages,\n        });\n      } catch (sessionError) {\n        if (\n          !shouldApplyCustomerMemorySessionRefresh({\n            latestRequestId: latestRefreshRequestIdRef.current,\n            requestId,\n          })\n        ) {\n          return;\n        }\n\n        setSessionErrorMessage(\n          sessionError instanceof Error\n            ? sessionError.message\n            : \"Failed to refresh the customer-memory session.\"\n        );\n      } finally {\n        if (\n          shouldApplyCustomerMemorySessionRefresh({\n            latestRequestId: latestRefreshRequestIdRef.current,\n            requestId,\n          })\n        ) {\n          setIsSessionLoading(false);\n        }\n      }\n    },\n    [applySession]\n  );\n\n  refreshSessionRef.current = refreshSession;\n\n  useEffect(() => {\n    const storedSelection = getStoredCustomerMemorySelection();\n\n    selectionRef.current = {\n      customerId: storedSelection.customerId,\n      threadId: storedSelection.threadId,\n    };\n    setCustomerId(storedSelection.customerId);\n    setActiveThreadId(storedSelection.threadId);\n  }, []);\n\n  useEffect(() => {\n    if (!isChatAvailable) {\n      setIsSessionLoading(false);\n      return;\n    }\n\n    refreshSession().catch(reportCustomerMemorySessionRefreshError);\n  }, [isChatAvailable, refreshSession]);\n\n  async function selectCustomer(nextCustomerId: string) {\n    if (isBusy || nextCustomerId === selectionRef.current.customerId) {\n      return;\n    }\n\n    selectionRef.current = {\n      customerId: nextCustomerId,\n      threadId: null,\n    };\n    setCustomerId(nextCustomerId);\n    setActiveThreadId(null);\n    setSession(null);\n    setMessages([]);\n    clearError();\n\n    await refreshSession();\n  }\n\n  async function selectThread(threadId: string) {\n    if (\n      isBusy ||\n      threadId === selectionRef.current.threadId ||\n      !selectionRef.current.customerId\n    ) {\n      return;\n    }\n\n    selectionRef.current = {\n      customerId: selectionRef.current.customerId,\n      threadId,\n    };\n    setActiveThreadId(threadId);\n    setSession(null);\n    setMessages([]);\n    clearError();\n\n    await refreshSession();\n  }\n\n  async function createThread() {\n    if (isBusy || isReadonlyAccount) {\n      return;\n    }\n\n    setIsSessionLoading(true);\n    setSessionErrorMessage(null);\n    setSession(null);\n\n    try {\n      const nextSession = await createCustomerMemoryThread(\n        selectionRef.current.customerId\n      );\n\n      applySession(nextSession);\n    } catch (threadError) {\n      setSessionErrorMessage(\n        threadError instanceof Error\n          ? threadError.message\n          : \"Failed to create a new customer-memory thread.\"\n      );\n    } finally {\n      setIsSessionLoading(false);\n    }\n  }\n\n  async function sendPrompt(text: string) {\n    const trimmedText = text.trim();\n\n    if (!(trimmedText && selectionRef.current.threadId) || isReadonlyAccount) {\n      return;\n    }\n\n    await sendMessage({ text: trimmedText });\n  }\n\n  async function regenerateLastTurn() {\n    if (!selectionRef.current.threadId || isReadonlyAccount) {\n      return;\n    }\n\n    await regenerate();\n  }\n\n  return {\n    activeThreadId,\n    chatErrorMessage: error?.message ?? null,\n    customerId,\n    isReadonlyAccount,\n    isBusy,\n    isReady: isChatAvailable && activeThreadId !== null,\n    isSessionLoading,\n    latestPrompt,\n    messages,\n    session,\n    sessionErrorMessage,\n    status,\n    viewState: buildCustomerMemorySessionViewState(session),\n    createThread,\n    regenerateLastTurn,\n    refreshSession,\n    selectCustomer,\n    selectThread,\n    sendPrompt,\n    stopChat: stop,\n  };\n}\n",
      "type": "registry:component",
      "target": "@components/customer-memory-agent/use-customer-memory-session.ts"
    },
    {
      "path": "registry/customer-memory-agent/components/ai-elements/prompt-input.tsx",
      "content": "\"use client\";\n\nimport { CornerDownLeftIcon, LoaderCircleIcon, SquareIcon } from \"lucide-react\";\nimport type {\n  ComponentProps,\n  FormEvent,\n  HTMLAttributes,\n  ReactNode,\n  TextareaHTMLAttributes,\n} from \"react\";\nimport {\n  createContext,\n  useCallback,\n  useContext,\n  useMemo,\n  useState,\n} from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { cn } from \"@/lib/utils\";\n\ntype PromptStatus = \"error\" | \"ready\" | \"streaming\" | \"submitted\";\n\ninterface PromptInputContextValue {\n  setText: (text: string) => void;\n  text: string;\n}\n\ninterface PromptInputMessage {\n  text: string;\n}\n\ninterface PromptInputProps\n  extends Omit<HTMLAttributes<HTMLFormElement>, \"onSubmit\"> {\n  children: ReactNode;\n  onSubmit: (\n    message: PromptInputMessage,\n    event: FormEvent<HTMLFormElement>\n  ) => void | Promise<void>;\n}\n\ninterface PromptInputSubmitProps\n  extends Omit<ComponentProps<typeof Button>, \"children\" | \"type\"> {\n  onStop?: () => void;\n  status?: PromptStatus;\n}\n\ntype PromptInputClickEvent = Parameters<\n  Exclude<ComponentProps<typeof Button>[\"onClick\"], undefined>\n>[0];\n\nconst PromptInputContext = createContext<PromptInputContextValue | null>(null);\n\nfunction usePromptInputContext() {\n  const context = useContext(PromptInputContext);\n\n  if (!context) {\n    throw new Error(\n      \"PromptInput components must be used inside <PromptInput>.\"\n    );\n  }\n\n  return context;\n}\n\nexport function PromptInput({\n  children,\n  className,\n  onSubmit,\n  ...props\n}: PromptInputProps) {\n  const [text, setText] = useState(\"\");\n\n  const contextValue = useMemo(\n    () => ({\n      setText,\n      text,\n    }),\n    [text]\n  );\n\n  const handleSubmit = useCallback(\n    async (event: FormEvent<HTMLFormElement>) => {\n      event.preventDefault();\n\n      const nextText = text.trim();\n      if (!nextText) {\n        return;\n      }\n\n      const result = onSubmit({ text: nextText }, event);\n\n      try {\n        await result;\n        setText(\"\");\n      } catch {\n        // Keep the text for retry after a failed submit.\n      }\n    },\n    [onSubmit, text]\n  );\n\n  return (\n    <PromptInputContext.Provider value={contextValue}>\n      <form\n        className={cn(\"w-full\", className)}\n        onSubmit={handleSubmit}\n        {...props}\n      >\n        {children}\n      </form>\n    </PromptInputContext.Provider>\n  );\n}\n\nexport function PromptInputBody({\n  className,\n  ...props\n}: HTMLAttributes<HTMLDivElement>) {\n  return <div className={cn(\"grid gap-3\", className)} {...props} />;\n}\n\nexport function PromptInputFooter({\n  className,\n  ...props\n}: HTMLAttributes<HTMLDivElement>) {\n  return <div className={cn(className)} {...props} />;\n}\n\nexport function PromptInputTextarea({\n  className,\n  disabled,\n  onChange,\n  ...props\n}: TextareaHTMLAttributes<HTMLTextAreaElement>) {\n  const context = usePromptInputContext();\n\n  return (\n    <Textarea\n      className={cn(\"min-h-16 resize-none\", className)}\n      disabled={disabled}\n      onChange={(event) => {\n        context.setText(event.currentTarget.value);\n        onChange?.(event);\n      }}\n      value={context.text}\n      {...props}\n    />\n  );\n}\n\nexport function PromptInputSubmit({\n  className,\n  disabled,\n  onClick,\n  onStop,\n  size = \"icon\",\n  status = \"ready\",\n  variant = \"default\",\n  ...props\n}: PromptInputSubmitProps) {\n  const { text } = usePromptInputContext();\n  const isBusy = status === \"submitted\" || status === \"streaming\";\n  const isDisabled = Boolean(disabled) || (!isBusy && text.trim().length === 0);\n\n  let icon = <CornerDownLeftIcon className=\"size-4\" />;\n\n  if (status === \"submitted\") {\n    icon = <LoaderCircleIcon className=\"size-4 animate-spin\" />;\n  } else if (status === \"streaming\") {\n    icon = <SquareIcon className=\"size-4\" />;\n  }\n\n  return (\n    <Button\n      aria-label={isBusy ? \"Stop\" : \"Submit\"}\n      className={cn(className)}\n      disabled={isDisabled}\n      onClick={(event: PromptInputClickEvent) => {\n        if (isBusy && onStop) {\n          event.preventDefault();\n          onStop();\n          return;\n        }\n\n        onClick?.(event);\n      }}\n      size={size}\n      type={isBusy && onStop ? \"button\" : \"submit\"}\n      variant={variant}\n      {...props}\n    >\n      {icon}\n    </Button>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/ai-elements/prompt-input.tsx"
    },
    {
      "path": "registry/customer-memory-agent/lib/customer-memory-agent/compaction-store.ts",
      "content": "import { desc, eq } from \"drizzle-orm\";\n\nimport { loadCustomerMemoryAgentDatabase } from \"./database\";\nimport { getCustomerMemoryAgentEnv } from \"./env\";\nimport { createPortableCustomerMemoryCompactionPersistence } from \"./portable-store\";\n\nexport interface CustomerMemoryCompactionRecord {\n  createdAt: string;\n  id: string;\n  messageCount: number;\n  summary: string;\n  threadId: string;\n}\n\nexport interface CustomerMemoryCompactionPersistence {\n  createCompaction(input: {\n    messageCount: number;\n    summary: string;\n    threadId: string;\n  }): Promise<CustomerMemoryCompactionRecord>;\n  getLatestCompaction(\n    threadId: string\n  ): Promise<CustomerMemoryCompactionRecord | null>;\n}\n\ninterface CustomerMemoryCompactionStoreDependencies {\n  persistence?: CustomerMemoryCompactionPersistence;\n}\n\ninterface CustomerMemoryDatabaseModule {\n  customerMemoryCompactions: Awaited<\n    ReturnType<typeof loadCustomerMemoryAgentDatabase>\n  >[\"customerMemoryCompactions\"];\n  database: Awaited<\n    ReturnType<typeof loadCustomerMemoryAgentDatabase>\n  >[\"database\"];\n}\n\nfunction toIsoString(value: Date | string) {\n  return value instanceof Date ? value.toISOString() : value;\n}\n\nfunction normalizeCompactionRecord(record: {\n  createdAt: Date | string;\n  id: string;\n  messageCount: number;\n  summary: string;\n  threadId: string;\n}): CustomerMemoryCompactionRecord {\n  return {\n    createdAt: toIsoString(record.createdAt),\n    id: record.id,\n    messageCount: record.messageCount,\n    summary: record.summary,\n    threadId: record.threadId,\n  };\n}\n\nfunction createDatabaseBackedPersistence(): CustomerMemoryCompactionPersistence {\n  return {\n    async createCompaction(input) {\n      const { customerMemoryCompactions, database } =\n        await loadCustomerMemoryAgentDatabase();\n      const [row] = await database\n        .insert(customerMemoryCompactions)\n        .values({\n          messageCount: input.messageCount,\n          summary: input.summary,\n          threadId: input.threadId,\n        })\n        .returning();\n\n      if (!row) {\n        throw new Error(\"Failed to save a customer-memory compaction.\");\n      }\n\n      return normalizeCompactionRecord(row);\n    },\n    async getLatestCompaction(threadId) {\n      const { customerMemoryCompactions, database } =\n        await loadCustomerMemoryAgentDatabase();\n      const [row] = await database\n        .select()\n        .from(customerMemoryCompactions)\n        .where(eq(customerMemoryCompactions.threadId, threadId))\n        .orderBy(desc(customerMemoryCompactions.createdAt))\n        .limit(1);\n\n      return row ? normalizeCompactionRecord(row) : null;\n    },\n  };\n}\n\nexport function shouldCompactCustomerMemoryThread(input: {\n  messageCount: number;\n  threshold: number;\n}) {\n  return input.messageCount >= input.threshold;\n}\n\nexport function createCustomerMemoryCompactionStore(\n  dependencies: CustomerMemoryCompactionStoreDependencies = {}\n) {\n  const persistence =\n    dependencies.persistence ??\n    (getCustomerMemoryAgentEnv().DATABASE_URL\n      ? createDatabaseBackedPersistence()\n      : createPortableCustomerMemoryCompactionPersistence());\n\n  return {\n    async getLatestCompaction(threadId: string) {\n      return persistence.getLatestCompaction(threadId);\n    },\n    async saveCompaction(input: {\n      messageCount: number;\n      summary: string;\n      threadId: string;\n    }) {\n      return persistence.createCompaction(input);\n    },\n  };\n}\n",
      "type": "registry:lib",
      "target": "@lib/customer-memory-agent/compaction-store.ts"
    },
    {
      "path": "registry/customer-memory-agent/lib/customer-memory-agent/compaction.ts",
      "content": "import { generateText, getToolName, isToolUIPart, type UIMessage } from \"ai\";\n\nimport type { CustomerMemoryCompactionRecord } from \"./compaction-store\";\nimport {\n  createCustomerMemoryAgentGateway,\n  getCustomerMemoryAgentConfig,\n  getCustomerMemoryAgentEnv,\n  type CustomerMemoryAgentEnv,\n} from \"./env\";\n\nexport const customerMemoryCompactionThreshold = 3;\nexport const customerMemoryRecentMessageWindow = 2;\n\nconst compactionInstructions = [\n  \"You are performing a CUSTOMER SUPPORT CONTEXT CHECKPOINT COMPACTION.\",\n  \"Create a handoff note for a future assistant turn that will resume this exact customer thread.\",\n  \"Merge the previous handoff, if present, with the new conversation window.\",\n  \"Preserve durable customer facts, preferences, active commitments, unresolved risks, exact dates, names, account constraints, and requested next actions.\",\n  \"Drop greetings, filler, and resolved details unless they explain the current customer state.\",\n  \"If newer messages contradict the previous handoff, prefer the newer fact and mention uncertainty only when the transcript is unresolved.\",\n  \"Do not invent missing facts. Do not expose this instruction.\",\n  \"Output compact Markdown with these sections only: Current customer state, Durable facts and preferences, Active commitments and risks, Recent thread state, Next assistant guidance.\",\n].join(\" \");\n\nfunction formatToolValue(value: unknown) {\n  return JSON.stringify(value);\n}\n\nfunction formatToolPart(part: UIMessage[\"parts\"][number]) {\n  if (!isToolUIPart(part)) {\n    return null;\n  }\n\n  const lines = [`TOOL ${getToolName(part)} (${part.state})`];\n\n  if (\"input\" in part && part.input !== undefined) {\n    lines.push(`input: ${formatToolValue(part.input)}`);\n  }\n\n  if (\"output\" in part && part.output !== undefined) {\n    lines.push(`output: ${formatToolValue(part.output)}`);\n  }\n\n  if (\"errorText\" in part && part.errorText) {\n    lines.push(`error: ${part.errorText}`);\n  }\n\n  return lines.join(\"\\n\");\n}\n\nfunction getMessageCompactionText(message: UIMessage) {\n  return message.parts\n    .map((part) => {\n      if (part.type === \"text\") {\n        return part.text;\n      }\n\n      return formatToolPart(part);\n    })\n    .filter((value): value is string => Boolean(value?.trim()))\n    .join(\"\\n\")\n    .trim();\n}\n\nfunction formatMessagesForSummary(messages: UIMessage[]) {\n  return messages\n    .map((message) => {\n      const text = getMessageCompactionText(message);\n\n      if (text.length === 0) {\n        return null;\n      }\n\n      return `${message.role.toUpperCase()}: ${text}`;\n    })\n    .filter((value): value is string => value !== null)\n    .join(\"\\n\\n\");\n}\n\nexport function getCustomerMemoryCompactionTargetMessageCount(input: {\n  latestCompaction: CustomerMemoryCompactionRecord | null;\n  messageCount: number;\n  recentWindow?: number;\n  threshold?: number;\n}) {\n  const recentWindow = input.recentWindow ?? customerMemoryRecentMessageWindow;\n  const threshold = input.threshold ?? customerMemoryCompactionThreshold;\n\n  if (input.messageCount < threshold) {\n    return null;\n  }\n\n  const targetCount = input.messageCount - recentWindow;\n\n  if (targetCount <= 0) {\n    return null;\n  }\n\n  if (\n    input.latestCompaction &&\n    input.latestCompaction.messageCount >= targetCount\n  ) {\n    return null;\n  }\n\n  return targetCount;\n}\n\nexport function buildCustomerMemoryCompactionInput(input: {\n  latestCompaction: CustomerMemoryCompactionRecord | null;\n  messages: UIMessage[];\n  targetMessageCount: number;\n}) {\n  const previousMessageCount = input.latestCompaction?.messageCount ?? 0;\n  const startIndex = Math.min(previousMessageCount, input.targetMessageCount);\n  const messages = input.messages.slice(startIndex, input.targetMessageCount);\n\n  if (messages.length === 0) {\n    throw new Error(\n      \"Cannot compact without newly compactable customer-memory messages.\"\n    );\n  }\n\n  return {\n    messages,\n    previousHandoff: input.latestCompaction?.summary ?? null,\n  };\n}\n\nexport async function generateCustomerMemoryCompactionSummary(\n  input: {\n    customerLabel: string;\n    messages: UIMessage[];\n    previousHandoff?: string | null;\n  },\n  env: CustomerMemoryAgentEnv = getCustomerMemoryAgentEnv()\n) {\n  const gateway = createCustomerMemoryAgentGateway(env);\n  const { chatModel } = getCustomerMemoryAgentConfig(env);\n  const messageTranscript = formatMessagesForSummary(input.messages);\n\n  if (messageTranscript.length === 0) {\n    throw new Error(\"Cannot compact an empty customer-memory message window.\");\n  }\n\n  const result = await generateText({\n    model: gateway(chatModel),\n    prompt: [\n      `Customer account: ${input.customerLabel}`,\n      \"Previous handoff:\",\n      input.previousHandoff?.trim() || \"No previous handoff exists.\",\n      \"New conversation window:\",\n      messageTranscript,\n    ].join(\"\\n\\n\"),\n    system: compactionInstructions,\n  });\n\n  return result.text.trim();\n}\n",
      "type": "registry:lib",
      "target": "@lib/customer-memory-agent/compaction.ts"
    },
    {
      "path": "registry/customer-memory-agent/lib/customer-memory-agent/contract.ts",
      "content": "import { validateUIMessages, type UIMessage } from \"ai\";\n\nexport const invalidCustomerIdError =\n  'Expected a non-empty \"customerId\" string.';\nexport const invalidMessagesError =\n  'Expected a JSON body with a \"messages\" array.';\nexport const invalidThreadIdError = 'Expected a non-empty \"threadId\" string.';\nexport const invalidUiMessagesError =\n  'Expected each \"messages\" entry to match the UIMessage format.';\nexport const malformedJsonError = \"Expected a valid JSON request body.\";\n\ninterface CustomerMemoryChatRequestBody {\n  customerId?: string;\n  messages?: UIMessage[];\n  threadId?: string;\n}\n\nfunction readNonEmptyString(value: unknown, errorMessage: string) {\n  if (typeof value !== \"string\" || value.trim().length === 0) {\n    throw new Error(errorMessage);\n  }\n\n  return value.trim();\n}\n\nexport async function readCustomerMemoryChatRequest(body: unknown): Promise<{\n  customerId: string;\n  messages: UIMessage[];\n  threadId: string;\n}> {\n  const { customerId, messages, threadId } = (body ??\n    {}) as CustomerMemoryChatRequestBody;\n\n  if (!Array.isArray(messages)) {\n    throw new Error(invalidMessagesError);\n  }\n\n  const normalizedCustomerId = readNonEmptyString(\n    customerId,\n    invalidCustomerIdError\n  );\n  const normalizedThreadId = readNonEmptyString(threadId, invalidThreadIdError);\n\n  try {\n    return {\n      customerId: normalizedCustomerId,\n      messages: await validateUIMessages({ messages }),\n      threadId: normalizedThreadId,\n    };\n  } catch {\n    throw new Error(invalidUiMessagesError);\n  }\n}\n\nexport function readCustomerMemorySessionQuery(requestUrl: string | URL): {\n  customerId: string;\n  query: string;\n  threadId: string | null;\n} {\n  const url = requestUrl instanceof URL ? requestUrl : new URL(requestUrl);\n  const customerId = readNonEmptyString(\n    url.searchParams.get(\"customerId\"),\n    invalidCustomerIdError\n  );\n  const threadId = url.searchParams.get(\"threadId\");\n\n  return {\n    customerId,\n    query: url.searchParams.get(\"query\")?.trim() ?? \"\",\n    threadId: threadId && threadId.trim().length > 0 ? threadId.trim() : null,\n  };\n}\n\nexport async function readCustomerMemoryThreadCreateRequest(body: unknown) {\n  const { customerId } = (body ?? {}) as { customerId?: string };\n\n  return {\n    customerId: readNonEmptyString(customerId, invalidCustomerIdError),\n  };\n}\n",
      "type": "registry:lib",
      "target": "@lib/customer-memory-agent/contract.ts"
    },
    {
      "path": "registry/customer-memory-agent/lib/customer-memory-agent/conversation.ts",
      "content": "import {\n  consumeStream,\n  convertToModelMessages,\n  createIdGenerator,\n  stepCountIs,\n  streamText,\n  tool,\n  type UIMessage,\n  type UIMessageStreamOnFinishCallback,\n} from \"ai\";\nimport { z } from \"zod\";\n\nimport type { CustomerMemoryProfile } from \"./customer-profiles\";\nimport {\n  buildCustomerMemoryCompactionInput,\n  customerMemoryCompactionThreshold,\n  customerMemoryRecentMessageWindow,\n  generateCustomerMemoryCompactionSummary,\n  getCustomerMemoryCompactionTargetMessageCount,\n} from \"./compaction\";\nimport {\n  type CustomerMemoryCompactionRecord,\n  createCustomerMemoryCompactionStore,\n} from \"./compaction-store\";\nimport {\n  createCustomerMemoryAgentGateway,\n  getCustomerMemoryAgentConfig,\n  getCustomerMemoryAgentEnv,\n  type CustomerMemoryAgentEnv,\n} from \"./env\";\nimport { createCustomerMemoryLifecycle } from \"./memory-lifecycle\";\nimport {\n  findRelevantCustomerMemory,\n  type RetrievedCustomerMemory,\n} from \"./memory-recall\";\nimport type { CustomerMemoryRecord } from \"./memory-store\";\nimport { createCustomerMemoryThreadStore } from \"./thread-store\";\n\nconst customerMemoryInstructions = [\n  \"You are the customer-memory-agent demo for a customer success and support team.\",\n  \"Carry forward durable customer context across sessions, while keeping normal replies concise.\",\n  \"Use the manageCustomerMemory tool when the user shares, corrects, or removes a stable preference, long-lived constraint, promised follow-up, recurring risk, or durable account fact.\",\n  \"Prefer updating an existing relevant memory over adding a near-duplicate memory.\",\n  \"Do not save transient greetings, one-off pleasantries, or information that is already obviously captured elsewhere in the thread.\",\n  \"When a retrieved memory conflicts with the latest user instruction, follow the latest instruction and explain the change.\",\n  \"If a memory is relevant, use it in the answer without repeating the whole memory panel.\",\n].join(\" \");\n\nconst memoryCategorySchema = z.enum([\n  \"constraint\",\n  \"preference\",\n  \"promise\",\n  \"risk\",\n  \"fact\",\n  \"follow_up\",\n]);\n\nconst memoryOperationInputSchema = z.discriminatedUnion(\"operation\", [\n  z.object({\n    category: memoryCategorySchema,\n    content: z\n      .string()\n      .min(10)\n      .describe(\"The durable customer memory to store.\"),\n    operation: z.literal(\"add\"),\n    reason: z.string().min(8).describe(\"Why this should be remembered.\"),\n    sourceMessageId: z\n      .string()\n      .nullable()\n      .describe(\"The message ID that justified this memory, when known.\"),\n    title: z.string().min(3).describe(\"A short label for the memory panel.\"),\n  }),\n  z.object({\n    category: memoryCategorySchema.optional(),\n    content: z\n      .string()\n      .min(10)\n      .describe(\"The revised durable customer memory.\"),\n    memoryId: z.string().min(1),\n    operation: z.literal(\"update\"),\n    reason: z\n      .string()\n      .min(8)\n      .describe(\"Why this replaces or enriches the existing memory.\"),\n    sourceMessageId: z.string().nullable(),\n    title: z.string().min(3).optional(),\n  }),\n  z.object({\n    memoryId: z.string().min(1),\n    operation: z.literal(\"delete\"),\n    reason: z.string().min(8).describe(\"Why this memory should be hidden.\"),\n    sourceMessageId: z.string().nullable(),\n  }),\n  z.object({\n    operation: z.literal(\"noop\"),\n    reason: z\n      .string()\n      .min(8)\n      .describe(\"Why no durable customer memory should change.\"),\n    sourceMessageId: z.string().nullable(),\n  }),\n]);\n\nexport const memoryOperationToolInputSchema = z.object({\n  category: memoryCategorySchema\n    .optional()\n    .describe(\"Required when adding a memory, optional when updating one.\"),\n  content: z\n    .string()\n    .min(10)\n    .optional()\n    .describe(\"Required for add and update operations.\"),\n  memoryId: z\n    .string()\n    .min(1)\n    .optional()\n    .describe(\"Required for update and delete operations.\"),\n  operation: z\n    .enum([\"add\", \"update\", \"delete\", \"noop\"])\n    .describe(\"The memory lifecycle operation to perform.\"),\n  reason: z.string().min(8).describe(\"Why this memory operation is needed.\"),\n  sourceMessageId: z\n    .string()\n    .nullable()\n    .optional()\n    .describe(\"The message ID that justified this operation, when known.\"),\n  title: z\n    .string()\n    .min(3)\n    .optional()\n    .describe(\"Required when adding a memory, optional when updating one.\"),\n});\n\ntype MemoryOperationToolInput = z.infer<typeof memoryOperationToolInputSchema>;\n\ninterface PendingMemoryOperation {\n  category: z.infer<typeof memoryCategorySchema>;\n  content: string;\n  memoryId: string | null;\n  operation: \"add\" | \"update\" | \"delete\" | \"noop\";\n  reason: string;\n  sourceMessageId: string | null;\n  title: string;\n}\n\ninterface StreamCustomerMemoryConversationInput {\n  customer: CustomerMemoryProfile;\n  messages: UIMessage[];\n  threadId: string;\n  visitorId: string;\n}\n\ninterface CreateConversationResponseInput {\n  messages: UIMessage[];\n  onFinish: UIMessageStreamOnFinishCallback<UIMessage>;\n  originalMessages: UIMessage[];\n  queueMemoryDraft: (draft: MemoryOperationToolInput) => Promise<{\n    accepted: true;\n    operation: PendingMemoryOperation[\"operation\"];\n    title: string | null;\n  }>;\n  systemPrompt: string;\n}\n\ninterface StreamCustomerMemoryConversationDependencies {\n  applyMemoryOperations?: (\n    operations: PendingMemoryOperation[],\n    input: StreamCustomerMemoryConversationInput\n  ) => Promise<CustomerMemoryRecord[]>;\n  createConversationResponse?: (\n    input: CreateConversationResponseInput,\n    env: CustomerMemoryAgentEnv\n  ) => Promise<Response>;\n  findRelevantMemory?: typeof findRelevantCustomerMemory;\n  getLatestCompaction?: (\n    threadId: string\n  ) => Promise<CustomerMemoryCompactionRecord | null>;\n  maybeCompactThread?: (input: {\n    customer: CustomerMemoryProfile;\n    latestCompaction: CustomerMemoryCompactionRecord | null;\n    messages: UIMessage[];\n    threadId: string;\n  }) => Promise<void>;\n  saveThreadMessages?: (input: {\n    messages: UIMessage[];\n    threadId: string;\n  }) => Promise<void>;\n}\n\nfunction getMessageText(message: UIMessage) {\n  return message.parts\n    .filter((part) => part.type === \"text\")\n    .map((part) => part.text)\n    .join(\"\\n\")\n    .trim();\n}\n\nfunction getLatestUserPrompt(messages: UIMessage[]) {\n  for (const message of [...messages].reverse()) {\n    if (message.role !== \"user\") {\n      continue;\n    }\n\n    const text = getMessageText(message);\n\n    if (text.length > 0) {\n      return text;\n    }\n  }\n\n  return \"\";\n}\n\nfunction buildContextMessages(\n  messages: UIMessage[],\n  latestCompaction: CustomerMemoryCompactionRecord | null\n) {\n  if (!latestCompaction) {\n    return messages;\n  }\n\n  const startIndex = Math.min(latestCompaction.messageCount, messages.length);\n\n  return messages.slice(startIndex);\n}\n\nfunction formatRelevantMemories(memories: RetrievedCustomerMemory[]) {\n  if (memories.length === 0) {\n    return \"No relevant saved customer memories were recalled for this prompt.\";\n  }\n\n  return memories\n    .map(\n      (memory, index) =>\n        `${index + 1}. [id: ${memory.id}] [${memory.category}] ${memory.title ?? \"Untitled memory\"}: ${memory.content}`\n    )\n    .join(\"\\n\");\n}\n\nfunction buildCustomerMemorySystemPrompt(input: {\n  customer: CustomerMemoryProfile;\n  latestCompaction: CustomerMemoryCompactionRecord | null;\n  relevantMemories: RetrievedCustomerMemory[];\n}) {\n  return [\n    customerMemoryInstructions,\n    `Customer account: ${input.customer.name} (${input.customer.industry})`,\n    `Account summary: ${input.customer.accountSummary}`,\n    `Operating notes:\\n- ${input.customer.operatingNotes.join(\"\\n- \")}`,\n    `Relevant saved memories:\\n${formatRelevantMemories(input.relevantMemories)}`,\n    input.latestCompaction\n      ? `Handoff compaction for older thread context:\\n${input.latestCompaction.summary}`\n      : \"No handoff compaction exists yet for this thread.\",\n  ].join(\"\\n\\n\");\n}\n\nfunction applyCustomerMemoryOperations(\n  operations: PendingMemoryOperation[],\n  input: StreamCustomerMemoryConversationInput\n) {\n  return createCustomerMemoryLifecycle().applyOperations(operations, {\n    customerId: input.customer.id,\n    threadId: input.threadId,\n    visitorId: input.visitorId,\n  });\n}\n\nasync function maybeCompactCustomerMemoryThread(input: {\n  customer: CustomerMemoryProfile;\n  latestCompaction: CustomerMemoryCompactionRecord | null;\n  messages: UIMessage[];\n  threadId: string;\n}) {\n  const targetMessageCount = getCustomerMemoryCompactionTargetMessageCount({\n    latestCompaction: input.latestCompaction,\n    messageCount: input.messages.length,\n    recentWindow: customerMemoryRecentMessageWindow,\n    threshold: customerMemoryCompactionThreshold,\n  });\n\n  if (targetMessageCount === null) {\n    return;\n  }\n\n  const compactionInput = buildCustomerMemoryCompactionInput({\n    latestCompaction: input.latestCompaction,\n    messages: input.messages,\n    targetMessageCount,\n  });\n\n  const summary = await generateCustomerMemoryCompactionSummary({\n    customerLabel: input.customer.name,\n    messages: compactionInput.messages,\n    previousHandoff: compactionInput.previousHandoff,\n  });\n\n  await createCustomerMemoryCompactionStore().saveCompaction({\n    messageCount: targetMessageCount,\n    summary,\n    threadId: input.threadId,\n  });\n}\n\nasync function createDefaultConversationResponse(\n  input: CreateConversationResponseInput,\n  env: CustomerMemoryAgentEnv\n) {\n  const gateway = createCustomerMemoryAgentGateway(env);\n  const { chatModel } = getCustomerMemoryAgentConfig(env);\n  const result = streamText({\n    model: gateway(chatModel),\n    system: input.systemPrompt,\n    messages: await convertToModelMessages(input.messages),\n    stopWhen: stepCountIs(6),\n    tools: {\n      manageCustomerMemory: tool({\n        description:\n          \"Add, update, delete, or skip a durable customer memory. Update an existing memory when the new fact corrects or enriches it.\",\n        inputSchema: memoryOperationToolInputSchema,\n        execute: input.queueMemoryDraft,\n      }),\n    },\n  });\n\n  return result.toUIMessageStreamResponse({\n    consumeSseStream: consumeStream,\n    generateMessageId: createIdGenerator({\n      prefix: \"cm-msg\",\n      size: 16,\n    }),\n    onFinish: input.onFinish,\n    originalMessages: input.originalMessages,\n  });\n}\n\nexport async function streamCustomerMemoryConversation(\n  input: StreamCustomerMemoryConversationInput,\n  env: CustomerMemoryAgentEnv = getCustomerMemoryAgentEnv(),\n  dependencies: StreamCustomerMemoryConversationDependencies = {}\n) {\n  const latestUserPrompt = getLatestUserPrompt(input.messages);\n  const getLatestCompaction =\n    dependencies.getLatestCompaction ??\n    ((threadId: string) =>\n      createCustomerMemoryCompactionStore().getLatestCompaction(threadId));\n  const relevantMemories = await (\n    dependencies.findRelevantMemory ?? findRelevantCustomerMemory\n  )(\n    {\n      customerId: input.customer.id,\n      query: latestUserPrompt,\n      visitorId: input.visitorId,\n    },\n    env\n  );\n  const latestCompaction = await getLatestCompaction(input.threadId);\n  const pendingMemoryOperations: PendingMemoryOperation[] = [];\n  const contextMessages = buildContextMessages(\n    input.messages,\n    latestCompaction\n  );\n  const createConversationResponse =\n    dependencies.createConversationResponse ??\n    createDefaultConversationResponse;\n\n  return createConversationResponse(\n    {\n      messages: contextMessages,\n      onFinish: async ({ isAborted, messages }) => {\n        if (isAborted) {\n          return;\n        }\n\n        await (\n          dependencies.saveThreadMessages ??\n          ((nextInput: { messages: UIMessage[]; threadId: string }) =>\n            createCustomerMemoryThreadStore().saveThreadMessages(nextInput))\n        )({\n          messages,\n          threadId: input.threadId,\n        });\n\n        await (\n          dependencies.applyMemoryOperations ?? applyCustomerMemoryOperations\n        )(pendingMemoryOperations, input);\n\n        await (\n          dependencies.maybeCompactThread ?? maybeCompactCustomerMemoryThread\n        )({\n          customer: input.customer,\n          latestCompaction,\n          messages,\n          threadId: input.threadId,\n        });\n      },\n      originalMessages: input.messages,\n      queueMemoryDraft: (draft) => {\n        const parsedDraft = memoryOperationInputSchema.parse({\n          ...draft,\n          sourceMessageId: draft.sourceMessageId ?? null,\n        });\n        const normalizedDraft: PendingMemoryOperation = {\n          category:\n            \"category\" in parsedDraft\n              ? (parsedDraft.category ?? \"fact\")\n              : \"fact\",\n          content: \"content\" in parsedDraft ? parsedDraft.content : \"\",\n          memoryId: \"memoryId\" in parsedDraft ? parsedDraft.memoryId : null,\n          operation: parsedDraft.operation,\n          reason: parsedDraft.reason,\n          sourceMessageId: parsedDraft.sourceMessageId,\n          title:\n            \"title\" in parsedDraft\n              ? (parsedDraft.title ?? \"Memory lifecycle event\")\n              : \"\",\n        };\n        pendingMemoryOperations.push(normalizedDraft);\n\n        return Promise.resolve({\n          accepted: true,\n          operation: parsedDraft.operation,\n          title: \"title\" in parsedDraft ? (parsedDraft.title ?? null) : null,\n        });\n      },\n      systemPrompt: buildCustomerMemorySystemPrompt({\n        customer: input.customer,\n        latestCompaction,\n        relevantMemories,\n      }),\n    },\n    env\n  );\n}\n",
      "type": "registry:lib",
      "target": "@lib/customer-memory-agent/conversation.ts"
    },
    {
      "path": "registry/customer-memory-agent/lib/customer-memory-agent/customer-profiles.ts",
      "content": "export interface CustomerMemoryProfile {\n  accessMode: \"shared_readonly\" | \"visitor_private\";\n  accountSummary: string;\n  id: string;\n  industry: string;\n  name: string;\n  operatingNotes: string[];\n}\n\nexport const customerMemoryProfiles: CustomerMemoryProfile[] = [\n  {\n    accessMode: \"visitor_private\",\n    accountSummary:\n      \"Growth-stage healthcare SaaS account with sensitive rollout language, recurring launch coordination, and room for hands-on support rehearsal.\",\n    id: \"demo-sandbox\",\n    industry: \"Healthcare SaaS (free to chat)\",\n    name: \"Brightfield Health\",\n    operatingNotes: [\n      \"Use this account to test memory writes, thread persistence, and compaction end to end.\",\n      \"Every browser keeps its own private threads and saved memories here.\",\n    ],\n  },\n  {\n    accessMode: \"shared_readonly\",\n    accountSummary:\n      \"Enterprise retail account with a strict brand-review process and a low tolerance for accidental over-promising.\",\n    id: \"acme-co\",\n    industry: \"Retail SaaS\",\n    name: \"Acme Co\",\n    operatingNotes: [\n      \"Support needs status updates that executives can forward directly.\",\n      \"Marketing claims usually need legal review before publication.\",\n    ],\n  },\n  {\n    accessMode: \"shared_readonly\",\n    accountSummary:\n      \"Developer tooling customer that values concise technical answers and durable action tracking.\",\n    id: \"helio-dev\",\n    industry: \"Developer Tools\",\n    name: \"Helio Dev\",\n    operatingNotes: [\n      \"They prefer root-cause detail over polished status language.\",\n      \"Follow-up promises should always include a concrete date.\",\n    ],\n  },\n  {\n    accessMode: \"shared_readonly\",\n    accountSummary:\n      \"Logistics platform account with complex escalation paths across operations and finance teams.\",\n    id: \"northstar-logistics\",\n    industry: \"Logistics\",\n    name: \"Northstar Logistics\",\n    operatingNotes: [\n      \"Incident updates need a clear owner and next checkpoint.\",\n      \"Downtime language should be conservative until root cause is confirmed.\",\n    ],\n  },\n];\n\nexport function getCustomerMemoryProfile(customerId: string) {\n  return (\n    customerMemoryProfiles.find((profile) => profile.id === customerId) ?? null\n  );\n}\n\nexport function getVisitorPrivateCustomerMemoryProfileIds() {\n  return customerMemoryProfiles\n    .filter((profile) => profile.accessMode === \"visitor_private\")\n    .map((profile) => profile.id);\n}\n",
      "type": "registry:lib",
      "target": "@lib/customer-memory-agent/customer-profiles.ts"
    },
    {
      "path": "registry/customer-memory-agent/lib/customer-memory-agent/database.ts",
      "content": "import { Pool } from \"@neondatabase/serverless\";\nimport { drizzle } from \"drizzle-orm/neon-serverless\";\n\nimport {\n  getCustomerMemoryAgentDatabaseConfig,\n  getCustomerMemoryAgentEnv,\n  type CustomerMemoryAgentEnv,\n} from \"./env\";\nimport {\n  customerMemoryAgentSchema,\n  customerMemoryCompactions,\n  customerMemoryEmbeddings,\n  customerMemoryEvents,\n  customerMemoryMemories,\n  customerMemoryMessages,\n  customerMemoryThreads,\n} from \"./schema\";\n\nfunction createCustomerMemoryAgentDatabase(connectionString: string) {\n  const client = new Pool({ connectionString });\n  const database = drizzle({\n    client,\n    schema: customerMemoryAgentSchema,\n  });\n\n  return { client, database };\n}\n\ntype CustomerMemoryAgentDatabase = ReturnType<\n  typeof createCustomerMemoryAgentDatabase\n>[\"database\"];\n\nexport interface CustomerMemoryAgentDatabaseModule {\n  customerMemoryCompactions: typeof customerMemoryCompactions;\n  customerMemoryEmbeddings: typeof customerMemoryEmbeddings;\n  customerMemoryEvents: typeof customerMemoryEvents;\n  customerMemoryMemories: typeof customerMemoryMemories;\n  customerMemoryMessages: typeof customerMemoryMessages;\n  customerMemoryThreads: typeof customerMemoryThreads;\n  database: CustomerMemoryAgentDatabase;\n}\n\nlet databaseModulePromise: Promise<CustomerMemoryAgentDatabaseModule> | null =\n  null;\n\nexport async function loadCustomerMemoryAgentDatabase(\n  env: CustomerMemoryAgentEnv = getCustomerMemoryAgentEnv()\n): Promise<CustomerMemoryAgentDatabaseModule> {\n  if (!databaseModulePromise) {\n    const { databaseUrl } = getCustomerMemoryAgentDatabaseConfig(env);\n    const { database } = createCustomerMemoryAgentDatabase(databaseUrl);\n\n    databaseModulePromise = Promise.resolve({\n      customerMemoryCompactions,\n      customerMemoryEmbeddings,\n      customerMemoryEvents,\n      customerMemoryMemories,\n      customerMemoryMessages,\n      customerMemoryThreads,\n      database,\n    });\n  }\n\n  return databaseModulePromise;\n}\n",
      "type": "registry:lib",
      "target": "@lib/customer-memory-agent/database.ts"
    },
    {
      "path": "registry/customer-memory-agent/lib/ai-gateway/contract.ts",
      "content": "import { createGateway } from \"ai\";\n\nexport const DEFAULT_GATEWAY_BASE_URL = \"https://ai-gateway.vercel.sh/v3/ai\";\nexport const MINIMUM_NODE_VERSION = \"22.13.0\";\nconst nodeVersionPattern = /^v?(\\d+)\\.(\\d+)\\.(\\d+)(?:[-+].*)?$/;\n\nexport type AiGatewayEnvRecord = Record<string, string | undefined>;\n\nexport interface ParsedNodeVersion {\n  major: number;\n  minor: number;\n  patch: number;\n}\n\nexport interface AiGatewayContractConfig {\n  apiKey: string;\n  baseURL: string;\n  chatModel: string;\n}\n\nexport interface AiGatewayResolvedEnv {\n  apiKey: string | undefined;\n  baseURL: string;\n  chatModel: string;\n}\n\nexport interface AiGatewaySetupConfig {\n  baseURL: string;\n  chatModel: string;\n}\n\nexport interface AiGatewayContractSetupState<\n  TConfig extends AiGatewaySetupConfig = AiGatewaySetupConfig,\n> {\n  config: TConfig;\n  isReady: boolean;\n  issues: string[];\n  nodeVersion: string;\n}\n\nexport interface AiGatewayContractOptions<\n  TConfig extends AiGatewaySetupConfig = AiGatewaySetupConfig,\n> {\n  buildConfig?: (\n    resolvedEnv: AiGatewayResolvedEnv,\n    env: AiGatewayEnvRecord\n  ) => TConfig;\n  defaultBaseURL?: string;\n  defaultChatModel: string;\n  getAdditionalIssues?: (\n    resolvedEnv: AiGatewayResolvedEnv,\n    env: AiGatewayEnvRecord\n  ) => string[];\n  missingApiKeyError: string;\n  missingApiKeyIssue?: string;\n}\n\nconst genericMissingApiKeyIssue =\n  \"AI_GATEWAY_API_KEY is missing. The demo can render, but chat requests will fail until it is configured.\";\n\nexport function parseNodeVersion(version: string): ParsedNodeVersion {\n  const match = nodeVersionPattern.exec(version);\n  const major = Number(match?.[1]);\n  const minor = Number(match?.[2]);\n  const patch = Number(match?.[3]);\n\n  if (![major, minor, patch].every(Number.isInteger)) {\n    throw new Error(`Unable to parse Node.js version: \"${version}\".`);\n  }\n\n  return { major, minor, patch };\n}\n\nexport function getNodeMajor(version: string): number {\n  return parseNodeVersion(version).major;\n}\n\nfunction compareNodeVersions(\n  left: ParsedNodeVersion,\n  right: ParsedNodeVersion\n): number {\n  if (left.major !== right.major) {\n    return left.major - right.major;\n  }\n\n  if (left.minor !== right.minor) {\n    return left.minor - right.minor;\n  }\n\n  return left.patch - right.patch;\n}\n\nexport function assertSupportedNodeRuntime(version = process.version): number {\n  const parsedVersion = parseNodeVersion(version);\n  const minimumVersion = parseNodeVersion(MINIMUM_NODE_VERSION);\n\n  if (compareNodeVersions(parsedVersion, minimumVersion) < 0) {\n    throw new Error(\n      `Node.js ${version} is unsupported. This demo workspace requires Node.js >=${MINIMUM_NODE_VERSION}.`\n    );\n  }\n\n  return parsedVersion.major;\n}\n\nexport function resolveAiGatewayContractEnv(\n  env: AiGatewayEnvRecord,\n  options: Pick<AiGatewayContractOptions, \"defaultBaseURL\" | \"defaultChatModel\">\n): AiGatewayResolvedEnv {\n  return {\n    apiKey: env.AI_GATEWAY_API_KEY,\n    baseURL:\n      env.AI_GATEWAY_BASE_URL ||\n      options.defaultBaseURL ||\n      DEFAULT_GATEWAY_BASE_URL,\n    chatModel: env.AI_GATEWAY_CHAT_MODEL || options.defaultChatModel,\n  };\n}\n\nexport function readAiGatewayContractConfig(\n  env: AiGatewayEnvRecord,\n  options: AiGatewayContractOptions\n): AiGatewayContractConfig {\n  assertSupportedNodeRuntime();\n  const resolvedEnv = resolveAiGatewayContractEnv(env, options);\n\n  if (!resolvedEnv.apiKey) {\n    throw new Error(options.missingApiKeyError);\n  }\n\n  return {\n    apiKey: resolvedEnv.apiKey,\n    baseURL: resolvedEnv.baseURL,\n    chatModel: resolvedEnv.chatModel,\n  };\n}\n\nexport function buildAiGatewayContractSetupState<\n  TConfig extends AiGatewaySetupConfig,\n>(\n  env: AiGatewayEnvRecord,\n  options: AiGatewayContractOptions<TConfig>\n): AiGatewayContractSetupState<TConfig> {\n  const issues: string[] = [];\n  const resolvedEnv = resolveAiGatewayContractEnv(env, options);\n\n  try {\n    assertSupportedNodeRuntime();\n  } catch (error) {\n    issues.push(\n      error instanceof Error ? error.message : \"Unsupported Node.js runtime.\"\n    );\n  }\n\n  if (!resolvedEnv.apiKey) {\n    issues.push(options.missingApiKeyIssue || genericMissingApiKeyIssue);\n  }\n\n  issues.push(...(options.getAdditionalIssues?.(resolvedEnv, env) ?? []));\n\n  return {\n    config:\n      options.buildConfig?.(resolvedEnv, env) ??\n      ({\n        baseURL: resolvedEnv.baseURL,\n        chatModel: resolvedEnv.chatModel,\n      } as TConfig),\n    isReady: issues.length === 0,\n    issues,\n    nodeVersion: process.version,\n  };\n}\n\nexport function createAiGatewayFromContract(\n  env: AiGatewayEnvRecord,\n  options: AiGatewayContractOptions\n): ReturnType<typeof createGateway> {\n  const { apiKey, baseURL } = readAiGatewayContractConfig(env, options);\n\n  return createGateway({\n    apiKey,\n    baseURL,\n  });\n}\n",
      "type": "registry:lib",
      "target": "@lib/ai-gateway/contract.ts"
    },
    {
      "path": "registry/customer-memory-agent/lib/customer-memory-agent/env-source.ts",
      "content": "export function getCustomerMemoryAgentAppEnv() {\n  // biome-ignore lint/style/noProcessEnv: Registry source installs into consumer apps without this repo's env wrapper.\n  return process.env;\n}\n",
      "type": "registry:lib",
      "target": "@lib/customer-memory-agent/env-source.ts"
    },
    {
      "path": "registry/customer-memory-agent/lib/customer-memory-agent/env.ts",
      "content": "import { getCustomerMemoryAgentAppEnv } from \"./env-source\";\nimport {\n  buildAiGatewayContractSetupState,\n  createAiGatewayFromContract,\n  readAiGatewayContractConfig,\n  type AiGatewayContractConfig,\n  type AiGatewayContractSetupState,\n  type AiGatewayEnvRecord,\n  type AiGatewaySetupConfig,\n} from \"@/lib/ai-gateway/contract\";\n\nconst DEFAULT_CUSTOMER_MEMORY_AGENT_CHAT_MODEL = \"openai/gpt-4.1-mini\";\nconst missingDatabaseUrlIssue =\n  \"DATABASE_URL is missing. The registry demo will use in-memory persistence until Postgres is configured.\";\n\nexport type CustomerMemoryAgentEnv = AiGatewayEnvRecord;\n\nexport interface CustomerMemoryAgentConfig extends AiGatewayContractConfig {\n  databaseUrl: string | undefined;\n}\n\nexport type CustomerMemoryAgentSetupState = AiGatewayContractSetupState<AiGatewaySetupConfig>;\n\nexport type CustomerMemoryAgentGateway = ReturnType<\n  typeof createAiGatewayFromContract\n>;\n\nconst customerMemoryAgentContract = {\n  defaultChatModel: DEFAULT_CUSTOMER_MEMORY_AGENT_CHAT_MODEL,\n  missingApiKeyError:\n    \"Missing AI_GATEWAY_API_KEY. Add it to .env.local before using the memory and persistence agent.\",\n  missingApiKeyIssue:\n    \"AI_GATEWAY_API_KEY is missing. The demo can render, but memory-agent chat requests will fail until it is configured.\",\n} as const;\n\nexport function getCustomerMemoryAgentEnv(): CustomerMemoryAgentEnv {\n  return getCustomerMemoryAgentAppEnv();\n}\n\nexport function getCustomerMemoryAgentConfig(\n  env: CustomerMemoryAgentEnv = getCustomerMemoryAgentEnv()\n): CustomerMemoryAgentConfig {\n  return {\n    ...readAiGatewayContractConfig(env, customerMemoryAgentContract),\n    databaseUrl: env.DATABASE_URL,\n  };\n}\n\nexport function getCustomerMemoryAgentDatabaseConfig(\n  env: CustomerMemoryAgentEnv = getCustomerMemoryAgentEnv()\n) {\n  const databaseUrl = env.DATABASE_URL;\n\n  if (!databaseUrl) {\n    throw new Error(missingDatabaseUrlIssue);\n  }\n\n  return { databaseUrl };\n}\n\nexport function getCustomerMemoryAgentSetupState(\n  env: CustomerMemoryAgentEnv = getCustomerMemoryAgentEnv()\n): CustomerMemoryAgentSetupState {\n  return buildAiGatewayContractSetupState(env, customerMemoryAgentContract);\n}\n\nexport function createCustomerMemoryAgentGateway(\n  env: CustomerMemoryAgentEnv = getCustomerMemoryAgentEnv()\n): CustomerMemoryAgentGateway {\n  return createAiGatewayFromContract(env, customerMemoryAgentContract);\n}\n",
      "type": "registry:lib",
      "target": "@lib/customer-memory-agent/env.ts"
    },
    {
      "path": "registry/customer-memory-agent/lib/customer-memory-agent/memory-lifecycle.ts",
      "content": "import {\n  createCustomerMemoryEmbeddingPersistence,\n  indexCustomerMemories,\n} from \"./memory-recall\";\nimport {\n  type CustomerMemoryRecord,\n  createCustomerMemoryStore,\n} from \"./memory-store\";\n\ntype CustomerMemoryCategory = CustomerMemoryRecord[\"category\"];\n\nexport interface CustomerMemoryLifecycleOperation {\n  category: CustomerMemoryCategory;\n  content: string;\n  memoryId: string | null;\n  operation: \"add\" | \"update\" | \"delete\" | \"noop\";\n  reason: string;\n  sourceMessageId: string | null;\n  title: string;\n}\n\ninterface CustomerMemoryLifecycleContext {\n  customerId: string;\n  threadId: string | null;\n  visitorId: string;\n}\n\ninterface CustomerMemorySeedEntry {\n  category: CustomerMemoryCategory;\n  content: string;\n  sourceMessageId: string | null;\n  title: string;\n}\n\ninterface CustomerMemoryLifecycleDependencies {\n  createEmbeddingPersistence?: typeof createCustomerMemoryEmbeddingPersistence;\n  indexMemories?: typeof indexCustomerMemories;\n  store?: Pick<\n    ReturnType<typeof createCustomerMemoryStore>,\n    \"addMemory\" | \"deleteMemory\" | \"recordEvent\" | \"updateMemory\"\n  >;\n}\n\nfunction dedupeMemoryOperations(\n  operations: CustomerMemoryLifecycleOperation[]\n) {\n  const uniqueOperations = new Map<string, CustomerMemoryLifecycleOperation>();\n\n  for (const operation of operations) {\n    const key = `${operation.operation}::${operation.memoryId ?? \"\"}::${operation.category}::${operation.title}::${operation.content}`;\n\n    if (!uniqueOperations.has(key)) {\n      uniqueOperations.set(key, operation);\n    }\n  }\n\n  return [...uniqueOperations.values()];\n}\n\nasync function indexSavedMemories(\n  memories: CustomerMemoryRecord[],\n  dependencies: CustomerMemoryLifecycleDependencies\n) {\n  if (memories.length === 0) {\n    return;\n  }\n\n  const persistence = (\n    dependencies.createEmbeddingPersistence ??\n    createCustomerMemoryEmbeddingPersistence\n  )();\n\n  await (dependencies.indexMemories ?? indexCustomerMemories)(\n    memories,\n    persistence\n  );\n}\n\nexport function createCustomerMemoryLifecycle(\n  dependencies: CustomerMemoryLifecycleDependencies = {}\n) {\n  const store = dependencies.store ?? createCustomerMemoryStore();\n\n  return {\n    async applyOperations(\n      operations: CustomerMemoryLifecycleOperation[],\n      context: CustomerMemoryLifecycleContext\n    ) {\n      const uniqueOperations = dedupeMemoryOperations(operations);\n      const memoriesToIndex: CustomerMemoryRecord[] = [];\n\n      for (const operation of uniqueOperations) {\n        if (operation.operation === \"noop\") {\n          await store.recordEvent({\n            afterContent: null,\n            beforeContent: null,\n            customerId: context.customerId,\n            memoryId: null,\n            operation: \"noop\",\n            reason: operation.reason,\n            sourceMessageId: operation.sourceMessageId,\n            threadId: context.threadId,\n            visitorId: context.visitorId,\n          });\n          continue;\n        }\n\n        if (operation.operation === \"delete\") {\n          if (!operation.memoryId) {\n            throw new Error(\"Customer-memory delete requires a memoryId.\");\n          }\n\n          await store.deleteMemory({\n            memoryId: operation.memoryId,\n            reason: operation.reason,\n            sourceMessageId: operation.sourceMessageId,\n          });\n          continue;\n        }\n\n        if (operation.operation === \"update\") {\n          if (!operation.memoryId) {\n            throw new Error(\"Customer-memory update requires a memoryId.\");\n          }\n\n          memoriesToIndex.push(\n            await store.updateMemory({\n              category: operation.category,\n              content: operation.content,\n              memoryId: operation.memoryId,\n              reason: operation.reason,\n              sourceMessageId: operation.sourceMessageId,\n              title: operation.title,\n            })\n          );\n          continue;\n        }\n\n        memoriesToIndex.push(\n          await store.addMemory({\n            category: operation.category,\n            content: operation.content,\n            customerId: context.customerId,\n            reason: operation.reason,\n            sourceMessageId: operation.sourceMessageId,\n            threadId: context.threadId,\n            title: operation.title,\n            visitorId: context.visitorId,\n          })\n        );\n      }\n\n      await indexSavedMemories(memoriesToIndex, dependencies);\n\n      return memoriesToIndex;\n    },\n\n    async seedMemories(\n      memories: CustomerMemorySeedEntry[],\n      context: CustomerMemoryLifecycleContext,\n      reason = \"Seeded shared customer-memory demo snapshot.\"\n    ) {\n      const savedMemories: CustomerMemoryRecord[] = [];\n\n      for (const memory of memories) {\n        savedMemories.push(\n          await store.addMemory({\n            category: memory.category,\n            content: memory.content,\n            customerId: context.customerId,\n            reason,\n            sourceMessageId: memory.sourceMessageId,\n            threadId: context.threadId,\n            title: memory.title,\n            visitorId: context.visitorId,\n          })\n        );\n      }\n\n      await indexSavedMemories(savedMemories, dependencies);\n\n      return savedMemories;\n    },\n  };\n}\n",
      "type": "registry:lib",
      "target": "@lib/customer-memory-agent/memory-lifecycle.ts"
    },
    {
      "path": "registry/customer-memory-agent/lib/customer-memory-agent/memory-recall.ts",
      "content": "import {\n  and,\n  cosineDistance,\n  desc,\n  eq,\n  gt,\n  inArray,\n  ne,\n  sql,\n} from \"drizzle-orm\";\nimport { embed, embedMany } from \"ai\";\n\nimport { loadCustomerMemoryAgentDatabase } from \"./database\";\nimport {\n  createCustomerMemoryAgentGateway,\n  getCustomerMemoryAgentEnv,\n  type CustomerMemoryAgentEnv,\n} from \"./env\";\nimport {\n  type CustomerMemoryRecord,\n  createCustomerMemoryStore,\n} from \"./memory-store\";\n\nconst embeddingModelId = \"openai/text-embedding-3-small\";\nconst memoryRecallLimit = 4;\nconst minimumSimilarity = 0.6;\n\nexport interface CustomerMemoryEmbeddingRecord {\n  content: string;\n  embedding: number[];\n  memoryId: string;\n}\n\nexport interface RetrievedCustomerMemory extends CustomerMemoryRecord {\n  similarity: number;\n}\n\nexport interface CustomerMemoryEmbeddingPersistence {\n  findMatches(input: {\n    customerId: string;\n    queryEmbedding: number[];\n    visitorId: string;\n  }): Promise<RetrievedCustomerMemory[]>;\n  replaceEmbeddings(records: CustomerMemoryEmbeddingRecord[]): Promise<void>;\n}\n\ninterface FindRelevantCustomerMemoryInput {\n  customerId: string;\n  query: string;\n  visitorId: string;\n}\n\ninterface CustomerMemoryRecallDependencies {\n  findMatches?: CustomerMemoryEmbeddingPersistence[\"findMatches\"];\n  generateEmbedding?: (\n    value: string,\n    env: CustomerMemoryAgentEnv\n  ) => Promise<number[]>;\n  generateEmbeddings?: (\n    values: string[],\n    env: CustomerMemoryAgentEnv\n  ) => Promise<number[][]>;\n  loadDatabase?: () => Promise<CustomerMemoryRecallDatabaseModule>;\n  recordSearchHits?: (memories: RetrievedCustomerMemory[]) => Promise<void>;\n}\n\ninterface CustomerMemoryRecallDatabaseModule {\n  customerMemoryEmbeddings: Awaited<\n    ReturnType<typeof loadCustomerMemoryAgentDatabase>\n  >[\"customerMemoryEmbeddings\"];\n  customerMemoryMemories: Awaited<\n    ReturnType<typeof loadCustomerMemoryAgentDatabase>\n  >[\"customerMemoryMemories\"];\n  database: Awaited<\n    ReturnType<typeof loadCustomerMemoryAgentDatabase>\n  >[\"database\"];\n}\n\nfunction buildEmbeddingContent(memory: CustomerMemoryRecord) {\n  return [memory.category, memory.title, memory.content]\n    .filter(Boolean)\n    .join(\"\\n\");\n}\n\nconst wordPattern = /[a-z0-9]+/g;\nconst stopWords = new Set([\n  \"a\",\n  \"and\",\n  \"are\",\n  \"by\",\n  \"for\",\n  \"how\",\n  \"if\",\n  \"is\",\n  \"it\",\n  \"safe\",\n  \"should\",\n  \"that\",\n  \"the\",\n  \"to\",\n  \"today\",\n  \"what\",\n]);\n\nfunction tokenizeMemoryText(value: string) {\n  return [...value.toLowerCase().matchAll(wordPattern)]\n    .map((match) => match[0])\n    .filter((token) => !stopWords.has(token));\n}\n\nfunction scoreMemoryForQuery(\n  queryTokens: string[],\n  memory: CustomerMemoryRecord\n) {\n  const searchableText = buildEmbeddingContent(memory).toLowerCase();\n  const matchedTokens = queryTokens.filter((token) =>\n    searchableText.includes(token)\n  );\n\n  return matchedTokens.length / Math.max(queryTokens.length, 1);\n}\n\nasync function findPortableCustomerMemoryMatches(input: {\n  customerId: string;\n  query: string;\n  visitorId: string;\n}): Promise<RetrievedCustomerMemory[]> {\n  const queryTokens = tokenizeMemoryText(input.query);\n  const store = createCustomerMemoryStore();\n  const memories = await store.listMemories({\n    customerId: input.customerId,\n    visitorId: input.visitorId,\n  });\n\n  return memories\n    .map((memory) => ({\n      ...memory,\n      similarity: scoreMemoryForQuery(queryTokens, memory),\n    }))\n    .filter((memory) => memory.similarity > 0)\n    .sort((left, right) => right.similarity - left.similarity)\n    .slice(0, memoryRecallLimit);\n}\n\nasync function replaceCustomerMemoryEmbeddings(\n  records: CustomerMemoryEmbeddingRecord[],\n  loadDatabase: () => Promise<CustomerMemoryRecallDatabaseModule>\n) {\n  const { customerMemoryEmbeddings, database } = await loadDatabase();\n\n  if (records.length === 0) {\n    return;\n  }\n\n  await database.transaction(async (transaction) => {\n    await transaction.delete(customerMemoryEmbeddings).where(\n      inArray(\n        customerMemoryEmbeddings.memoryId,\n        records.map((record) => record.memoryId)\n      )\n    );\n\n    await transaction.insert(customerMemoryEmbeddings).values(records);\n  });\n}\n\nasync function findCustomerMemoryMatches(\n  input: {\n    customerId: string;\n    queryEmbedding: number[];\n    visitorId: string;\n  },\n  loadDatabase: () => Promise<CustomerMemoryRecallDatabaseModule>\n): Promise<RetrievedCustomerMemory[]> {\n  const { customerMemoryEmbeddings, customerMemoryMemories, database } =\n    await loadDatabase();\n  const similarity = sql<number>`1 - (${cosineDistance(\n    customerMemoryEmbeddings.embedding,\n    input.queryEmbedding\n  )})`;\n  const boostedSimilarity = sql<number>`${similarity} + least(${customerMemoryMemories.accessCount} * 0.01, 0.05)`;\n\n  return database\n    .select({\n      accessCount: customerMemoryMemories.accessCount,\n      category: customerMemoryMemories.category,\n      content: customerMemoryMemories.content,\n      createdAt: customerMemoryMemories.createdAt,\n      customerId: customerMemoryMemories.customerId,\n      id: customerMemoryMemories.id,\n      lastAccessedAt: customerMemoryMemories.lastAccessedAt,\n      metadata: customerMemoryMemories.metadata,\n      similarity: boostedSimilarity,\n      sourceMessageId: customerMemoryMemories.sourceMessageId,\n      status: customerMemoryMemories.status,\n      threadId: customerMemoryMemories.threadId,\n      title: customerMemoryMemories.title,\n      updatedAt: customerMemoryMemories.updatedAt,\n      visitorId: customerMemoryMemories.visitorId,\n    })\n    .from(customerMemoryEmbeddings)\n    .innerJoin(\n      customerMemoryMemories,\n      eq(customerMemoryEmbeddings.memoryId, customerMemoryMemories.id)\n    )\n    .where(\n      and(\n        eq(customerMemoryMemories.customerId, input.customerId),\n        eq(customerMemoryMemories.visitorId, input.visitorId),\n        ne(customerMemoryMemories.status, \"deleted\"),\n        gt(similarity, minimumSimilarity)\n      )\n    )\n    .orderBy((table) => desc(table.similarity))\n    .limit(memoryRecallLimit)\n    .then((rows) =>\n      rows.map((row) => ({\n        ...row,\n        createdAt:\n          row.createdAt instanceof Date\n            ? row.createdAt.toISOString()\n            : row.createdAt,\n        lastAccessedAt:\n          row.lastAccessedAt instanceof Date\n            ? row.lastAccessedAt.toISOString()\n            : row.lastAccessedAt,\n        metadata: (row.metadata as Record<string, unknown> | null) ?? null,\n        status: row.status as CustomerMemoryRecord[\"status\"],\n        updatedAt:\n          row.updatedAt instanceof Date\n            ? row.updatedAt.toISOString()\n            : row.updatedAt,\n      }))\n    );\n}\n\nexport async function generateCustomerMemoryEmbedding(\n  value: string,\n  env: CustomerMemoryAgentEnv = getCustomerMemoryAgentEnv()\n): Promise<number[]> {\n  const gateway = createCustomerMemoryAgentGateway(env);\n  const normalizedValue = value.replaceAll(\"\\n\", \" \").trim();\n  const { embedding } = await embed({\n    model: gateway.embeddingModel(embeddingModelId),\n    value: normalizedValue,\n  });\n\n  return embedding;\n}\n\nexport async function generateCustomerMemoryEmbeddings(\n  values: string[],\n  env: CustomerMemoryAgentEnv = getCustomerMemoryAgentEnv()\n): Promise<number[][]> {\n  const gateway = createCustomerMemoryAgentGateway(env);\n  const { embeddings } = await embedMany({\n    model: gateway.embeddingModel(embeddingModelId),\n    values: values.map((value) => value.replaceAll(\"\\n\", \" \").trim()),\n  });\n\n  return embeddings;\n}\n\nexport async function indexCustomerMemories(\n  memories: CustomerMemoryRecord[],\n  persistence: Pick<CustomerMemoryEmbeddingPersistence, \"replaceEmbeddings\">,\n  dependencies: Pick<\n    CustomerMemoryRecallDependencies,\n    \"generateEmbeddings\"\n  > = {}\n) {\n  if (memories.length === 0) {\n    return;\n  }\n\n  if (!getCustomerMemoryAgentEnv().DATABASE_URL) {\n    return;\n  }\n\n  const generateEmbeddings =\n    dependencies.generateEmbeddings ?? generateCustomerMemoryEmbeddings;\n  const contents = memories.map(buildEmbeddingContent);\n  const embeddings = await generateEmbeddings(\n    contents,\n    getCustomerMemoryAgentEnv()\n  );\n\n  await persistence.replaceEmbeddings(\n    memories.map((memory, index) => {\n      const embedding = embeddings[index];\n\n      if (!embedding) {\n        throw new Error(\n          `Missing customer-memory embedding ${index} for ${memory.id}.`\n        );\n      }\n\n      return {\n        content: contents[index] ?? memory.content,\n        embedding,\n        memoryId: memory.id,\n      };\n    })\n  );\n}\n\nexport const indexCustomerMemoryEntries = indexCustomerMemories;\n\nexport async function findRelevantCustomerMemory(\n  input: FindRelevantCustomerMemoryInput,\n  env: CustomerMemoryAgentEnv = getCustomerMemoryAgentEnv(),\n  dependencies: CustomerMemoryRecallDependencies = {}\n) {\n  const normalizedQuery = input.query.trim();\n\n  if (normalizedQuery.length === 0) {\n    return [];\n  }\n\n  if (!env.DATABASE_URL && !dependencies.findMatches) {\n    const matches = await findPortableCustomerMemoryMatches({\n      customerId: input.customerId,\n      query: normalizedQuery,\n      visitorId: input.visitorId,\n    });\n\n    await (\n      dependencies.recordSearchHits ??\n      (async (memories: RetrievedCustomerMemory[]) => {\n        const store = createCustomerMemoryStore();\n        await store.markMemoriesAccessed(memories.map((memory) => memory.id));\n        await Promise.all(\n          memories.map((memory) =>\n            store.recordEvent({\n              afterContent: memory.content,\n              beforeContent: null,\n              customerId: memory.customerId,\n              memoryId: memory.id,\n              metadata: memory.metadata,\n              operation: \"search_hit\",\n              reason: `Retrieved for query: ${normalizedQuery}`,\n              sourceMessageId: memory.sourceMessageId,\n              threadId: memory.threadId,\n              visitorId: memory.visitorId,\n            })\n          )\n        );\n      })\n    )(matches);\n\n    return matches;\n  }\n\n  const generateEmbedding =\n    dependencies.generateEmbedding ?? generateCustomerMemoryEmbedding;\n  const findMatches =\n    dependencies.findMatches ??\n    ((nextInput: {\n      customerId: string;\n      queryEmbedding: number[];\n      visitorId: string;\n    }) =>\n      findCustomerMemoryMatches(\n        nextInput,\n        dependencies.loadDatabase ?? loadCustomerMemoryAgentDatabase\n      ));\n  const queryEmbedding = await generateEmbedding(normalizedQuery, env);\n  const matches = await findMatches({\n    customerId: input.customerId,\n    queryEmbedding,\n    visitorId: input.visitorId,\n  });\n\n  await (\n    dependencies.recordSearchHits ??\n    (async (memories: RetrievedCustomerMemory[]) => {\n      const store = createCustomerMemoryStore();\n      await store.markMemoriesAccessed(memories.map((memory) => memory.id));\n      await Promise.all(\n        memories.map((memory) =>\n          store.recordEvent({\n            afterContent: memory.content,\n            beforeContent: null,\n            customerId: memory.customerId,\n            memoryId: memory.id,\n            metadata: memory.metadata,\n            operation: \"search_hit\",\n            reason: `Retrieved for query: ${normalizedQuery}`,\n            sourceMessageId: memory.sourceMessageId,\n            threadId: memory.threadId,\n            visitorId: memory.visitorId,\n          })\n        )\n      );\n    })\n  )(matches);\n\n  return matches;\n}\n\nexport function createCustomerMemoryEmbeddingPersistence(\n  loadDatabase: () => Promise<CustomerMemoryRecallDatabaseModule> = loadCustomerMemoryAgentDatabase\n): CustomerMemoryEmbeddingPersistence {\n  return {\n    findMatches: (input) => findCustomerMemoryMatches(input, loadDatabase),\n    replaceEmbeddings: (records) =>\n      replaceCustomerMemoryEmbeddings(records, loadDatabase),\n  };\n}\n",
      "type": "registry:lib",
      "target": "@lib/customer-memory-agent/memory-recall.ts"
    },
    {
      "path": "registry/customer-memory-agent/lib/customer-memory-agent/memory-store.ts",
      "content": "import { and, desc, eq, inArray, ne, sql } from \"drizzle-orm\";\n\nimport { loadCustomerMemoryAgentDatabase } from \"./database\";\nimport { getCustomerMemoryAgentEnv } from \"./env\";\nimport { createPortableCustomerMemoryPersistence } from \"./portable-store\";\n\nexport const customerMemoryStatuses = [\"active\", \"updated\", \"deleted\"] as const;\nexport const customerMemoryOperations = [\n  \"add\",\n  \"update\",\n  \"delete\",\n  \"search_hit\",\n  \"compact_capture\",\n  \"noop\",\n] as const;\n\nexport type CustomerMemoryStatus = (typeof customerMemoryStatuses)[number];\nexport type CustomerMemoryOperation = (typeof customerMemoryOperations)[number];\nexport type CustomerMemoryMetadata = Record<string, unknown> | null;\n\nexport interface CustomerMemoryRecord {\n  accessCount: number;\n  category: string;\n  content: string;\n  createdAt: string;\n  customerId: string;\n  id: string;\n  lastAccessedAt: string | null;\n  metadata: CustomerMemoryMetadata;\n  sourceMessageId: string | null;\n  status: CustomerMemoryStatus;\n  threadId: string | null;\n  title: string | null;\n  updatedAt: string;\n  visitorId: string;\n}\n\nexport interface CustomerMemoryEventRecord {\n  afterContent: string | null;\n  beforeContent: string | null;\n  createdAt: string;\n  customerId: string;\n  id: string;\n  memoryId: string | null;\n  metadata: CustomerMemoryMetadata;\n  operation: CustomerMemoryOperation;\n  reason: string | null;\n  sourceMessageId: string | null;\n  threadId: string | null;\n  visitorId: string;\n}\n\nexport interface CustomerMemoryMemoryPersistence {\n  createMemory(input: {\n    category: string;\n    content: string;\n    customerId: string;\n    metadata?: CustomerMemoryMetadata;\n    reason?: string | null;\n    sourceMessageId?: string | null;\n    threadId?: string | null;\n    title?: string | null;\n    visitorId: string;\n  }): Promise<CustomerMemoryRecord>;\n  deleteMemory(input: {\n    memoryId: string;\n    reason?: string | null;\n    sourceMessageId?: string | null;\n  }): Promise<void>;\n  listEventsForCustomer(input: {\n    customerId: string;\n    visitorId: string;\n  }): Promise<CustomerMemoryEventRecord[]>;\n  listVisibleMemoriesForCustomer(input: {\n    customerId: string;\n    visitorId: string;\n  }): Promise<CustomerMemoryRecord[]>;\n  markMemoryAccessed(memoryIds: string[]): Promise<void>;\n  recordEvent(input: {\n    afterContent?: string | null;\n    beforeContent?: string | null;\n    customerId: string;\n    memoryId?: string | null;\n    metadata?: CustomerMemoryMetadata;\n    operation: CustomerMemoryOperation;\n    reason?: string | null;\n    sourceMessageId?: string | null;\n    threadId?: string | null;\n    visitorId: string;\n  }): Promise<CustomerMemoryEventRecord>;\n  updateMemory(input: {\n    category?: string;\n    content: string;\n    memoryId: string;\n    metadata?: CustomerMemoryMetadata;\n    reason?: string | null;\n    sourceMessageId?: string | null;\n    title?: string | null;\n  }): Promise<CustomerMemoryRecord>;\n}\n\ninterface CustomerMemoryStoreDependencies {\n  persistence?: CustomerMemoryMemoryPersistence;\n}\n\ninterface CustomerMemoryDatabaseModule {\n  customerMemoryEvents: Awaited<\n    ReturnType<typeof loadCustomerMemoryAgentDatabase>\n  >[\"customerMemoryEvents\"];\n  customerMemoryMemories: Awaited<\n    ReturnType<typeof loadCustomerMemoryAgentDatabase>\n  >[\"customerMemoryMemories\"];\n  database: Awaited<\n    ReturnType<typeof loadCustomerMemoryAgentDatabase>\n  >[\"database\"];\n}\n\nfunction toIsoString(value: Date | string | null) {\n  if (value === null) {\n    return null;\n  }\n\n  return value instanceof Date ? value.toISOString() : value;\n}\n\nfunction normalizeStatus(value: string): CustomerMemoryStatus {\n  if (customerMemoryStatuses.includes(value as CustomerMemoryStatus)) {\n    return value as CustomerMemoryStatus;\n  }\n\n  throw new Error(`Unknown customer-memory status: ${value}`);\n}\n\nfunction normalizeOperation(value: string): CustomerMemoryOperation {\n  if (customerMemoryOperations.includes(value as CustomerMemoryOperation)) {\n    return value as CustomerMemoryOperation;\n  }\n\n  throw new Error(`Unknown customer-memory operation: ${value}`);\n}\n\nfunction normalizeMetadata(value: unknown): CustomerMemoryMetadata {\n  if (value === null || value === undefined) {\n    return null;\n  }\n\n  if (typeof value !== \"object\" || Array.isArray(value)) {\n    throw new Error(\"Customer-memory metadata must be a JSON object.\");\n  }\n\n  return value as Record<string, unknown>;\n}\n\nfunction normalizeMemoryRecord(record: {\n  accessCount: number;\n  category: string;\n  content: string;\n  createdAt: Date | string;\n  customerId: string;\n  id: string;\n  lastAccessedAt: Date | string | null;\n  metadata: unknown;\n  sourceMessageId: string | null;\n  status: string;\n  threadId: string | null;\n  title: string | null;\n  updatedAt: Date | string;\n  visitorId: string;\n}): CustomerMemoryRecord {\n  return {\n    accessCount: record.accessCount,\n    category: record.category,\n    content: record.content,\n    createdAt: toIsoString(record.createdAt) ?? \"\",\n    customerId: record.customerId,\n    id: record.id,\n    lastAccessedAt: toIsoString(record.lastAccessedAt),\n    metadata: normalizeMetadata(record.metadata),\n    sourceMessageId: record.sourceMessageId,\n    status: normalizeStatus(record.status),\n    threadId: record.threadId,\n    title: record.title,\n    updatedAt: toIsoString(record.updatedAt) ?? \"\",\n    visitorId: record.visitorId,\n  };\n}\n\nfunction normalizeEventRecord(record: {\n  afterContent: string | null;\n  beforeContent: string | null;\n  createdAt: Date | string;\n  customerId: string;\n  id: string;\n  memoryId: string | null;\n  metadata: unknown;\n  operation: string;\n  reason: string | null;\n  sourceMessageId: string | null;\n  threadId: string | null;\n  visitorId: string;\n}): CustomerMemoryEventRecord {\n  return {\n    afterContent: record.afterContent,\n    beforeContent: record.beforeContent,\n    createdAt: toIsoString(record.createdAt) ?? \"\",\n    customerId: record.customerId,\n    id: record.id,\n    memoryId: record.memoryId,\n    metadata: normalizeMetadata(record.metadata),\n    operation: normalizeOperation(record.operation),\n    reason: record.reason,\n    sourceMessageId: record.sourceMessageId,\n    threadId: record.threadId,\n    visitorId: record.visitorId,\n  };\n}\n\nasync function insertMemoryEvent(\n  input: Parameters<CustomerMemoryMemoryPersistence[\"recordEvent\"]>[0],\n  databaseModule: CustomerMemoryDatabaseModule\n) {\n  const [row] = await databaseModule.database\n    .insert(databaseModule.customerMemoryEvents)\n    .values({\n      afterContent: input.afterContent ?? null,\n      beforeContent: input.beforeContent ?? null,\n      customerId: input.customerId,\n      memoryId: input.memoryId ?? null,\n      metadata: input.metadata ?? null,\n      operation: input.operation,\n      reason: input.reason ?? null,\n      sourceMessageId: input.sourceMessageId ?? null,\n      threadId: input.threadId ?? null,\n      visitorId: input.visitorId,\n    })\n    .returning();\n\n  if (!row) {\n    throw new Error(\"Failed to save a customer-memory event.\");\n  }\n\n  return normalizeEventRecord(row);\n}\n\nasync function getMemoryForMutation(\n  memoryId: string,\n  databaseModule: CustomerMemoryDatabaseModule\n) {\n  const [row] = await databaseModule.database\n    .select()\n    .from(databaseModule.customerMemoryMemories)\n    .where(\n      and(\n        eq(databaseModule.customerMemoryMemories.id, memoryId),\n        ne(databaseModule.customerMemoryMemories.status, \"deleted\")\n      )\n    )\n    .limit(1);\n\n  if (!row) {\n    throw new Error(`No active customer memory found for ${memoryId}.`);\n  }\n\n  return row;\n}\n\nfunction createDatabaseBackedPersistence(): CustomerMemoryMemoryPersistence {\n  return {\n    async createMemory(input) {\n      const databaseModule = await loadCustomerMemoryAgentDatabase();\n      let savedMemory: CustomerMemoryRecord | null = null;\n\n      await databaseModule.database.transaction(async (transaction) => {\n        const [row] = await transaction\n          .insert(databaseModule.customerMemoryMemories)\n          .values({\n            category: input.category,\n            content: input.content,\n            customerId: input.customerId,\n            metadata: input.metadata ?? null,\n            sourceMessageId: input.sourceMessageId ?? null,\n            threadId: input.threadId ?? null,\n            title: input.title ?? null,\n            visitorId: input.visitorId,\n          })\n          .returning();\n\n        if (!row) {\n          throw new Error(\"Failed to save a customer memory.\");\n        }\n\n        await insertMemoryEvent(\n          {\n            afterContent: row.content,\n            beforeContent: null,\n            customerId: row.customerId,\n            memoryId: row.id,\n            metadata: normalizeMetadata(row.metadata),\n            operation: \"add\",\n            reason: input.reason ?? null,\n            sourceMessageId: row.sourceMessageId,\n            threadId: row.threadId,\n            visitorId: row.visitorId,\n          },\n          {\n            ...databaseModule,\n            database: transaction as unknown as typeof databaseModule.database,\n          }\n        );\n\n        savedMemory = normalizeMemoryRecord(row);\n      });\n\n      if (!savedMemory) {\n        throw new Error(\"Failed to save a customer memory.\");\n      }\n\n      return savedMemory;\n    },\n    async deleteMemory(input) {\n      const databaseModule = await loadCustomerMemoryAgentDatabase();\n\n      await databaseModule.database.transaction(async (transaction) => {\n        const transactionModule = {\n          ...databaseModule,\n          database: transaction as unknown as typeof databaseModule.database,\n        };\n        const current = await getMemoryForMutation(\n          input.memoryId,\n          transactionModule\n        );\n        await transaction\n          .update(databaseModule.customerMemoryMemories)\n          .set({ status: \"deleted\", updatedAt: new Date() })\n          .where(eq(databaseModule.customerMemoryMemories.id, input.memoryId));\n\n        await insertMemoryEvent(\n          {\n            afterContent: null,\n            beforeContent: current.content,\n            customerId: current.customerId,\n            memoryId: current.id,\n            metadata: normalizeMetadata(current.metadata),\n            operation: \"delete\",\n            reason: input.reason ?? null,\n            sourceMessageId: input.sourceMessageId ?? current.sourceMessageId,\n            threadId: current.threadId,\n            visitorId: current.visitorId,\n          },\n          transactionModule\n        );\n      });\n    },\n    async listEventsForCustomer(input) {\n      const { customerMemoryEvents, database } =\n        await loadCustomerMemoryAgentDatabase();\n      const rows = await database\n        .select()\n        .from(customerMemoryEvents)\n        .where(\n          and(\n            eq(customerMemoryEvents.customerId, input.customerId),\n            eq(customerMemoryEvents.visitorId, input.visitorId)\n          )\n        )\n        .orderBy(desc(customerMemoryEvents.createdAt));\n\n      return rows.map(normalizeEventRecord);\n    },\n    async listVisibleMemoriesForCustomer(input) {\n      const { customerMemoryMemories, database } =\n        await loadCustomerMemoryAgentDatabase();\n      const rows = await database\n        .select()\n        .from(customerMemoryMemories)\n        .where(\n          and(\n            eq(customerMemoryMemories.customerId, input.customerId),\n            eq(customerMemoryMemories.visitorId, input.visitorId),\n            ne(customerMemoryMemories.status, \"deleted\")\n          )\n        )\n        .orderBy(desc(customerMemoryMemories.updatedAt));\n\n      return rows.map(normalizeMemoryRecord);\n    },\n    async markMemoryAccessed(memoryIds) {\n      if (memoryIds.length === 0) {\n        return;\n      }\n\n      const { customerMemoryMemories, database } =\n        await loadCustomerMemoryAgentDatabase();\n      await database\n        .update(customerMemoryMemories)\n        .set({\n          accessCount: sql`${customerMemoryMemories.accessCount} + 1`,\n          lastAccessedAt: new Date(),\n        })\n        .where(\n          and(\n            inArray(customerMemoryMemories.id, memoryIds),\n            ne(customerMemoryMemories.status, \"deleted\")\n          )\n        );\n    },\n    async recordEvent(input) {\n      return insertMemoryEvent(input, await loadCustomerMemoryAgentDatabase());\n    },\n    async updateMemory(input) {\n      const databaseModule = await loadCustomerMemoryAgentDatabase();\n      let savedMemory: CustomerMemoryRecord | null = null;\n\n      await databaseModule.database.transaction(async (transaction) => {\n        const transactionModule = {\n          ...databaseModule,\n          database: transaction as unknown as typeof databaseModule.database,\n        };\n        const current = await getMemoryForMutation(\n          input.memoryId,\n          transactionModule\n        );\n        const [row] = await transaction\n          .update(databaseModule.customerMemoryMemories)\n          .set({\n            category: input.category ?? current.category,\n            content: input.content,\n            metadata: input.metadata ?? current.metadata,\n            sourceMessageId: input.sourceMessageId ?? current.sourceMessageId,\n            status: \"updated\",\n            title: input.title ?? current.title,\n            updatedAt: new Date(),\n          })\n          .where(eq(databaseModule.customerMemoryMemories.id, input.memoryId))\n          .returning();\n\n        if (!row) {\n          throw new Error(\n            `Failed to update customer memory ${input.memoryId}.`\n          );\n        }\n\n        await insertMemoryEvent(\n          {\n            afterContent: row.content,\n            beforeContent: current.content,\n            customerId: row.customerId,\n            memoryId: row.id,\n            metadata: normalizeMetadata(row.metadata),\n            operation: \"update\",\n            reason: input.reason ?? null,\n            sourceMessageId: row.sourceMessageId,\n            threadId: row.threadId,\n            visitorId: row.visitorId,\n          },\n          transactionModule\n        );\n\n        savedMemory = normalizeMemoryRecord(row);\n      });\n\n      if (!savedMemory) {\n        throw new Error(`Failed to update customer memory ${input.memoryId}.`);\n      }\n\n      return savedMemory;\n    },\n  };\n}\n\nexport function createCustomerMemoryStore(\n  dependencies: CustomerMemoryStoreDependencies = {}\n) {\n  const persistence =\n    dependencies.persistence ??\n    (getCustomerMemoryAgentEnv().DATABASE_URL\n      ? createDatabaseBackedPersistence()\n      : createPortableCustomerMemoryPersistence());\n\n  return {\n    addMemory: persistence.createMemory,\n    deleteMemory: persistence.deleteMemory,\n    listEvents: persistence.listEventsForCustomer,\n    listMemories: persistence.listVisibleMemoriesForCustomer,\n    markMemoriesAccessed: persistence.markMemoryAccessed,\n    recordEvent: persistence.recordEvent,\n    updateMemory: persistence.updateMemory,\n  };\n}\n",
      "type": "registry:lib",
      "target": "@lib/customer-memory-agent/memory-store.ts"
    },
    {
      "path": "registry/customer-memory-agent/lib/customer-memory-agent/portable-store.ts",
      "content": "import type { UIMessage } from \"ai\";\n\nimport type {\n  CustomerMemoryCompactionPersistence,\n  CustomerMemoryCompactionRecord,\n} from \"./compaction-store\";\nimport type {\n  CustomerMemoryEventRecord,\n  CustomerMemoryMemoryPersistence,\n  CustomerMemoryMetadata,\n  CustomerMemoryRecord,\n} from \"./memory-store\";\nimport type {\n  CustomerMemoryThreadPersistence,\n  CustomerMemoryThreadRecord,\n  CustomerMemoryThreadSummary,\n  PersistedCustomerMemoryMessage,\n} from \"./thread-store\";\n\ninterface PortableCustomerMemoryState {\n  compactions: CustomerMemoryCompactionRecord[];\n  events: CustomerMemoryEventRecord[];\n  memories: Map<string, CustomerMemoryRecord>;\n  messagesByThreadId: Map<string, PersistedCustomerMemoryMessage[]>;\n  threads: Map<string, CustomerMemoryThreadRecord>;\n}\n\nconst globalStore = globalThis as typeof globalThis & {\n  __customerMemoryPortableStore?: PortableCustomerMemoryState;\n};\n\nfunction getPortableState(): PortableCustomerMemoryState {\n  globalStore.__customerMemoryPortableStore ??= {\n    compactions: [],\n    events: [],\n    memories: new Map(),\n    messagesByThreadId: new Map(),\n    threads: new Map(),\n  };\n\n  return globalStore.__customerMemoryPortableStore;\n}\n\nfunction createPortableId(prefix: string) {\n  return `${prefix}-${crypto.randomUUID()}`;\n}\n\nfunction nowIsoString() {\n  return new Date().toISOString();\n}\n\nfunction cloneRecord<T>(value: T): T {\n  return structuredClone(value);\n}\n\nfunction normalizeMetadata(\n  metadata: CustomerMemoryMetadata | undefined\n): CustomerMemoryMetadata {\n  return metadata ?? null;\n}\n\nfunction createMemoryEventRecord(input: {\n  afterContent?: string | null;\n  beforeContent?: string | null;\n  customerId: string;\n  memoryId?: string | null;\n  metadata?: CustomerMemoryMetadata;\n  operation: CustomerMemoryEventRecord[\"operation\"];\n  reason?: string | null;\n  sourceMessageId?: string | null;\n  threadId?: string | null;\n  visitorId: string;\n}): CustomerMemoryEventRecord {\n  return {\n    afterContent: input.afterContent ?? null,\n    beforeContent: input.beforeContent ?? null,\n    createdAt: nowIsoString(),\n    customerId: input.customerId,\n    id: createPortableId(\"cm-event\"),\n    memoryId: input.memoryId ?? null,\n    metadata: normalizeMetadata(input.metadata),\n    operation: input.operation,\n    reason: input.reason ?? null,\n    sourceMessageId: input.sourceMessageId ?? null,\n    threadId: input.threadId ?? null,\n    visitorId: input.visitorId,\n  };\n}\n\nfunction pushMemoryEvent(\n  state: PortableCustomerMemoryState,\n  input: Parameters<CustomerMemoryMemoryPersistence[\"recordEvent\"]>[0]\n) {\n  const event = createMemoryEventRecord(input);\n  state.events.push(event);\n  return cloneRecord(event);\n}\n\nexport function createPortableCustomerMemoryThreadPersistence(): CustomerMemoryThreadPersistence {\n  const state = getPortableState();\n\n  return {\n    async createThread(input) {\n      const timestamp = nowIsoString();\n      const thread: CustomerMemoryThreadRecord = {\n        createdAt: timestamp,\n        customerId: input.customerId,\n        id: createPortableId(\"cm-thread\"),\n        title: input.title ?? null,\n        updatedAt: timestamp,\n        visitorId: input.visitorId,\n      };\n\n      state.threads.set(thread.id, thread);\n      state.messagesByThreadId.set(thread.id, []);\n\n      return cloneRecord(thread);\n    },\n    async findThreadById(threadId) {\n      const thread = state.threads.get(threadId);\n      return thread ? cloneRecord(thread) : null;\n    },\n    async loadThreadMessages(threadId) {\n      return cloneRecord(state.messagesByThreadId.get(threadId) ?? []);\n    },\n    async listThreadsForCustomer(input) {\n      return [...state.threads.values()]\n        .filter(\n          (thread) =>\n            thread.customerId === input.customerId &&\n            thread.visitorId === input.visitorId\n        )\n        .map(\n          (thread): CustomerMemoryThreadSummary => ({\n            ...thread,\n            messageCount: state.messagesByThreadId.get(thread.id)?.length ?? 0,\n          })\n        )\n        .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))\n        .map(cloneRecord);\n    },\n    async replaceThreadMessages(input) {\n      const thread = state.threads.get(input.threadId);\n\n      if (!thread) {\n        throw new Error(`No customer-memory thread found for ${input.threadId}.`);\n      }\n\n      const timestamp = nowIsoString();\n      state.messagesByThreadId.set(\n        input.threadId,\n        input.messages.map((message, messageIndex) => ({\n          createdAt: timestamp,\n          id: createPortableId(\"cm-message\"),\n          message: cloneRecord(message),\n          messageId: message.id,\n          messageIndex,\n          role: message.role,\n          threadId: input.threadId,\n        }))\n      );\n      state.threads.set(input.threadId, { ...thread, updatedAt: timestamp });\n    },\n  };\n}\n\nexport function createPortableCustomerMemoryPersistence(): CustomerMemoryMemoryPersistence {\n  const state = getPortableState();\n\n  return {\n    async createMemory(input) {\n      const timestamp = nowIsoString();\n      const memory: CustomerMemoryRecord = {\n        accessCount: 0,\n        category: input.category,\n        content: input.content,\n        createdAt: timestamp,\n        customerId: input.customerId,\n        id: createPortableId(\"cm-memory\"),\n        lastAccessedAt: null,\n        metadata: normalizeMetadata(input.metadata),\n        sourceMessageId: input.sourceMessageId ?? null,\n        status: \"active\",\n        threadId: input.threadId ?? null,\n        title: input.title ?? null,\n        updatedAt: timestamp,\n        visitorId: input.visitorId,\n      };\n\n      state.memories.set(memory.id, memory);\n      pushMemoryEvent(state, {\n        afterContent: memory.content,\n        beforeContent: null,\n        customerId: memory.customerId,\n        memoryId: memory.id,\n        metadata: memory.metadata,\n        operation: \"add\",\n        reason: input.reason ?? null,\n        sourceMessageId: memory.sourceMessageId,\n        threadId: memory.threadId,\n        visitorId: memory.visitorId,\n      });\n\n      return cloneRecord(memory);\n    },\n    async deleteMemory(input) {\n      const current = state.memories.get(input.memoryId);\n\n      if (!current || current.status === \"deleted\") {\n        throw new Error(`No active customer memory found for ${input.memoryId}.`);\n      }\n\n      const next = {\n        ...current,\n        status: \"deleted\" as const,\n        updatedAt: nowIsoString(),\n      };\n      state.memories.set(input.memoryId, next);\n      pushMemoryEvent(state, {\n        afterContent: null,\n        beforeContent: current.content,\n        customerId: current.customerId,\n        memoryId: current.id,\n        metadata: current.metadata,\n        operation: \"delete\",\n        reason: input.reason ?? null,\n        sourceMessageId: input.sourceMessageId ?? current.sourceMessageId,\n        threadId: current.threadId,\n        visitorId: current.visitorId,\n      });\n    },\n    async listEventsForCustomer(input) {\n      return state.events\n        .filter(\n          (event) =>\n            event.customerId === input.customerId &&\n            event.visitorId === input.visitorId\n        )\n        .sort((left, right) => right.createdAt.localeCompare(left.createdAt))\n        .map(cloneRecord);\n    },\n    async listVisibleMemoriesForCustomer(input) {\n      return [...state.memories.values()]\n        .filter(\n          (memory) =>\n            memory.customerId === input.customerId &&\n            memory.visitorId === input.visitorId &&\n            memory.status !== \"deleted\"\n        )\n        .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))\n        .map(cloneRecord);\n    },\n    async markMemoryAccessed(memoryIds) {\n      const timestamp = nowIsoString();\n\n      for (const memoryId of memoryIds) {\n        const memory = state.memories.get(memoryId);\n\n        if (!memory || memory.status === \"deleted\") {\n          continue;\n        }\n\n        state.memories.set(memoryId, {\n          ...memory,\n          accessCount: memory.accessCount + 1,\n          lastAccessedAt: timestamp,\n        });\n      }\n    },\n    async recordEvent(input) {\n      return pushMemoryEvent(state, input);\n    },\n    async updateMemory(input) {\n      const current = state.memories.get(input.memoryId);\n\n      if (!current || current.status === \"deleted\") {\n        throw new Error(`No active customer memory found for ${input.memoryId}.`);\n      }\n\n      const next: CustomerMemoryRecord = {\n        ...current,\n        category: input.category ?? current.category,\n        content: input.content,\n        metadata: normalizeMetadata(input.metadata ?? current.metadata),\n        sourceMessageId: input.sourceMessageId ?? current.sourceMessageId,\n        status: \"updated\",\n        title: input.title ?? current.title,\n        updatedAt: nowIsoString(),\n      };\n\n      state.memories.set(input.memoryId, next);\n      pushMemoryEvent(state, {\n        afterContent: next.content,\n        beforeContent: current.content,\n        customerId: next.customerId,\n        memoryId: next.id,\n        metadata: next.metadata,\n        operation: \"update\",\n        reason: input.reason ?? null,\n        sourceMessageId: next.sourceMessageId,\n        threadId: next.threadId,\n        visitorId: next.visitorId,\n      });\n\n      return cloneRecord(next);\n    },\n  };\n}\n\nexport function createPortableCustomerMemoryCompactionPersistence(): CustomerMemoryCompactionPersistence {\n  const state = getPortableState();\n\n  return {\n    async createCompaction(input) {\n      const compaction: CustomerMemoryCompactionRecord = {\n        createdAt: nowIsoString(),\n        id: createPortableId(\"cm-compaction\"),\n        messageCount: input.messageCount,\n        summary: input.summary,\n        threadId: input.threadId,\n      };\n\n      state.compactions.push(compaction);\n\n      return cloneRecord(compaction);\n    },\n    async getLatestCompaction(threadId) {\n      const compaction = state.compactions\n        .filter((candidate) => candidate.threadId === threadId)\n        .sort((left, right) => right.createdAt.localeCompare(left.createdAt))[0];\n\n      return compaction ? cloneRecord(compaction) : null;\n    },\n  };\n}\n",
      "type": "registry:lib",
      "target": "@lib/customer-memory-agent/portable-store.ts"
    },
    {
      "path": "registry/customer-memory-agent/lib/customer-memory-agent/runtime.ts",
      "content": "import type { UIMessage } from \"ai\";\n\nimport { getCustomerMemoryProfile } from \"./customer-profiles\";\nimport { customerMemoryCompactionThreshold } from \"./compaction\";\nimport {\n  invalidCustomerIdError,\n  invalidMessagesError,\n  invalidThreadIdError,\n  invalidUiMessagesError,\n  malformedJsonError,\n  readCustomerMemoryChatRequest,\n} from \"./contract\";\nimport { streamCustomerMemoryConversation } from \"./conversation\";\nimport {\n  getCustomerMemoryAgentEnv,\n  getCustomerMemoryAgentSetupState,\n  type CustomerMemoryAgentEnv,\n} from \"./env\";\nimport { createCustomerMemoryThreadStore } from \"./thread-store\";\nimport {\n  type CustomerMemoryViewerContext,\n  getReadonlyCustomerMemoryError,\n  resolveCustomerMemoryViewerContext,\n} from \"./viewer-context\";\n\ninterface CustomerMemoryChatRequestDependencies {\n  ensureThreadOwnership?: (input: {\n    customerId: string;\n    threadId: string;\n    visitorId: string;\n  }) => Promise<void>;\n  streamCustomerMemoryConversation: (\n    input: {\n      customer: NonNullable<ReturnType<typeof getCustomerMemoryProfile>>;\n      messages: UIMessage[];\n      threadId: string;\n      visitorId: string;\n    },\n    env: CustomerMemoryAgentEnv\n  ) => Promise<Response>;\n}\n\nexport interface CustomerMemoryRuntimeState {\n  chatModel: string;\n  compactionThreshold: number;\n  isChatAvailable: boolean;\n  nodeVersion: string;\n  persistenceLabel: string;\n  setupMessage: string | null;\n  statusLabel: \"Ready\" | \"Setup required\";\n}\n\nexport function getCustomerMemoryRuntimeState(\n  env: CustomerMemoryAgentEnv = getCustomerMemoryAgentEnv()\n): CustomerMemoryRuntimeState {\n  const setup = getCustomerMemoryAgentSetupState(env);\n\n  return {\n    chatModel: setup.config.chatModel,\n    compactionThreshold: customerMemoryCompactionThreshold,\n    isChatAvailable: setup.isReady,\n    nodeVersion: setup.nodeVersion,\n    persistenceLabel: env.DATABASE_URL ? \"Postgres\" : \"In-memory\",\n    setupMessage: setup.issues.length > 0 ? setup.issues.join(\" \") : null,\n    statusLabel: setup.isReady ? \"Ready\" : \"Setup required\",\n  };\n}\n\nasync function ensureCustomerMemoryThreadOwnership(input: {\n  customerId: string;\n  threadId: string;\n  visitorId: string;\n}) {\n  const snapshot =\n    await createCustomerMemoryThreadStore().loadThreadForViewer(input);\n\n  if (!snapshot) {\n    throw new Error(\n      `No customer-memory thread found for ${input.threadId} under ${input.customerId}.`\n    );\n  }\n}\n\nexport async function handleCustomerMemoryChatRequest(\n  request: Request,\n  viewer: CustomerMemoryViewerContext,\n  env: CustomerMemoryAgentEnv = getCustomerMemoryAgentEnv(),\n  dependencies: CustomerMemoryChatRequestDependencies = {\n    streamCustomerMemoryConversation,\n  }\n) {\n  const runtimeState = getCustomerMemoryRuntimeState(env);\n\n  if (!runtimeState.isChatAvailable) {\n    return Response.json(\n      {\n        error: runtimeState.setupMessage,\n      },\n      { status: 500 }\n    );\n  }\n\n  let body: unknown;\n\n  try {\n    body = await request.json();\n  } catch {\n    return Response.json(\n      {\n        error: malformedJsonError,\n      },\n      { status: 400 }\n    );\n  }\n\n  try {\n    const { customerId, messages, threadId } =\n      await readCustomerMemoryChatRequest(body);\n    const customer = getCustomerMemoryProfile(customerId);\n\n    if (!customer) {\n      throw new Error(invalidCustomerIdError);\n    }\n\n    const viewerContext = resolveCustomerMemoryViewerContext({\n      customer,\n      visitorId: viewer.visitorId,\n    });\n\n    if (viewerContext.isReadonly) {\n      return Response.json(\n        {\n          error: getReadonlyCustomerMemoryError(customer),\n        },\n        { status: 403 }\n      );\n    }\n\n    const ensureThreadOwnership =\n      dependencies.ensureThreadOwnership ?? ensureCustomerMemoryThreadOwnership;\n\n    await ensureThreadOwnership({\n      customerId,\n      threadId,\n      visitorId: viewerContext.visitorId,\n    });\n\n    return dependencies.streamCustomerMemoryConversation(\n      {\n        customer,\n        messages,\n        threadId,\n        visitorId: viewerContext.visitorId,\n      },\n      env\n    );\n  } catch (error) {\n    if (\n      error instanceof Error &&\n      [\n        invalidCustomerIdError,\n        invalidMessagesError,\n        invalidThreadIdError,\n        invalidUiMessagesError,\n      ].includes(error.message)\n    ) {\n      return Response.json(\n        {\n          error: error.message,\n        },\n        { status: 400 }\n      );\n    }\n\n    if (\n      error instanceof Error &&\n      error.message.startsWith(\"No customer-memory thread found\")\n    ) {\n      return Response.json(\n        {\n          error: error.message,\n        },\n        { status: 404 }\n      );\n    }\n\n    throw error;\n  }\n}\n",
      "type": "registry:lib",
      "target": "@lib/customer-memory-agent/runtime.ts"
    },
    {
      "path": "registry/customer-memory-agent/lib/customer-memory-agent/schema.ts",
      "content": "import {\n  foreignKey,\n  index,\n  integer,\n  jsonb,\n  pgTable,\n  text,\n  timestamp,\n  uniqueIndex,\n  uuid,\n  varchar,\n  vector,\n} from \"drizzle-orm/pg-core\";\n\nexport const customerMemoryThreads = pgTable(\n  \"customer_memory_threads\",\n  {\n    id: uuid(\"id\").defaultRandom().primaryKey(),\n    customerId: varchar(\"customer_id\", { length: 191 }).notNull(),\n    visitorId: varchar(\"visitor_id\", { length: 191 }).notNull(),\n    title: text(\"title\"),\n    createdAt: timestamp(\"created_at\", { withTimezone: true })\n      .defaultNow()\n      .notNull(),\n    updatedAt: timestamp(\"updated_at\", { withTimezone: true })\n      .defaultNow()\n      .notNull(),\n  },\n  (table) => ({\n    customerIdIndex: index(\"customer_memory_threads_customer_id_idx\").on(\n      table.customerId\n    ),\n    customerVisitorIndex: index(\n      \"customer_memory_threads_customer_visitor_idx\"\n    ).on(table.customerId, table.visitorId),\n  })\n);\n\nexport const customerMemoryMessages = pgTable(\n  \"customer_memory_messages\",\n  {\n    id: uuid(\"id\").defaultRandom().primaryKey(),\n    threadId: uuid(\"thread_id\").notNull(),\n    messageIndex: integer(\"message_index\").notNull(),\n    messageId: varchar(\"message_id\", { length: 191 }).notNull(),\n    role: varchar(\"role\", { length: 32 }).notNull(),\n    payload: jsonb(\"payload\").notNull(),\n    createdAt: timestamp(\"created_at\", { withTimezone: true })\n      .defaultNow()\n      .notNull(),\n  },\n  (table) => ({\n    threadIndex: index(\"customer_memory_messages_thread_id_idx\").on(\n      table.threadId\n    ),\n    threadMessageIndex: uniqueIndex(\n      \"customer_memory_messages_thread_message_idx\"\n    ).on(table.threadId, table.messageIndex),\n    threadMessageIdIndex: uniqueIndex(\n      \"customer_memory_messages_thread_message_id_idx\"\n    ).on(table.threadId, table.messageId),\n    threadForeignKey: foreignKey({\n      columns: [table.threadId],\n      foreignColumns: [customerMemoryThreads.id],\n      name: \"cm_messages_thread_fk\",\n    }).onDelete(\"cascade\"),\n  })\n);\n\nexport const customerMemoryMemories = pgTable(\n  \"customer_memory_memories\",\n  {\n    id: uuid(\"id\").defaultRandom().primaryKey(),\n    customerId: varchar(\"customer_id\", { length: 191 }).notNull(),\n    visitorId: varchar(\"visitor_id\", { length: 191 }).notNull(),\n    threadId: uuid(\"thread_id\"),\n    sourceMessageId: varchar(\"source_message_id\", { length: 191 }),\n    category: varchar(\"category\", { length: 64 }).notNull(),\n    title: text(\"title\"),\n    content: text(\"content\").notNull(),\n    status: varchar(\"status\", { length: 32 }).default(\"active\").notNull(),\n    metadata: jsonb(\"metadata\"),\n    lastAccessedAt: timestamp(\"last_accessed_at\", { withTimezone: true }),\n    accessCount: integer(\"access_count\").default(0).notNull(),\n    createdAt: timestamp(\"created_at\", { withTimezone: true })\n      .defaultNow()\n      .notNull(),\n    updatedAt: timestamp(\"updated_at\", { withTimezone: true })\n      .defaultNow()\n      .notNull(),\n  },\n  (table) => ({\n    customerIdIndex: index(\"customer_memory_memories_customer_id_idx\").on(\n      table.customerId\n    ),\n    customerVisitorIndex: index(\n      \"customer_memory_memories_customer_visitor_idx\"\n    ).on(table.customerId, table.visitorId, table.status),\n    threadIndex: index(\"customer_memory_memories_thread_id_idx\").on(\n      table.threadId\n    ),\n    threadForeignKey: foreignKey({\n      columns: [table.threadId],\n      foreignColumns: [customerMemoryThreads.id],\n      name: \"cm_memories_thread_fk\",\n    }).onDelete(\"set null\"),\n  })\n);\n\nexport const customerMemoryEvents = pgTable(\n  \"customer_memory_events\",\n  {\n    id: uuid(\"id\").defaultRandom().primaryKey(),\n    memoryId: uuid(\"memory_id\"),\n    customerId: varchar(\"customer_id\", { length: 191 }).notNull(),\n    visitorId: varchar(\"visitor_id\", { length: 191 }).notNull(),\n    threadId: uuid(\"thread_id\"),\n    sourceMessageId: varchar(\"source_message_id\", { length: 191 }),\n    operation: varchar(\"operation\", { length: 32 }).notNull(),\n    reason: text(\"reason\"),\n    beforeContent: text(\"before_content\"),\n    afterContent: text(\"after_content\"),\n    metadata: jsonb(\"metadata\"),\n    createdAt: timestamp(\"created_at\", { withTimezone: true })\n      .defaultNow()\n      .notNull(),\n  },\n  (table) => ({\n    customerVisitorIndex: index(\n      \"customer_memory_events_customer_visitor_idx\"\n    ).on(table.customerId, table.visitorId),\n    memoryIndex: index(\"customer_memory_events_memory_id_idx\").on(\n      table.memoryId\n    ),\n    threadIndex: index(\"customer_memory_events_thread_id_idx\").on(\n      table.threadId\n    ),\n    memoryForeignKey: foreignKey({\n      columns: [table.memoryId],\n      foreignColumns: [customerMemoryMemories.id],\n      name: \"cm_events_memory_fk\",\n    }).onDelete(\"set null\"),\n    threadForeignKey: foreignKey({\n      columns: [table.threadId],\n      foreignColumns: [customerMemoryThreads.id],\n      name: \"cm_events_thread_fk\",\n    }).onDelete(\"set null\"),\n  })\n);\n\nexport const customerMemoryCompactions = pgTable(\n  \"customer_memory_compactions\",\n  {\n    id: uuid(\"id\").defaultRandom().primaryKey(),\n    threadId: uuid(\"thread_id\").notNull(),\n    messageCount: integer(\"message_count\").notNull(),\n    summary: text(\"summary\").notNull(),\n    createdAt: timestamp(\"created_at\", { withTimezone: true })\n      .defaultNow()\n      .notNull(),\n  },\n  (table) => ({\n    threadIndex: index(\"customer_memory_compactions_thread_id_idx\").on(\n      table.threadId\n    ),\n    threadCreatedAtIndex: uniqueIndex(\n      \"customer_memory_compactions_thread_created_at_idx\"\n    ).on(table.threadId, table.createdAt),\n    threadForeignKey: foreignKey({\n      columns: [table.threadId],\n      foreignColumns: [customerMemoryThreads.id],\n      name: \"cm_compactions_thread_fk\",\n    }).onDelete(\"cascade\"),\n  })\n);\n\nexport const customerMemoryEmbeddings = pgTable(\n  \"customer_memory_embeddings\",\n  {\n    id: uuid(\"id\").defaultRandom().primaryKey(),\n    memoryId: uuid(\"memory_id\").notNull(),\n    content: text(\"content\").notNull(),\n    embedding: vector(\"embedding\", { dimensions: 1536 }).notNull(),\n    createdAt: timestamp(\"created_at\", { withTimezone: true })\n      .defaultNow()\n      .notNull(),\n  },\n  (table) => ({\n    memoryIndex: uniqueIndex(\"customer_memory_embeddings_memory_idx\").on(\n      table.memoryId\n    ),\n    embeddingIndex: index(\"customer_memory_embeddings_embedding_idx\").using(\n      \"hnsw\",\n      table.embedding.op(\"vector_cosine_ops\")\n    ),\n    memoryForeignKey: foreignKey({\n      columns: [table.memoryId],\n      foreignColumns: [customerMemoryMemories.id],\n      name: \"cm_embeddings_memory_fk\",\n    }).onDelete(\"cascade\"),\n  })\n);\n\nexport const customerMemoryAgentSchema = {\n  customerMemoryCompactions,\n  customerMemoryEmbeddings,\n  customerMemoryEvents,\n  customerMemoryMemories,\n  customerMemoryMessages,\n  customerMemoryThreads,\n} as const;\n",
      "type": "registry:lib",
      "target": "@lib/customer-memory-agent/schema.ts"
    },
    {
      "path": "registry/customer-memory-agent/lib/customer-memory-agent/session-data.ts",
      "content": "import type { UIMessage } from \"ai\";\n\nimport type { CustomerMemoryProfile } from \"./customer-profiles\";\nimport type { CustomerMemoryCompactionRecord } from \"./compaction-store\";\nimport type { RetrievedCustomerMemory } from \"./memory-recall\";\nimport type {\n  CustomerMemoryEventRecord,\n  CustomerMemoryRecord,\n} from \"./memory-store\";\nimport type {\n  CustomerMemoryThreadRecord,\n  CustomerMemoryThreadSummary,\n} from \"./thread-store\";\n\nexport interface CustomerMemorySessionData {\n  customer: CustomerMemoryProfile;\n  memoryEvents: CustomerMemoryEventRecord[];\n  latestCompaction: CustomerMemoryCompactionRecord | null;\n  memories: CustomerMemoryRecord[];\n  messages: UIMessage[];\n  relevantMemories: RetrievedCustomerMemory[];\n  thread: CustomerMemoryThreadRecord;\n  threads: CustomerMemoryThreadSummary[];\n}\n",
      "type": "registry:lib",
      "target": "@lib/customer-memory-agent/session-data.ts"
    },
    {
      "path": "registry/customer-memory-agent/lib/customer-memory-agent/session-runtime.ts",
      "content": "import { getCustomerMemoryProfile } from \"./customer-profiles\";\nimport { getCustomerMemoryAgentEnv, type CustomerMemoryAgentEnv } from \"./env\";\nimport {\n  invalidCustomerIdError,\n  malformedJsonError,\n  readCustomerMemorySessionQuery,\n  readCustomerMemoryThreadCreateRequest,\n} from \"./contract\";\nimport {\n  createCustomerMemoryThread,\n  loadCustomerMemorySession,\n} from \"./session\";\nimport {\n  type CustomerMemoryViewerContext,\n  getReadonlyCustomerMemoryError,\n  resolveCustomerMemoryViewerContext,\n} from \"./viewer-context\";\n\ninterface CustomerMemorySessionRequestDependencies {\n  loadCustomerMemorySession?: typeof loadCustomerMemorySession;\n}\n\ninterface CustomerMemoryThreadCreateRequestDependencies {\n  createCustomerMemoryThread?: typeof createCustomerMemoryThread;\n  loadCustomerMemorySession?: typeof loadCustomerMemorySession;\n}\n\nfunction getDatabaseSetupError(env: CustomerMemoryAgentEnv) {\n  if (env.DATABASE_URL) {\n    return null;\n  }\n\n  return null;\n}\n\nexport async function handleCustomerMemorySessionRequest(\n  request: Request,\n  viewer: CustomerMemoryViewerContext,\n  env: CustomerMemoryAgentEnv = getCustomerMemoryAgentEnv(),\n  dependencies: CustomerMemorySessionRequestDependencies = {}\n) {\n  const setupError = getDatabaseSetupError(env);\n\n  if (setupError) {\n    return Response.json(\n      {\n        error: setupError,\n      },\n      { status: 500 }\n    );\n  }\n\n  try {\n    const query = readCustomerMemorySessionQuery(request.url);\n    const session = await (\n      dependencies.loadCustomerMemorySession ?? loadCustomerMemorySession\n    )({\n      ...query,\n      visitorId: viewer.visitorId,\n    });\n\n    return Response.json(session);\n  } catch (error) {\n    if (\n      error instanceof Error &&\n      [invalidCustomerIdError, malformedJsonError].includes(error.message)\n    ) {\n      return Response.json(\n        {\n          error: error.message,\n        },\n        { status: 400 }\n      );\n    }\n\n    if (\n      error instanceof Error &&\n      (error.message.startsWith(\"Unknown customer-memory profile\") ||\n        error.message.startsWith(\"No customer-memory thread found\") ||\n        error.message.startsWith(\n          \"No shared customer-memory demo thread is available\"\n        ))\n    ) {\n      return Response.json(\n        {\n          error: error.message,\n        },\n        { status: 404 }\n      );\n    }\n\n    throw error;\n  }\n}\n\nexport async function handleCustomerMemoryThreadCreateRequest(\n  request: Request,\n  viewer: CustomerMemoryViewerContext,\n  env: CustomerMemoryAgentEnv = getCustomerMemoryAgentEnv(),\n  dependencies: CustomerMemoryThreadCreateRequestDependencies = {}\n) {\n  const setupError = getDatabaseSetupError(env);\n\n  if (setupError) {\n    return Response.json(\n      {\n        error: setupError,\n      },\n      { status: 500 }\n    );\n  }\n\n  let body: unknown;\n\n  try {\n    body = await request.json();\n  } catch {\n    return Response.json(\n      {\n        error: malformedJsonError,\n      },\n      { status: 400 }\n    );\n  }\n\n  try {\n    const { customerId } = await readCustomerMemoryThreadCreateRequest(body);\n    const customer = getCustomerMemoryProfile(customerId);\n\n    if (!customer) {\n      throw new Error(`Unknown customer-memory profile: ${customerId}`);\n    }\n\n    const viewerContext = resolveCustomerMemoryViewerContext({\n      customer,\n      visitorId: viewer.visitorId,\n    });\n\n    if (viewerContext.isReadonly) {\n      return Response.json(\n        {\n          error: getReadonlyCustomerMemoryError(customer),\n        },\n        { status: 403 }\n      );\n    }\n\n    const created = await (\n      dependencies.createCustomerMemoryThread ?? createCustomerMemoryThread\n    )({\n      customerId,\n      visitorId: viewerContext.visitorId,\n    });\n    const session = await (\n      dependencies.loadCustomerMemorySession ?? loadCustomerMemorySession\n    )({\n      customerId: created.customer.id,\n      threadId: created.thread.id,\n      visitorId: viewerContext.visitorId,\n    });\n\n    return Response.json(session, { status: 201 });\n  } catch (error) {\n    if (error instanceof Error && error.message === invalidCustomerIdError) {\n      return Response.json(\n        {\n          error: error.message,\n        },\n        { status: 400 }\n      );\n    }\n\n    if (\n      error instanceof Error &&\n      error.message.startsWith(\"Unknown customer-memory profile\")\n    ) {\n      return Response.json(\n        {\n          error: error.message,\n        },\n        { status: 404 }\n      );\n    }\n\n    throw error;\n  }\n}\n",
      "type": "registry:lib",
      "target": "@lib/customer-memory-agent/session-runtime.ts"
    },
    {
      "path": "registry/customer-memory-agent/lib/customer-memory-agent/session.ts",
      "content": "import {\n  type CustomerMemoryProfile,\n  getCustomerMemoryProfile,\n} from \"./customer-profiles\";\nimport type { CustomerMemorySessionData } from \"./session-data\";\nimport { createCustomerMemoryCompactionStore } from \"./compaction-store\";\nimport { findRelevantCustomerMemory } from \"./memory-recall\";\nimport { createCustomerMemoryStore } from \"./memory-store\";\nimport { ensureCustomerMemorySharedDemoSeed } from \"./shared-demo-seed\";\nimport {\n  type CustomerMemoryThreadSnapshot,\n  type CustomerMemoryThreadSummary,\n  createCustomerMemoryThreadStore,\n} from \"./thread-store\";\nimport {\n  type CustomerMemoryViewerContext,\n  resolveCustomerMemoryViewerContext,\n} from \"./viewer-context\";\n\ninterface CustomerMemorySessionDependencies {\n  compactionStore?: ReturnType<typeof createCustomerMemoryCompactionStore>;\n  ensureSharedDemoSeed?: typeof ensureCustomerMemorySharedDemoSeed;\n  findRelevantCustomerMemory?: typeof findRelevantCustomerMemory;\n  memoryStore?: Pick<\n    ReturnType<typeof createCustomerMemoryStore>,\n    \"listEvents\" | \"listMemories\"\n  >;\n  threadStore?: ReturnType<typeof createCustomerMemoryThreadStore>;\n}\n\nfunction buildDefaultThreadTitle(customer: CustomerMemoryProfile) {\n  return `${customer.name} memory thread`;\n}\n\ntype CustomerMemoryThreadStore = ReturnType<\n  typeof createCustomerMemoryThreadStore\n>;\n\ninterface ResolvedCustomerMemoryThreadSnapshot {\n  threadSnapshot: CustomerMemoryThreadSnapshot;\n  threads: CustomerMemoryThreadSummary[];\n}\n\nfunction getMissingCustomerMemorySessionThreadError(input: {\n  customerId: string;\n  threadId: string | null;\n}) {\n  return `No customer-memory thread found for ${input.threadId} under ${input.customerId}.`;\n}\n\nasync function createWritableDefaultThreadSnapshot(input: {\n  customer: CustomerMemoryProfile;\n  threadStore: CustomerMemoryThreadStore;\n  viewer: CustomerMemoryViewerContext;\n}): Promise<ResolvedCustomerMemoryThreadSnapshot> {\n  const thread = await input.threadStore.createThread({\n    customerId: input.customer.id,\n    title: buildDefaultThreadTitle(input.customer),\n    visitorId: input.viewer.visitorId,\n  });\n  const threads = await input.threadStore.listThreads({\n    customerId: input.customer.id,\n    visitorId: input.viewer.visitorId,\n  });\n  const threadSnapshot = await input.threadStore.loadThreadForViewer({\n    customerId: input.customer.id,\n    threadId: thread.id,\n    visitorId: input.viewer.visitorId,\n  });\n\n  if (!threadSnapshot) {\n    throw new Error(\n      getMissingCustomerMemorySessionThreadError({\n        customerId: input.customer.id,\n        threadId: thread.id,\n      })\n    );\n  }\n\n  return { threadSnapshot, threads };\n}\n\nasync function resolveMissingThreadSnapshot(input: {\n  activeThreadId: string | null;\n  customer: CustomerMemoryProfile;\n  threadStore: CustomerMemoryThreadStore;\n  threads: CustomerMemoryThreadSummary[];\n  viewer: CustomerMemoryViewerContext;\n}): Promise<ResolvedCustomerMemoryThreadSnapshot> {\n  const fallbackThreadId = input.threads[0]?.id ?? null;\n\n  if (!fallbackThreadId) {\n    if (input.viewer.isReadonly) {\n      throw new Error(\n        getMissingCustomerMemorySessionThreadError({\n          customerId: input.customer.id,\n          threadId: input.activeThreadId,\n        })\n      );\n    }\n\n    return createWritableDefaultThreadSnapshot(input);\n  }\n\n  const threadSnapshot = await input.threadStore.loadThreadForViewer({\n    customerId: input.customer.id,\n    threadId: fallbackThreadId,\n    visitorId: input.viewer.visitorId,\n  });\n\n  if (!threadSnapshot) {\n    throw new Error(\n      getMissingCustomerMemorySessionThreadError({\n        customerId: input.customer.id,\n        threadId: fallbackThreadId,\n      })\n    );\n  }\n\n  return { threadSnapshot, threads: input.threads };\n}\n\nasync function resolveCustomerMemoryThreadSnapshot(input: {\n  customer: CustomerMemoryProfile;\n  requestedThreadId?: string | null;\n  threadStore: CustomerMemoryThreadStore;\n  viewer: CustomerMemoryViewerContext;\n}): Promise<ResolvedCustomerMemoryThreadSnapshot> {\n  const threads = await input.threadStore.listThreads({\n    customerId: input.customer.id,\n    visitorId: input.viewer.visitorId,\n  });\n  const activeThreadId = input.requestedThreadId ?? threads[0]?.id ?? null;\n\n  if (!activeThreadId) {\n    if (input.viewer.isReadonly) {\n      throw new Error(\n        `No shared customer-memory demo thread is available for ${input.customer.id}.`\n      );\n    }\n\n    return createWritableDefaultThreadSnapshot(input);\n  }\n\n  const threadSnapshot = await input.threadStore.loadThreadForViewer({\n    customerId: input.customer.id,\n    threadId: activeThreadId,\n    visitorId: input.viewer.visitorId,\n  });\n\n  if (!threadSnapshot) {\n    return resolveMissingThreadSnapshot({ ...input, activeThreadId, threads });\n  }\n\n  return { threadSnapshot, threads };\n}\n\nexport async function loadCustomerMemorySession(\n  input: {\n    customerId: string;\n    query?: string;\n    threadId?: string | null;\n    visitorId: string;\n  },\n  dependencies: CustomerMemorySessionDependencies = {}\n) {\n  const customer = getCustomerMemoryProfile(input.customerId);\n\n  if (!customer) {\n    throw new Error(`Unknown customer-memory profile: ${input.customerId}`);\n  }\n\n  const viewer = resolveCustomerMemoryViewerContext({\n    customer,\n    visitorId: input.visitorId,\n  });\n  const threadStore =\n    dependencies.threadStore ?? createCustomerMemoryThreadStore();\n  const memoryStore = dependencies.memoryStore ?? createCustomerMemoryStore();\n  const compactionStore =\n    dependencies.compactionStore ?? createCustomerMemoryCompactionStore();\n\n  if (viewer.isReadonly) {\n    await (\n      dependencies.ensureSharedDemoSeed ?? ensureCustomerMemorySharedDemoSeed\n    )(customer);\n  }\n\n  const { threadSnapshot, threads } = await resolveCustomerMemoryThreadSnapshot(\n    {\n      customer,\n      requestedThreadId: input.threadId,\n      threadStore,\n      viewer,\n    }\n  );\n\n  const [latestCompaction, memories, memoryEvents, relevantMemories] =\n    await Promise.all([\n      compactionStore.getLatestCompaction(threadSnapshot.thread.id),\n      memoryStore.listMemories({\n        customerId: customer.id,\n        visitorId: viewer.visitorId,\n      }),\n      memoryStore.listEvents({\n        customerId: customer.id,\n        visitorId: viewer.visitorId,\n      }),\n      (dependencies.findRelevantCustomerMemory ?? findRelevantCustomerMemory)({\n        customerId: customer.id,\n        query: input.query ?? \"\",\n        visitorId: viewer.visitorId,\n      }),\n    ]);\n\n  return {\n    customer,\n    latestCompaction,\n    memoryEvents,\n    memories,\n    messages: threadSnapshot.messages,\n    relevantMemories,\n    thread: threadSnapshot.thread,\n    threads,\n  } satisfies CustomerMemorySessionData;\n}\n\nexport async function createCustomerMemoryThread(\n  input: {\n    customerId: string;\n    visitorId: string;\n  },\n  dependencies: Pick<CustomerMemorySessionDependencies, \"threadStore\"> = {}\n) {\n  const customer = getCustomerMemoryProfile(input.customerId);\n\n  if (!customer) {\n    throw new Error(`Unknown customer-memory profile: ${input.customerId}`);\n  }\n\n  const viewer = resolveCustomerMemoryViewerContext({\n    customer,\n    visitorId: input.visitorId,\n  });\n  const thread = await (\n    dependencies.threadStore ?? createCustomerMemoryThreadStore()\n  ).createThread({\n    customerId: customer.id,\n    title: buildDefaultThreadTitle(customer),\n    visitorId: viewer.visitorId,\n  });\n\n  return {\n    customer,\n    thread,\n  };\n}\n",
      "type": "registry:lib",
      "target": "@lib/customer-memory-agent/session.ts"
    },
    {
      "path": "registry/customer-memory-agent/lib/customer-memory-agent/shared-demo-seed.ts",
      "content": "import type { UIMessage } from \"ai\";\n\nimport type { CustomerMemoryProfile } from \"./customer-profiles\";\n\nimport { createCustomerMemoryCompactionStore } from \"./compaction-store\";\nimport { createCustomerMemoryLifecycle } from \"./memory-lifecycle\";\nimport { createCustomerMemoryThreadStore } from \"./thread-store\";\nimport { customerMemorySharedVisitorId } from \"./viewer-context\";\n\ninterface SharedDemoSeedMemory {\n  category:\n    | \"constraint\"\n    | \"preference\"\n    | \"promise\"\n    | \"risk\"\n    | \"fact\"\n    | \"follow_up\";\n  content: string;\n  sourceMessageId: string | null;\n  title: string;\n}\n\ninterface SharedDemoSeedSnapshot {\n  compaction: {\n    messageCount: number;\n    summary: string;\n  };\n  memories: SharedDemoSeedMemory[];\n  messages: UIMessage[];\n  threadTitle: string;\n}\n\nconst sharedDemoSnapshots: Record<string, SharedDemoSeedSnapshot> = {\n  \"acme-co\": {\n    compaction: {\n      messageCount: 4,\n      summary:\n        \"Older turns established two durable constraints for Acme Co: legal review is required before marketing claims go live, and executive updates must stay forwardable without over-promising timeline certainty.\",\n    },\n    memories: [\n      {\n        category: \"constraint\",\n        content:\n          \"Marketing claims need legal review before any customer-facing launch language ships.\",\n        sourceMessageId: \"acme-user-2\",\n        title: \"Legal review on claims\",\n      },\n      {\n        category: \"preference\",\n        content:\n          \"Executive-facing updates should be concise, calm, and safe to forward without editing.\",\n        sourceMessageId: \"acme-user-3\",\n        title: \"Forwardable executive updates\",\n      },\n    ],\n    messages: [\n      {\n        id: \"acme-user-1\",\n        parts: [\n          {\n            text: \"We need this launch thread to remember that Acme executives forward our updates directly.\",\n            type: \"text\",\n          },\n        ],\n        role: \"user\",\n      },\n      {\n        id: \"acme-assistant-1\",\n        parts: [\n          {\n            text: \"Understood. I will keep executive-facing updates concise and low-risk.\",\n            type: \"text\",\n          },\n        ],\n        role: \"assistant\",\n      },\n      {\n        id: \"acme-user-2\",\n        parts: [\n          {\n            text: \"Any customer-facing marketing claim still needs legal review before it goes out.\",\n            type: \"text\",\n          },\n        ],\n        role: \"user\",\n      },\n      {\n        id: \"acme-assistant-2\",\n        parts: [\n          {\n            text: \"I will treat legal review as a standing launch constraint for this account.\",\n            type: \"text\",\n          },\n        ],\n        role: \"assistant\",\n      },\n      {\n        id: \"acme-user-3\",\n        parts: [\n          {\n            text: \"Draft the weekly update so leadership can forward it as-is without changing the tone.\",\n            type: \"text\",\n          },\n        ],\n        role: \"user\",\n      },\n      {\n        id: \"acme-assistant-3\",\n        parts: [\n          {\n            text: \"I would frame the update around verified status, next checkpoint, and any blocked risks that already have owners.\",\n            type: \"text\",\n          },\n        ],\n        role: \"assistant\",\n      },\n    ],\n    threadTitle: \"Launch review thread\",\n  },\n  \"helio-dev\": {\n    compaction: {\n      messageCount: 4,\n      summary:\n        \"Earlier Helio Dev turns established that technical depth is preferred over polished phrasing, and any follow-up promise should include a concrete date or checkpoint owner.\",\n    },\n    memories: [\n      {\n        category: \"preference\",\n        content:\n          \"Helio Dev prefers root-cause detail over polished status language.\",\n        sourceMessageId: \"helio-user-1\",\n        title: \"Lead with root cause\",\n      },\n      {\n        category: \"promise\",\n        content:\n          \"Follow-up commitments should include a concrete date or checkpoint owner.\",\n        sourceMessageId: \"helio-user-2\",\n        title: \"Date every promise\",\n      },\n    ],\n    messages: [\n      {\n        id: \"helio-user-1\",\n        parts: [\n          {\n            text: \"For Helio Dev, keep the answer technical. They care more about why the incident happened than polished wording.\",\n            type: \"text\",\n          },\n        ],\n        role: \"user\",\n      },\n      {\n        id: \"helio-assistant-1\",\n        parts: [\n          {\n            text: \"Got it. I will keep the explanation rooted in failure mode, fix, and remaining uncertainty.\",\n            type: \"text\",\n          },\n        ],\n        role: \"assistant\",\n      },\n      {\n        id: \"helio-user-2\",\n        parts: [\n          {\n            text: \"If we promise a follow-up, put a real date on it. They dislike vague 'soon' language.\",\n            type: \"text\",\n          },\n        ],\n        role: \"user\",\n      },\n      {\n        id: \"helio-assistant-2\",\n        parts: [\n          {\n            text: \"I will avoid vague follow-up language and attach a date or named checkpoint.\",\n            type: \"text\",\n          },\n        ],\n        role: \"assistant\",\n      },\n      {\n        id: \"helio-user-3\",\n        parts: [\n          {\n            text: \"Summarize the current blocker for the engineering lead and include the next debug checkpoint.\",\n            type: \"text\",\n          },\n        ],\n        role: \"user\",\n      },\n      {\n        id: \"helio-assistant-3\",\n        parts: [\n          {\n            text: \"The blocker is still the retry queue deadlock. The next checkpoint is a lock-contention profile review tomorrow at 10am with the platform owner.\",\n            type: \"text\",\n          },\n        ],\n        role: \"assistant\",\n      },\n    ],\n    threadTitle: \"Incident follow-up thread\",\n  },\n  \"northstar-logistics\": {\n    compaction: {\n      messageCount: 4,\n      summary:\n        \"Earlier Northstar turns established that downtime language must stay conservative until root cause is confirmed, and every escalation update needs an explicit owner plus next checkpoint across operations and finance.\",\n    },\n    memories: [\n      {\n        category: \"constraint\",\n        content:\n          \"Downtime language must stay conservative until root cause is confirmed.\",\n        sourceMessageId: \"northstar-user-1\",\n        title: \"Conservative incident wording\",\n      },\n      {\n        category: \"follow_up\",\n        content:\n          \"Every escalation update needs a named owner and next checkpoint across operations and finance.\",\n        sourceMessageId: \"northstar-user-2\",\n        title: \"Owner and checkpoint on escalations\",\n      },\n    ],\n    messages: [\n      {\n        id: \"northstar-user-1\",\n        parts: [\n          {\n            text: \"Northstar does not want us calling this resolved until root cause is confirmed. Keep the downtime language conservative.\",\n            type: \"text\",\n          },\n        ],\n        role: \"user\",\n      },\n      {\n        id: \"northstar-assistant-1\",\n        parts: [\n          {\n            text: \"Understood. I will avoid declaring full resolution until the root cause is verified.\",\n            type: \"text\",\n          },\n        ],\n        role: \"assistant\",\n      },\n      {\n        id: \"northstar-user-2\",\n        parts: [\n          {\n            text: \"Escalation notes also need an owner and the next checkpoint so operations and finance stay aligned.\",\n            type: \"text\",\n          },\n        ],\n        role: \"user\",\n      },\n      {\n        id: \"northstar-assistant-2\",\n        parts: [\n          {\n            text: \"I will include both the acting owner and the next checkpoint in every incident update.\",\n            type: \"text\",\n          },\n        ],\n        role: \"assistant\",\n      },\n      {\n        id: \"northstar-user-3\",\n        parts: [\n          {\n            text: \"Prepare the latest incident note for operations and finance with that structure.\",\n            type: \"text\",\n          },\n        ],\n        role: \"user\",\n      },\n      {\n        id: \"northstar-assistant-3\",\n        parts: [\n          {\n            text: \"Current owner: platform operations. Next checkpoint: payment queue replay review at 14:00 UTC. Customer impact remains under investigation, so the incident is still treated as active.\",\n            type: \"text\",\n          },\n        ],\n        role: \"assistant\",\n      },\n    ],\n    threadTitle: \"Operations escalation thread\",\n  },\n};\n\nexport function getCustomerMemorySharedDemoSnapshot(customerId: string) {\n  return sharedDemoSnapshots[customerId] ?? null;\n}\n\ninterface SharedDemoSeedDependencies {\n  compactionStore?: Pick<\n    ReturnType<typeof createCustomerMemoryCompactionStore>,\n    \"saveCompaction\"\n  >;\n  memoryLifecycle?: Pick<\n    ReturnType<typeof createCustomerMemoryLifecycle>,\n    \"seedMemories\"\n  >;\n  threadStore?: Pick<\n    ReturnType<typeof createCustomerMemoryThreadStore>,\n    \"createThread\" | \"listThreads\" | \"saveThreadMessages\"\n  >;\n}\n\nexport async function ensureCustomerMemorySharedDemoSeed(\n  customer: CustomerMemoryProfile,\n  dependencies: SharedDemoSeedDependencies = {}\n) {\n  if (customer.accessMode !== \"shared_readonly\") {\n    return;\n  }\n\n  const snapshot = getCustomerMemorySharedDemoSnapshot(customer.id);\n\n  if (!snapshot) {\n    return;\n  }\n\n  const threadStore =\n    dependencies.threadStore ?? createCustomerMemoryThreadStore();\n  const existingThreads = await threadStore.listThreads({\n    customerId: customer.id,\n    visitorId: customerMemorySharedVisitorId,\n  });\n\n  if (existingThreads.length > 0) {\n    return;\n  }\n\n  const thread = await threadStore.createThread({\n    customerId: customer.id,\n    title: snapshot.threadTitle,\n    visitorId: customerMemorySharedVisitorId,\n  });\n\n  await threadStore.saveThreadMessages({\n    messages: snapshot.messages,\n    threadId: thread.id,\n  });\n\n  await (\n    dependencies.memoryLifecycle ?? createCustomerMemoryLifecycle()\n  ).seedMemories(snapshot.memories, {\n    customerId: customer.id,\n    threadId: thread.id,\n    visitorId: customerMemorySharedVisitorId,\n  });\n\n  const compactionStore =\n    dependencies.compactionStore ?? createCustomerMemoryCompactionStore();\n\n  await compactionStore.saveCompaction({\n    messageCount: snapshot.compaction.messageCount,\n    summary: snapshot.compaction.summary,\n    threadId: thread.id,\n  });\n}\n",
      "type": "registry:lib",
      "target": "@lib/customer-memory-agent/shared-demo-seed.ts"
    },
    {
      "path": "registry/customer-memory-agent/lib/customer-memory-agent/thread-store.ts",
      "content": "import type { UIMessage } from \"ai\";\nimport { and, asc, desc, eq, sql } from \"drizzle-orm\";\n\nimport { loadCustomerMemoryAgentDatabase } from \"./database\";\nimport { getCustomerMemoryAgentEnv } from \"./env\";\nimport { createPortableCustomerMemoryThreadPersistence } from \"./portable-store\";\n\nexport interface CustomerMemoryThreadRecord {\n  createdAt: string;\n  customerId: string;\n  id: string;\n  title: string | null;\n  updatedAt: string;\n  visitorId: string;\n}\n\nexport interface PersistedCustomerMemoryMessage {\n  createdAt: string;\n  id: string;\n  message: UIMessage;\n  messageId: string;\n  messageIndex: number;\n  role: UIMessage[\"role\"];\n  threadId: string;\n}\n\nexport interface CustomerMemoryThreadSnapshot {\n  messages: UIMessage[];\n  thread: CustomerMemoryThreadRecord;\n}\n\nexport interface CustomerMemoryThreadSummary\n  extends CustomerMemoryThreadRecord {\n  messageCount: number;\n}\n\nexport interface CustomerMemoryThreadPersistence {\n  createThread(input: {\n    customerId: string;\n    title?: string | null;\n    visitorId: string;\n  }): Promise<CustomerMemoryThreadRecord>;\n  findThreadById(threadId: string): Promise<CustomerMemoryThreadRecord | null>;\n  loadThreadMessages(\n    threadId: string\n  ): Promise<PersistedCustomerMemoryMessage[]>;\n  listThreadsForCustomer(input: {\n    customerId: string;\n    visitorId: string;\n  }): Promise<CustomerMemoryThreadSummary[]>;\n  replaceThreadMessages(input: {\n    messages: UIMessage[];\n    threadId: string;\n  }): Promise<void>;\n}\n\ninterface CustomerMemoryThreadStoreDependencies {\n  persistence?: CustomerMemoryThreadPersistence;\n}\n\ninterface CustomerMemoryThreadDatabaseModule {\n  customerMemoryMessages: Awaited<\n    ReturnType<typeof loadCustomerMemoryAgentDatabase>\n  >[\"customerMemoryMessages\"];\n  customerMemoryThreads: Awaited<\n    ReturnType<typeof loadCustomerMemoryAgentDatabase>\n  >[\"customerMemoryThreads\"];\n  database: Awaited<\n    ReturnType<typeof loadCustomerMemoryAgentDatabase>\n  >[\"database\"];\n}\n\nexport function getMissingCustomerMemoryThreadError(threadId: string) {\n  return `No customer-memory thread found for ${threadId}.`;\n}\n\nexport function getInvalidCustomerMemoryMessageIdError(messageIndex: number) {\n  return `Expected customer-memory message ${messageIndex} to have a non-empty id before persistence.`;\n}\n\nfunction toIsoString(value: Date | string) {\n  return value instanceof Date ? value.toISOString() : value;\n}\n\nfunction normalizeThreadRecord(record: {\n  createdAt: Date | string;\n  customerId: string;\n  id: string;\n  title: string | null;\n  updatedAt: Date | string;\n  visitorId: string;\n}): CustomerMemoryThreadRecord {\n  return {\n    createdAt: toIsoString(record.createdAt),\n    customerId: record.customerId,\n    id: record.id,\n    title: record.title,\n    updatedAt: toIsoString(record.updatedAt),\n    visitorId: record.visitorId,\n  };\n}\n\nfunction isInvalidCustomerMemoryThreadIdError(error: unknown) {\n  const candidates =\n    error instanceof Error\n      ? [error, (error as Error & { cause?: unknown }).cause]\n      : [error];\n\n  return candidates.some((candidate) => {\n    if (!(candidate instanceof Error)) {\n      return false;\n    }\n\n    const candidateWithCode = candidate as Error & { code?: string };\n\n    return (\n      candidateWithCode.code === \"22P02\" ||\n      candidate.message.includes(\"invalid input syntax for type uuid\")\n    );\n  });\n}\n\nfunction createDatabaseBackedPersistence(): CustomerMemoryThreadPersistence {\n  return {\n    async createThread(input) {\n      const { customerMemoryThreads, database } =\n        await loadCustomerMemoryAgentDatabase();\n      const [row] = await database\n        .insert(customerMemoryThreads)\n        .values({\n          customerId: input.customerId,\n          title: input.title ?? null,\n          visitorId: input.visitorId,\n        })\n        .returning();\n\n      if (!row) {\n        throw new Error(\"Failed to create a customer-memory thread.\");\n      }\n\n      return normalizeThreadRecord(row);\n    },\n    async findThreadById(threadId) {\n      const { customerMemoryThreads, database } =\n        await loadCustomerMemoryAgentDatabase();\n      const [row] = await database\n        .select()\n        .from(customerMemoryThreads)\n        .where(eq(customerMemoryThreads.id, threadId))\n        .limit(1);\n\n      return row ? normalizeThreadRecord(row) : null;\n    },\n    async loadThreadMessages(threadId) {\n      const { customerMemoryMessages, database } =\n        await loadCustomerMemoryAgentDatabase();\n      const rows = await database\n        .select()\n        .from(customerMemoryMessages)\n        .where(eq(customerMemoryMessages.threadId, threadId))\n        .orderBy(asc(customerMemoryMessages.messageIndex));\n\n      return rows.map((row) => ({\n        createdAt: toIsoString(row.createdAt),\n        id: row.id,\n        message: row.payload as UIMessage,\n        messageId: row.messageId,\n        messageIndex: row.messageIndex,\n        role: row.role as UIMessage[\"role\"],\n        threadId: row.threadId,\n      }));\n    },\n    async listThreadsForCustomer(input) {\n      const { customerMemoryMessages, customerMemoryThreads, database } =\n        await loadCustomerMemoryAgentDatabase();\n      const rows = await database\n        .select({\n          createdAt: customerMemoryThreads.createdAt,\n          customerId: customerMemoryThreads.customerId,\n          id: customerMemoryThreads.id,\n          messageCount:\n            sql<number>`count(${customerMemoryMessages.id})`.mapWith(Number),\n          title: customerMemoryThreads.title,\n          updatedAt: customerMemoryThreads.updatedAt,\n          visitorId: customerMemoryThreads.visitorId,\n        })\n        .from(customerMemoryThreads)\n        .leftJoin(\n          customerMemoryMessages,\n          eq(customerMemoryMessages.threadId, customerMemoryThreads.id)\n        )\n        .where(\n          and(\n            eq(customerMemoryThreads.customerId, input.customerId),\n            eq(customerMemoryThreads.visitorId, input.visitorId)\n          )\n        )\n        .groupBy(customerMemoryThreads.id)\n        .orderBy(desc(customerMemoryThreads.updatedAt));\n\n      return rows.map((row) => ({\n        ...normalizeThreadRecord(row),\n        messageCount: row.messageCount,\n      }));\n    },\n    async replaceThreadMessages(input) {\n      const { customerMemoryMessages, customerMemoryThreads, database } =\n        await loadCustomerMemoryAgentDatabase();\n      const thread = await database\n        .select({ id: customerMemoryThreads.id })\n        .from(customerMemoryThreads)\n        .where(eq(customerMemoryThreads.id, input.threadId))\n        .limit(1);\n\n      if (thread.length === 0) {\n        throw new Error(getMissingCustomerMemoryThreadError(input.threadId));\n      }\n\n      await database.transaction(async (tx) => {\n        await tx\n          .delete(customerMemoryMessages)\n          .where(eq(customerMemoryMessages.threadId, input.threadId));\n\n        if (input.messages.length > 0) {\n          await tx.insert(customerMemoryMessages).values(\n            input.messages.map((message, messageIndex) => ({\n              messageId: message.id,\n              messageIndex,\n              payload: message as unknown as Record<string, unknown>,\n              role: message.role,\n              threadId: input.threadId,\n            }))\n          );\n        }\n\n        await tx\n          .update(customerMemoryThreads)\n          .set({ updatedAt: new Date() })\n          .where(eq(customerMemoryThreads.id, input.threadId));\n      });\n    },\n  };\n}\n\nexport function createCustomerMemoryThreadStore(\n  dependencies: CustomerMemoryThreadStoreDependencies = {}\n) {\n  const persistence =\n    dependencies.persistence ??\n    (getCustomerMemoryAgentEnv().DATABASE_URL\n      ? createDatabaseBackedPersistence()\n      : createPortableCustomerMemoryThreadPersistence());\n\n  async function findThreadByIdSafely(threadId: string) {\n    try {\n      return await persistence.findThreadById(threadId);\n    } catch (error) {\n      if (isInvalidCustomerMemoryThreadIdError(error)) {\n        return null;\n      }\n\n      throw error;\n    }\n  }\n\n  return {\n    async createThread(input: {\n      customerId: string;\n      title?: string | null;\n      visitorId: string;\n    }) {\n      return persistence.createThread(input);\n    },\n    async loadThread(\n      threadId: string\n    ): Promise<CustomerMemoryThreadSnapshot | null> {\n      const thread = await findThreadByIdSafely(threadId);\n\n      if (!thread) {\n        return null;\n      }\n\n      const messages = await persistence.loadThreadMessages(threadId);\n\n      return {\n        messages: messages.map((message) => message.message),\n        thread,\n      };\n    },\n    async listThreads(input: { customerId: string; visitorId: string }) {\n      return persistence.listThreadsForCustomer(input);\n    },\n    async loadThreadForViewer(input: {\n      customerId: string;\n      threadId: string;\n      visitorId: string;\n    }): Promise<CustomerMemoryThreadSnapshot | null> {\n      const thread = await findThreadByIdSafely(input.threadId);\n\n      if (\n        !thread ||\n        thread.customerId !== input.customerId ||\n        thread.visitorId !== input.visitorId\n      ) {\n        return null;\n      }\n\n      const messages = await persistence.loadThreadMessages(input.threadId);\n\n      return {\n        messages: messages.map((message) => message.message),\n        thread,\n      };\n    },\n    async saveThreadMessages(input: {\n      messages: UIMessage[];\n      threadId: string;\n    }) {\n      const invalidMessageIndex = input.messages.findIndex(\n        (message) => message.id.trim().length === 0\n      );\n\n      if (invalidMessageIndex >= 0) {\n        throw new Error(\n          getInvalidCustomerMemoryMessageIdError(invalidMessageIndex)\n        );\n      }\n\n      const thread = await findThreadByIdSafely(input.threadId);\n\n      if (!thread) {\n        throw new Error(getMissingCustomerMemoryThreadError(input.threadId));\n      }\n\n      await persistence.replaceThreadMessages(input);\n    },\n  };\n}\n",
      "type": "registry:lib",
      "target": "@lib/customer-memory-agent/thread-store.ts"
    },
    {
      "path": "registry/customer-memory-agent/lib/customer-memory-agent/viewer-context.ts",
      "content": "import type { CustomerMemoryProfile } from \"./customer-profiles\";\n\nexport const customerMemoryVisitorCookieName = \"cm_visitor_id\";\nexport const customerMemorySharedVisitorId = \"demo-shared\";\n\nexport interface CustomerMemoryViewerContext {\n  isReadonly: boolean;\n  visitorId: string;\n}\n\nexport function getReadonlyCustomerMemoryError(\n  customer: CustomerMemoryProfile\n) {\n  return `Customer-memory demo account \"${customer.name}\" is read-only. Switch to Demo Sandbox to create your own threads.`;\n}\n\nexport function resolveCustomerMemoryViewerContext(input: {\n  customer: CustomerMemoryProfile;\n  visitorId: string;\n}): CustomerMemoryViewerContext {\n  if (input.customer.accessMode === \"shared_readonly\") {\n    return {\n      isReadonly: true,\n      visitorId: customerMemorySharedVisitorId,\n    };\n  }\n\n  return {\n    isReadonly: false,\n    visitorId: input.visitorId,\n  };\n}\n\nfunction parseCookieHeader(value: string | null) {\n  if (!value) {\n    return {};\n  }\n\n  return Object.fromEntries(\n    value\n      .split(\";\")\n      .map((part) => part.trim())\n      .filter((part) => part.length > 0)\n      .map((part) => {\n        const separatorIndex = part.indexOf(\"=\");\n\n        if (separatorIndex === -1) {\n          return [part, \"\"] as const;\n        }\n\n        return [\n          decodeURIComponent(part.slice(0, separatorIndex)),\n          decodeURIComponent(part.slice(separatorIndex + 1)),\n        ] as const;\n      })\n  );\n}\n\nexport function readCustomerMemoryVisitorId(request: Request) {\n  const cookies = parseCookieHeader(request.headers.get(\"cookie\"));\n  const visitorId = cookies[customerMemoryVisitorCookieName];\n\n  return typeof visitorId === \"string\" && visitorId.trim().length > 0\n    ? visitorId.trim()\n    : null;\n}\n\nexport function getOrCreateCustomerMemoryVisitorId(request: Request) {\n  const existingVisitorId = readCustomerMemoryVisitorId(request);\n\n  if (existingVisitorId) {\n    return {\n      shouldSetCookie: false,\n      visitorId: existingVisitorId,\n    };\n  }\n\n  return {\n    shouldSetCookie: true,\n    visitorId: crypto.randomUUID(),\n  };\n}\n\nexport function buildCustomerMemoryVisitorCookie(visitorId: string) {\n  return `${customerMemoryVisitorCookieName}=${encodeURIComponent(visitorId)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${60 * 60 * 24 * 30}`;\n}\n",
      "type": "registry:lib",
      "target": "@lib/customer-memory-agent/viewer-context.ts"
    }
  ],
  "envVars": {
    "AI_GATEWAY_API_KEY": "",
    "AI_GATEWAY_BASE_URL": "https://ai-gateway.vercel.sh/v3/ai",
    "AI_GATEWAY_CHAT_MODEL": "openai/gpt-4.1-mini"
  },
  "docs": "Install into a Next.js App Router project initialized with shadcn/ui. This item adds the customer-memory demo page, session and thread routes, visitor-scoped in-memory persistence, saved-memory retrieval, compaction checkpoints, and AI Gateway env vars required for end-to-end chat. Add DATABASE_URL when you want Postgres-backed persistence.",
  "type": "registry:block"
}
