{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "persistent-agent",
  "title": "Persistent Agent",
  "description": "An addressable chat resource that persists conversation state in Postgres and reconnects in-flight streams through Redis.",
  "dependencies": [
    "@ai-sdk/react",
    "@base-ui/react",
    "@neondatabase/serverless",
    "@phosphor-icons/react",
    "ai",
    "class-variance-authority",
    "drizzle-orm",
    "ioredis",
    "lucide-react",
    "resumable-stream"
  ],
  "registryDependencies": [
    "utils",
    "badge",
    "breadcrumb",
    "button",
    "textarea",
    "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"
  ],
  "files": [
    {
      "path": "registry/persistent-agent/app/demos/persistent-agent/page.tsx",
      "content": "import { loadPersistentAgentScreenData } from \"@/lib/persistent-agent/server/session\";\nimport { PersistentAgentScreen } from \"@/components/persistent-agent/persistent-agent-screen\";\n\nexport default async function PersistentAgentPage() {\n  const screenData = await loadPersistentAgentScreenData({\n    chatId: null,\n  });\n\n  return <PersistentAgentScreen {...screenData} />;\n}\n",
      "type": "registry:page",
      "target": "app/demos/persistent-agent/page.tsx"
    },
    {
      "path": "registry/persistent-agent/app/demos/persistent-agent/[id]/page.tsx",
      "content": "import { notFound } from \"next/navigation\";\n\nimport { loadPersistentAgentScreenData } from \"@/lib/persistent-agent/server/session\";\nimport { PersistentAgentScreen } from \"@/components/persistent-agent/persistent-agent-screen\";\n\nexport default async function PersistentAgentConversationPage({\n  params,\n}: {\n  params: Promise<{ id: string }>;\n}) {\n  const { id } = await params;\n  const screenData = await loadPersistentAgentScreenData({\n    chatId: id,\n  });\n\n  if (!screenData.initialSession) {\n    notFound();\n  }\n\n  return <PersistentAgentScreen {...screenData} />;\n}\n",
      "type": "registry:page",
      "target": "app/demos/persistent-agent/[id]/page.tsx"
    },
    {
      "path": "registry/persistent-agent/app/api/demos/persistent-agent/route.ts",
      "content": "import { handlePersistentAgentChatRequest } from \"@/lib/persistent-agent/server/runtime\";\nimport {\n  buildPersistentAgentVisitorCookie,\n  getOrCreatePersistentAgentVisitorId,\n} from \"@/lib/persistent-agent/server/viewer-context\";\n\nexport async function POST(request: Request) {\n  const visitor = getOrCreatePersistentAgentVisitorId(request);\n  const response = await handlePersistentAgentChatRequest(request, {\n    visitorId: visitor.visitorId,\n  });\n\n  if (visitor.shouldSetCookie) {\n    response.headers.append(\n      \"set-cookie\",\n      buildPersistentAgentVisitorCookie(visitor.visitorId)\n    );\n  }\n\n  return response;\n}\n",
      "type": "registry:page",
      "target": "app/api/demos/persistent-agent/route.ts"
    },
    {
      "path": "registry/persistent-agent/app/api/demos/persistent-agent/[id]/stream/route.ts",
      "content": "import { handlePersistentAgentStreamResumeRequest } from \"@/lib/persistent-agent/server/runtime\";\nimport {\n  buildPersistentAgentVisitorCookie,\n  getOrCreatePersistentAgentVisitorId,\n} from \"@/lib/persistent-agent/server/viewer-context\";\n\nexport async function GET(\n  request: Request,\n  { params }: { params: Promise<{ id: string }> }\n) {\n  const { id } = await params;\n  const visitor = getOrCreatePersistentAgentVisitorId(request);\n  const response = await handlePersistentAgentStreamResumeRequest(id, {\n    visitorId: visitor.visitorId,\n  });\n\n  if (visitor.shouldSetCookie) {\n    response.headers.append(\n      \"set-cookie\",\n      buildPersistentAgentVisitorCookie(visitor.visitorId)\n    );\n  }\n\n  return response;\n}\n",
      "type": "registry:page",
      "target": "app/api/demos/persistent-agent/[id]/stream/route.ts"
    },
    {
      "path": "registry/persistent-agent/app/api/demos/persistent-agent/[id]/session/route.ts",
      "content": "import { handlePersistentAgentSessionRequest } from \"@/lib/persistent-agent/server/runtime\";\nimport {\n  buildPersistentAgentVisitorCookie,\n  getOrCreatePersistentAgentVisitorId,\n} from \"@/lib/persistent-agent/server/viewer-context\";\n\nexport async function GET(\n  request: Request,\n  { params }: { params: Promise<{ id: string }> }\n) {\n  const { id } = await params;\n  const visitor = getOrCreatePersistentAgentVisitorId(request);\n  const response = await handlePersistentAgentSessionRequest(id, {\n    visitorId: visitor.visitorId,\n  });\n\n  if (visitor.shouldSetCookie) {\n    response.headers.append(\n      \"set-cookie\",\n      buildPersistentAgentVisitorCookie(visitor.visitorId)\n    );\n  }\n\n  return response;\n}\n",
      "type": "registry:page",
      "target": "app/api/demos/persistent-agent/[id]/session/route.ts"
    },
    {
      "path": "registry/persistent-agent/components/persistent-agent/persistent-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 {\n  getPersistentAgentRuntimeState,\n  type PersistentAgentRuntimeState,\n} from \"@/lib/persistent-agent/server/runtime\";\nimport type { PersistentAgentScreenData } from \"@/lib/persistent-agent/server/session\";\nimport { PersistentAgentWorkspace } from \"./persistent-agent-workspace\";\n\ninterface PersistentAgentScreenProps extends PersistentAgentScreenData {\n  runtimeState?: PersistentAgentRuntimeState;\n}\n\nexport function PersistentAgentScreen({\n  draftChatId,\n  initialSession,\n  recentChats,\n  runtimeState = getPersistentAgentRuntimeState(),\n}: PersistentAgentScreenProps) {\n  return (\n    <main className=\"min-h-svh bg-background text-foreground\">\n      <div className=\"mx-auto flex w-full max-w-7xl 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                    Persistent &amp; Resume Agent\n                  </BreadcrumbPage>\n                </BreadcrumbItem>\n              </BreadcrumbList>\n            </Breadcrumb>\n            <h1 className=\"max-w-3xl font-medium text-2xl tracking-tight\">\n              Persistent &amp; Resume Agent\n            </h1>\n            <p className=\"max-w-3xl text-muted-foreground text-sm/relaxed\">\n              This demo focuses on one contract: a chat becomes an addressable\n              resource at <code>/demos/persistent-agent/[id]</code>, survives\n              refresh, and keeps its visitor-private identity through a\n              server-set cookie.\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.chatModel}</Badge>\n            <Badge variant=\"outline\">{runtimeState.persistenceLabel}</Badge>\n            <Badge variant=\"outline\">{runtimeState.resumeLabel}</Badge>\n          </div>\n        </header>\n\n        <div className=\"lg:h-svh\">\n          <PersistentAgentWorkspace\n            chatModel={runtimeState.chatModel}\n            draftChatId={draftChatId}\n            initialSession={initialSession}\n            isChatAvailable={runtimeState.isChatAvailable}\n            nodeVersion={runtimeState.nodeVersion}\n            persistenceLabel={runtimeState.persistenceLabel}\n            recentChats={recentChats}\n            resumeLabel={runtimeState.resumeLabel}\n            setupMessage={runtimeState.setupMessage}\n          />\n        </div>\n      </div>\n    </main>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/persistent-agent/persistent-agent-screen.tsx"
    },
    {
      "path": "registry/persistent-agent/components/persistent-agent/persistent-agent-workspace.tsx",
      "content": "\"use client\";\n\nimport { useChat } from \"@ai-sdk/react\";\nimport { ArrowClockwiseIcon, LinkIcon, RobotIcon } from \"@phosphor-icons/react\";\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 { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\nimport { DefaultChatTransport, type UIMessage } from \"ai\";\nimport Link from \"next/link\";\nimport {\n  type Dispatch,\n  type SetStateAction,\n  useEffect,\n  useRef,\n  useState,\n} from \"react\";\n\nimport type {\n  PersistentAgentChatRecord,\n  PersistentAgentChatSession,\n} from \"@/lib/persistent-agent/server/chat-store\";\nimport { shouldShowResumeThinking } from \"./resume-pending-state\";\n\nfunction getTextContent(message: UIMessage) {\n  return message.parts\n    .filter((part) => part.type === \"text\")\n    .map((part) => part.text)\n    .join(\"\\n\");\n}\n\nfunction formatDateTime(value: string | null) {\n  if (!value) {\n    return \"Pending\";\n  }\n\n  const date = new Date(value);\n\n  if (Number.isNaN(date.valueOf())) {\n    return value;\n  }\n\n  return new Intl.DateTimeFormat(\"en\", {\n    day: \"2-digit\",\n    hour: \"2-digit\",\n    minute: \"2-digit\",\n    month: \"short\",\n  }).format(date);\n}\n\nfunction toConversationPath(chatId: string) {\n  return `/demos/persistent-agent/${chatId}`;\n}\n\nconst nodeVersionPrefixPattern = /^v/;\nconst resumeRecoveryAttemptLimit = 6;\nconst persistentAgentSamplePrompts = [\n  \"Explain the route and storage contract for this persistent chat demo.\",\n  \"Summarize how a user could adapt this demo from in-memory storage to Postgres and Redis.\",\n  \"Give me a concise QA checklist for verifying refresh-safe chat state.\",\n] as const;\n\nasync function loadPersistentAgentSessionSnapshot(chatId: string) {\n  const response = await fetch(\n    `/api/demos/persistent-agent/${chatId}/session`,\n    {\n      credentials: \"include\",\n    }\n  );\n\n  if (!response.ok) {\n    return null;\n  }\n\n  return (await response.json()) as PersistentAgentChatSession;\n}\n\nfunction shouldApplyRecoveredSession(\n  session: PersistentAgentChatSession,\n  messagesLength: number\n) {\n  return (\n    session.messages.length > messagesLength ||\n    session.messages.at(-1)?.role !== \"user\"\n  );\n}\n\ninterface PersistentAgentWorkspaceProps {\n  chatModel: string;\n  draftChatId: string;\n  initialSession: PersistentAgentChatSession | null;\n  isChatAvailable: boolean;\n  nodeVersion: string;\n  persistenceLabel: string;\n  recentChats: PersistentAgentChatRecord[];\n  resumeLabel: string;\n  setupMessage: string | null;\n}\n\nfunction useResumeRecovery(input: {\n  chatId: string;\n  initialSession: PersistentAgentChatSession | null;\n  latestMessageRole: UIMessage[\"role\"] | undefined;\n  messagesLength: number;\n  setChatMeta: Dispatch<\n    SetStateAction<{\n      createdAt: string | null;\n      id: string;\n      updatedAt: string | null;\n    }>\n  >;\n  setMessages: (messages: UIMessage[]) => void;\n  status: string;\n}) {\n  const {\n    chatId,\n    initialSession,\n    latestMessageRole,\n    messagesLength,\n    setChatMeta,\n    setMessages,\n    status,\n  } = input;\n  const shouldRecoverMissedResumeRef = useRef(\n    initialSession?.chat.activeStreamId != null ||\n      initialSession?.messages.at(-1)?.role === \"user\"\n  );\n  const initialMessageCountRef = useRef(initialSession?.messages.length ?? 0);\n\n  useEffect(() => {\n    if (!shouldRecoverMissedResumeRef.current) {\n      return;\n    }\n\n    if (status !== \"ready\") {\n      return;\n    }\n\n    if (\n      messagesLength > initialMessageCountRef.current ||\n      latestMessageRole !== \"user\"\n    ) {\n      shouldRecoverMissedResumeRef.current = false;\n      return;\n    }\n\n    let isCancelled = false;\n    let timeoutId: number | undefined;\n    let attempt = 0;\n\n    const runRecoveryAttempt = () => {\n      attempt += 1;\n\n      loadPersistentAgentSessionSnapshot(chatId).then((session) => {\n        if (isCancelled) {\n          return;\n        }\n\n        if (session && shouldApplyRecoveredSession(session, messagesLength)) {\n          shouldRecoverMissedResumeRef.current = false;\n          setMessages(session.messages);\n          setChatMeta({\n            createdAt: session.chat.createdAt,\n            id: session.chat.id,\n            updatedAt: session.chat.updatedAt,\n          });\n          return;\n        }\n\n        if (attempt >= resumeRecoveryAttemptLimit) {\n          shouldRecoverMissedResumeRef.current = false;\n          return;\n        }\n\n        timeoutId = window.setTimeout(runRecoveryAttempt, 1500);\n      });\n    };\n\n    timeoutId = window.setTimeout(runRecoveryAttempt, 2500);\n\n    return () => {\n      isCancelled = true;\n      if (timeoutId) {\n        window.clearTimeout(timeoutId);\n      }\n    };\n  }, [\n    chatId,\n    latestMessageRole,\n    messagesLength,\n    setChatMeta,\n    setMessages,\n    status,\n  ]);\n}\n\nexport function PersistentAgentWorkspace({\n  chatModel,\n  draftChatId,\n  initialSession,\n  isChatAvailable,\n  nodeVersion,\n  persistenceLabel,\n  recentChats,\n  resumeLabel,\n  setupMessage,\n}: PersistentAgentWorkspaceProps) {\n  const generatedChatIdRef = useRef(initialSession?.chat.id ?? draftChatId);\n  const hasPromotedRouteRef = useRef(Boolean(initialSession));\n  const chatId = generatedChatIdRef.current;\n  const [chatMeta, setChatMeta] = useState(() => ({\n    createdAt: initialSession?.chat.createdAt ?? null,\n    id: chatId,\n    updatedAt: initialSession?.chat.updatedAt ?? null,\n  }));\n  const { error, messages, resumeStream, sendMessage, setMessages, status } =\n    useChat({\n      id: chatId,\n      messages: initialSession?.messages ?? [],\n      resume: initialSession?.chat.activeStreamId != null,\n      transport: new DefaultChatTransport({\n        api: \"/api/demos/persistent-agent\",\n        credentials: \"include\",\n        prepareReconnectToStreamRequest: ({ id }) => ({\n          api: `/api/demos/persistent-agent/${id}/stream`,\n          credentials: \"include\",\n        }),\n        prepareSendMessagesRequest: ({ id, messages: nextMessages }) => ({\n          body: {\n            id,\n            message: nextMessages.at(-1),\n          },\n        }),\n      }),\n    });\n  const hasMessages = messages.length > 0;\n  const isBusy = status === \"submitted\" || status === \"streaming\";\n  const latestAssistant = [...messages]\n    .reverse()\n    .find((message) => message.role === \"assistant\");\n  const latestMessageRole = messages.at(-1)?.role;\n  const latestAssistantText = latestAssistant\n    ? getTextContent(latestAssistant)\n    : \"\";\n  const showThinking = isBusy && latestAssistantText.length === 0;\n  const showResumeThinking = shouldShowResumeThinking({\n    initialSession,\n    messages,\n  });\n  const submitStatus =\n    showResumeThinking && status === \"streaming\" ? \"submitted\" : status;\n\n  useEffect(() => {\n    if (messages.length === 0) {\n      return;\n    }\n\n    setChatMeta((current) => ({\n      ...current,\n      createdAt: current.createdAt ?? new Date().toISOString(),\n      updatedAt: new Date().toISOString(),\n    }));\n  }, [messages.length]);\n\n  useResumeRecovery({\n    chatId,\n    initialSession,\n    latestMessageRole,\n    messagesLength: messages.length,\n    setChatMeta,\n    setMessages,\n    status,\n  });\n\n  async function handleSubmit(text: string) {\n    const trimmedText = text.trim();\n\n    if (trimmedText.length === 0) {\n      return;\n    }\n\n    if (!hasPromotedRouteRef.current) {\n      hasPromotedRouteRef.current = true;\n      window.history.replaceState({}, \"\", toConversationPath(chatId));\n      setChatMeta((current) => ({\n        ...current,\n        createdAt: current.createdAt ?? new Date().toISOString(),\n      }));\n    }\n\n    await sendMessage({ text: trimmedText });\n  }\n\n  function renderMessageBody(message: UIMessage) {\n    const text = getTextContent(message);\n\n    if (text) {\n      return <MessageResponse>{text}</MessageResponse>;\n    }\n\n    if (message.role === \"assistant\" && showThinking) {\n      return <Shimmer className=\"text-sm\">Thinking...</Shimmer>;\n    }\n\n    return (\n      <p className=\"text-muted-foreground text-sm\">\n        Waiting for visible output.\n      </p>\n    );\n  }\n\n  return (\n    <div className=\"grid min-h-[70svh] gap-4 lg:h-full lg:min-h-0 lg:grid-cols-[minmax(0,1fr)_20rem]\">\n      <section className=\"flex min-h-[70svh] flex-col border border-foreground/10 bg-background lg:h-full lg: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\n        {error ? (\n          <div className=\"border-foreground/10 border-b px-4 py-3 text-destructive text-xs/relaxed\">\n            {error.message}\n          </div>\n        ) : null}\n\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            {hasMessages ? (\n              <>\n                {messages.map((message) => (\n                  <Message from={message.role} key={message.id}>\n                    <MessageContent\n                      className={cn(\n                        message.role === \"assistant\" ? \"max-w-3xl\" : \"max-w-2xl\"\n                      )}\n                    >\n                      {renderMessageBody(message)}\n                    </MessageContent>\n                  </Message>\n                ))}\n                {showResumeThinking ? (\n                  <Message from=\"assistant\">\n                    <MessageContent className=\"max-w-3xl\">\n                      <Shimmer className=\"text-sm\">Thinking...</Shimmer>\n                    </MessageContent>\n                  </Message>\n                ) : null}\n              </>\n            ) : (\n              <ConversationEmptyState\n                description=\"Start a conversation, refresh the page, and the same chat will restore under this URL.\"\n                icon={<RobotIcon className=\"size-5\" />}\n                title=\"Persistent route is ready\"\n              />\n            )}\n          </ConversationContent>\n          <ConversationScrollButton />\n        </Conversation>\n\n        <div className=\"border-foreground/10 border-t px-4 py-4\">\n          <div className=\"mx-auto w-full max-w-3xl\">\n            <PromptInput onSubmit={({ text }) => handleSubmit(text)}>\n              <PromptInputBody>\n                <PromptInputTextarea\n                  disabled={!isChatAvailable || isBusy}\n                  placeholder=\"Ask a question, refresh mid-stream, then return to the same URL.\"\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\">Visitor scoped</Badge>\n                  <Badge variant=\"outline\">{persistenceLabel}</Badge>\n                  <Badge variant=\"outline\">{chatModel}</Badge>\n                </div>\n                <PromptInputSubmit\n                  disabled={!isChatAvailable}\n                  status={submitStatus}\n                />\n              </PromptInputFooter>\n            </PromptInput>\n            {hasMessages ? null : (\n              <div className=\"mt-3 flex flex-wrap gap-2\">\n                {persistentAgentSamplePrompts.map((prompt) => (\n                  <Button\n                    className=\"max-w-full justify-start text-left\"\n                    disabled={!isChatAvailable || isBusy}\n                    key={prompt}\n                    onClick={() => void handleSubmit(prompt)}\n                    size=\"sm\"\n                    type=\"button\"\n                    variant=\"outline\"\n                  >\n                    {prompt}\n                  </Button>\n                ))}\n              </div>\n            )}\n          </div>\n        </div>\n      </section>\n\n      <aside className=\"border border-foreground/10 bg-background p-4 lg:min-h-0 lg:overflow-y-auto\">\n        <div className=\"space-y-5\">\n          <div className=\"flex items-center justify-between gap-2\">\n            <div>\n              <p className=\"text-[11px] text-muted-foreground uppercase tracking-[0.2em]\">\n                Session\n              </p>\n              <p className=\"mt-1 font-medium text-sm\">\n                {initialSession ? \"Restored route\" : \"New route promotion\"}\n              </p>\n            </div>\n            <Link\n              className=\"inline-flex h-8 items-center justify-center border border-foreground/10 px-3 text-sm transition-colors hover:border-foreground/30\"\n              href=\"/demos/persistent-agent\"\n            >\n              New chat\n            </Link>\n          </div>\n\n          <div>\n            <p className=\"text-[11px] text-muted-foreground uppercase tracking-[0.2em]\">\n              Chat ID\n            </p>\n            <p className=\"mt-1 break-all font-mono text-xs\">{chatMeta.id}</p>\n          </div>\n\n          <div className=\"grid gap-3 sm:grid-cols-2 lg:grid-cols-1\">\n            <div>\n              <p className=\"text-[11px] text-muted-foreground uppercase tracking-[0.2em]\">\n                Created\n              </p>\n              <p className=\"mt-1 text-sm\">\n                {formatDateTime(chatMeta.createdAt)}\n              </p>\n            </div>\n            <div>\n              <p className=\"text-[11px] text-muted-foreground uppercase tracking-[0.2em]\">\n                Updated\n              </p>\n              <p className=\"mt-1 text-sm\">\n                {formatDateTime(chatMeta.updatedAt)}\n              </p>\n            </div>\n          </div>\n\n          <div>\n            <p className=\"text-[11px] text-muted-foreground uppercase tracking-[0.2em]\">\n              Route contract\n            </p>\n            <p className=\"mt-1 text-sm\">\n              First send promotes this page into a stable URL. Refresh reloads\n              messages from the active store. Redis-backed deployments can\n              reconnect mid-stream through the resume endpoint.\n            </p>\n          </div>\n\n          <div>\n            <p className=\"text-[11px] text-muted-foreground uppercase tracking-[0.2em]\">\n              Recent chats\n            </p>\n            <div className=\"mt-2 grid gap-2\">\n              {recentChats.length > 0 ? (\n                recentChats.slice(0, 6).map((chat) => (\n                  <Link\n                    className={cn(\n                      \"border px-3 py-2 text-left text-sm transition-colors\",\n                      chat.id === chatMeta.id\n                        ? \"border-foreground bg-foreground text-background\"\n                        : \"border-foreground/10 hover:border-foreground/30\"\n                    )}\n                    href={toConversationPath(chat.id)}\n                    key={chat.id}\n                  >\n                    <div className=\"flex items-start justify-between gap-3\">\n                      <span className=\"line-clamp-2\">{chat.title}</span>\n                      <LinkIcon className=\"mt-0.5 size-3.5 shrink-0\" />\n                    </div>\n                    <p\n                      className={cn(\n                        \"mt-2 text-[11px]\",\n                        chat.id === chatMeta.id\n                          ? \"text-background/80\"\n                          : \"text-muted-foreground\"\n                      )}\n                    >\n                      {formatDateTime(chat.updatedAt)}\n                    </p>\n                  </Link>\n                ))\n              ) : (\n                <p className=\"text-muted-foreground text-sm\">\n                  No chats for this visitor yet.\n                </p>\n              )}\n            </div>\n          </div>\n\n          <div>\n            <p className=\"text-[11px] text-muted-foreground uppercase tracking-[0.2em]\">\n              Runtime\n            </p>\n            <div className=\"mt-2 flex flex-wrap gap-2\">\n              <Badge variant=\"outline\">\n                Node {nodeVersion.replace(nodeVersionPrefixPattern, \"\")}\n              </Badge>\n              <Badge variant=\"outline\">HTTP-only cookie</Badge>\n              <Badge variant=\"outline\">{resumeLabel}</Badge>\n            </div>\n          </div>\n\n          {initialSession?.chat.activeStreamId ? (\n            <Button\n              onClick={() => resumeStream()}\n              size=\"sm\"\n              type=\"button\"\n              variant=\"outline\"\n            >\n              <ArrowClockwiseIcon className=\"size-3.5\" />\n              Retry resume\n            </Button>\n          ) : null}\n        </div>\n      </aside>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/persistent-agent/persistent-agent-workspace.tsx"
    },
    {
      "path": "registry/persistent-agent/components/persistent-agent/resume-pending-state.ts",
      "content": "import type { UIMessage } from \"ai\";\n\nimport type { PersistentAgentChatSession } from \"@/lib/persistent-agent/server/chat-store\";\n\nexport function shouldShowResumeThinking(input: {\n  initialSession: PersistentAgentChatSession | null;\n  messages: UIMessage[];\n}) {\n  const { initialSession, messages } = input;\n\n  if (!initialSession?.chat.activeStreamId) {\n    return false;\n  }\n\n  return messages.at(-1)?.role === \"user\";\n}\n",
      "type": "registry:component",
      "target": "@components/persistent-agent/resume-pending-state.ts"
    },
    {
      "path": "registry/persistent-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/persistent-agent/lib/persistent-agent/server/chat-store.ts",
      "content": "import { and, asc, desc, eq, lt } from \"drizzle-orm\";\nimport type { UIMessage } from \"ai\";\nimport {\n  persistentAgentChats,\n  persistentAgentMessages,\n} from \"./schema\";\nimport { getPersistentAgentEnv } from \"./env\";\nimport { createPortablePersistentAgentChatPersistence } from \"./portable-store\";\n\nconst maxPersistentAgentTitleLength = 72;\n\nexport const persistentAgentCleanupCronScheduleUtc = \"0 20 * * *\";\nexport const persistentAgentCleanupRetentionDays = 7;\n\nexport interface PersistentAgentChatRecord {\n  activeStreamId: string | null;\n  createdAt: string;\n  id: string;\n  title: string;\n  updatedAt: string;\n  visitorId: string;\n}\n\nexport interface PersistentAgentChatSession {\n  chat: PersistentAgentChatRecord;\n  messages: UIMessage[];\n}\n\nexport interface PersistentAgentChatPersistence {\n  listChatsForVisitor(visitorId: string): Promise<PersistentAgentChatRecord[]>;\n  loadChatSession(\n    chatId: string,\n    visitorId: string\n  ): Promise<PersistentAgentChatSession | null>;\n  saveIncomingUserMessage(input: {\n    chatId: string;\n    message: UIMessage;\n    visitorId: string;\n  }): Promise<void>;\n  saveFinishedMessages(input: {\n    chatId: string;\n    messages: UIMessage[];\n    visitorId: string;\n  }): Promise<void>;\n  setActiveStream(input: {\n    activeStreamId: string | null;\n    chatId: string;\n    visitorId: string;\n  }): Promise<void>;\n  cleanupExpiredChats(input?: { now?: Date }): Promise<{\n    deletedChats: number;\n    expiresBefore: string;\n  }>;\n}\n\ninterface PersistentAgentChatStoreDependencies {\n  persistence?: PersistentAgentChatPersistence;\n}\n\ntype PersistentAgentDatabaseModule = Awaited<\n  ReturnType<typeof import(\"./database\")[\"loadPersistentAgentDatabase\"]>\n>;\n\nfunction toIsoString(value: Date | string) {\n  return value instanceof Date ? value.toISOString() : value;\n}\n\nfunction normalizeChatRecord(record: {\n  activeStreamId: string | null;\n  createdAt: Date | string;\n  id: string;\n  title: string;\n  updatedAt: Date | string;\n  visitorId: string;\n}): PersistentAgentChatRecord {\n  return {\n    activeStreamId: record.activeStreamId,\n    createdAt: toIsoString(record.createdAt),\n    id: record.id,\n    title: record.title,\n    updatedAt: toIsoString(record.updatedAt),\n    visitorId: record.visitorId,\n  };\n}\n\nasync function loadPersistentAgentDatabase(): Promise<PersistentAgentDatabaseModule> {\n  return import(\"./database\").then((module) => module.loadPersistentAgentDatabase());\n}\n\nfunction getMessageText(message: UIMessage) {\n  return message.parts\n    .map((part) => (part.type === \"text\" ? part.text : \"\"))\n    .filter((part) => part.length > 0)\n    .join(\" \")\n    .replace(/\\s+/g, \" \")\n    .trim();\n}\n\nfunction buildChatTitle(message: UIMessage) {\n  const text = getMessageText(message);\n\n  if (text.length === 0) {\n    return \"New chat\";\n  }\n\n  return text.slice(0, maxPersistentAgentTitleLength);\n}\n\nfunction readMessageCreatedAt(message: UIMessage, fallback: Date) {\n  const metadata = message.metadata as { createdAt?: string } | undefined;\n\n  if (!metadata?.createdAt) {\n    return fallback;\n  }\n\n  const createdAt = new Date(metadata.createdAt);\n\n  return Number.isNaN(createdAt.valueOf()) ? fallback : createdAt;\n}\n\nfunction toUiMessage(row: {\n  createdAt: Date | string;\n  messageId: string;\n  parts: unknown;\n  role: string;\n}) {\n  return {\n    id: row.messageId,\n    metadata: {\n      createdAt: toIsoString(row.createdAt),\n    },\n    parts: row.parts as UIMessage[\"parts\"],\n    role: row.role as UIMessage[\"role\"],\n  } satisfies UIMessage;\n}\n\nexport function getPersistentAgentChatNotFoundError(chatId: string) {\n  return `No persistent-agent chat found for ${chatId}.`;\n}\n\nfunction isInvalidPersistentAgentChatIdError(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 createDatabaseBackedPersistentAgentChatPersistence(): PersistentAgentChatPersistence {\n  return {\n    async listChatsForVisitor(visitorId: string) {\n      const { database, persistentAgentChats } =\n        await loadPersistentAgentDatabase();\n      const rows = await database\n        .select()\n        .from(persistentAgentChats)\n        .where(eq(persistentAgentChats.visitorId, visitorId))\n        .orderBy(desc(persistentAgentChats.updatedAt));\n\n      return rows.map(normalizeChatRecord);\n    },\n    async loadChatSession(\n      chatId: string,\n      visitorId: string\n    ): Promise<PersistentAgentChatSession | null> {\n      try {\n        const { database, persistentAgentChats, persistentAgentMessages } =\n          await loadPersistentAgentDatabase();\n        const [chat] = await database\n          .select()\n          .from(persistentAgentChats)\n          .where(\n            and(\n              eq(persistentAgentChats.id, chatId),\n              eq(persistentAgentChats.visitorId, visitorId)\n            )\n          )\n          .limit(1);\n\n        if (!chat) {\n          return null;\n        }\n\n        const rows = await database\n          .select()\n          .from(persistentAgentMessages)\n          .where(eq(persistentAgentMessages.chatId, chatId))\n          .orderBy(asc(persistentAgentMessages.createdAt));\n\n        return {\n          chat: normalizeChatRecord(chat),\n          messages: rows.map(toUiMessage),\n        };\n      } catch (error) {\n        if (isInvalidPersistentAgentChatIdError(error)) {\n          return null;\n        }\n\n        throw error;\n      }\n    },\n    async saveIncomingUserMessage(input: {\n      chatId: string;\n      message: UIMessage;\n      visitorId: string;\n    }) {\n      const { database, persistentAgentChats, persistentAgentMessages } =\n        await loadPersistentAgentDatabase();\n      const now = new Date();\n      const title = buildChatTitle(input.message);\n\n      await database.transaction(async (tx) => {\n        const [existingChat] = await tx\n          .select()\n          .from(persistentAgentChats)\n          .where(eq(persistentAgentChats.id, input.chatId))\n          .limit(1);\n\n        if (existingChat && existingChat.visitorId !== input.visitorId) {\n          throw new Error(getPersistentAgentChatNotFoundError(input.chatId));\n        }\n\n        if (existingChat) {\n          await tx\n            .update(persistentAgentChats)\n            .set({\n              activeStreamId: null,\n              title:\n                existingChat.title === \"New chat\" && title !== \"New chat\"\n                  ? title\n                  : existingChat.title,\n              updatedAt: now,\n            })\n            .where(eq(persistentAgentChats.id, input.chatId));\n        } else {\n          await tx.insert(persistentAgentChats).values({\n            activeStreamId: null,\n            id: input.chatId,\n            title,\n            updatedAt: now,\n            visitorId: input.visitorId,\n          });\n        }\n\n        await tx\n          .insert(persistentAgentMessages)\n          .values({\n            attachments: [],\n            chatId: input.chatId,\n            createdAt: readMessageCreatedAt(input.message, now),\n            messageId: input.message.id,\n            parts: input.message.parts,\n            role: input.message.role,\n          })\n          .onConflictDoUpdate({\n            set: {\n              parts: input.message.parts,\n              role: input.message.role,\n            },\n            target: [\n              persistentAgentMessages.chatId,\n              persistentAgentMessages.messageId,\n            ],\n          });\n      });\n    },\n    async saveFinishedMessages(input: {\n      chatId: string;\n      messages: UIMessage[];\n      visitorId: string;\n    }) {\n      const { database, persistentAgentChats, persistentAgentMessages } =\n        await loadPersistentAgentDatabase();\n      const now = new Date();\n\n      await database.transaction(async (tx) => {\n        const [chat] = await tx\n          .select()\n          .from(persistentAgentChats)\n          .where(\n            and(\n              eq(persistentAgentChats.id, input.chatId),\n              eq(persistentAgentChats.visitorId, input.visitorId)\n            )\n          )\n          .limit(1);\n\n        if (!chat) {\n          throw new Error(getPersistentAgentChatNotFoundError(input.chatId));\n        }\n\n        if (input.messages.length > 0) {\n          await tx\n            .insert(persistentAgentMessages)\n            .values(\n              input.messages.map((message) => ({\n                attachments: [],\n                chatId: input.chatId,\n                createdAt: readMessageCreatedAt(message, now),\n                messageId: message.id,\n                parts: message.parts,\n                role: message.role,\n              }))\n            )\n            .onConflictDoNothing({\n              target: [\n                persistentAgentMessages.chatId,\n                persistentAgentMessages.messageId,\n              ],\n            });\n        }\n\n        await tx\n          .update(persistentAgentChats)\n          .set({\n            activeStreamId: null,\n            updatedAt: now,\n          })\n          .where(eq(persistentAgentChats.id, input.chatId));\n      });\n    },\n    async setActiveStream(input: {\n      activeStreamId: string | null;\n      chatId: string;\n      visitorId: string;\n    }) {\n      const { database, persistentAgentChats } =\n        await loadPersistentAgentDatabase();\n      const rows = await database\n        .update(persistentAgentChats)\n        .set({\n          activeStreamId: input.activeStreamId,\n          updatedAt: new Date(),\n        })\n        .where(\n          and(\n            eq(persistentAgentChats.id, input.chatId),\n            eq(persistentAgentChats.visitorId, input.visitorId)\n          )\n        )\n        .returning({ id: persistentAgentChats.id });\n\n      if (rows.length === 0) {\n        throw new Error(getPersistentAgentChatNotFoundError(input.chatId));\n      }\n    },\n    async cleanupExpiredChats(input?: { now?: Date }) {\n      const { database, persistentAgentChats } =\n        await loadPersistentAgentDatabase();\n      const now = input?.now ?? new Date();\n      const expiresBefore = new Date(\n        now.getTime() -\n          persistentAgentCleanupRetentionDays * 24 * 60 * 60 * 1000\n      );\n      const expiredChats = await database\n        .select({ id: persistentAgentChats.id })\n        .from(persistentAgentChats)\n        .where(lt(persistentAgentChats.updatedAt, expiresBefore));\n\n      if (expiredChats.length === 0) {\n        return {\n          deletedChats: 0,\n          expiresBefore: expiresBefore.toISOString(),\n        };\n      }\n\n      const deletedChats = await database\n        .delete(persistentAgentChats)\n        .where(lt(persistentAgentChats.updatedAt, expiresBefore))\n        .returning({ id: persistentAgentChats.id });\n\n      return {\n        deletedChats: deletedChats.length,\n        expiresBefore: expiresBefore.toISOString(),\n      };\n    },\n  };\n}\n\nexport function createPersistentAgentChatStore(\n  dependencies: PersistentAgentChatStoreDependencies = {}\n) {\n  return (\n    dependencies.persistence ??\n    (getPersistentAgentEnv().DATABASE_URL\n      ? createDatabaseBackedPersistentAgentChatPersistence()\n      : createPortablePersistentAgentChatPersistence())\n  );\n}\n",
      "type": "registry:lib",
      "target": "@lib/persistent-agent/server/chat-store.ts"
    },
    {
      "path": "registry/persistent-agent/lib/persistent-agent/server/cleanup.ts",
      "content": "import {\n  persistentAgentCleanupCronScheduleUtc as cleanupCronScheduleUtc,\n  createPersistentAgentChatStore,\n} from \"./chat-store\";\n\nexport const persistentAgentCleanupCronScheduleUtc = cleanupCronScheduleUtc;\n\nexport function cleanupExpiredPersistentAgentChats() {\n  return createPersistentAgentChatStore().cleanupExpiredChats();\n}\n",
      "type": "registry:lib",
      "target": "@lib/persistent-agent/server/cleanup.ts"
    },
    {
      "path": "registry/persistent-agent/lib/persistent-agent/server/database.ts",
      "content": "import \"server-only\";\n\nimport { Pool } from \"@neondatabase/serverless\";\nimport { drizzle } from \"drizzle-orm/neon-serverless\";\n\nimport {\n  getPersistentAgentDatabaseConfig,\n  getPersistentAgentEnv,\n  type PersistentAgentEnv,\n} from \"./env\";\nimport {\n  persistentAgentChats,\n  persistentAgentMessages,\n  persistentAgentSchema,\n} from \"./schema\";\n\nfunction createPersistentAgentDatabase(connectionString: string) {\n  const client = new Pool({ connectionString });\n  const database = drizzle({\n    client,\n    schema: persistentAgentSchema,\n  });\n\n  return { client, database };\n}\n\ntype PersistentAgentDatabase = ReturnType<\n  typeof createPersistentAgentDatabase\n>[\"database\"];\n\nexport interface PersistentAgentDatabaseModule {\n  database: PersistentAgentDatabase;\n  persistentAgentChats: typeof persistentAgentChats;\n  persistentAgentMessages: typeof persistentAgentMessages;\n}\n\nlet databaseModulePromise: Promise<PersistentAgentDatabaseModule> | null = null;\n\nexport async function loadPersistentAgentDatabase(\n  env: PersistentAgentEnv = getPersistentAgentEnv()\n): Promise<PersistentAgentDatabaseModule> {\n  if (!databaseModulePromise) {\n    const { databaseUrl } = getPersistentAgentDatabaseConfig(env);\n    const { database } = createPersistentAgentDatabase(databaseUrl);\n\n    databaseModulePromise = Promise.resolve({\n      database,\n      persistentAgentChats,\n      persistentAgentMessages,\n    });\n  }\n\n  return databaseModulePromise;\n}\n",
      "type": "registry:lib",
      "target": "@lib/persistent-agent/server/database.ts"
    },
    {
      "path": "registry/persistent-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/persistent-agent/lib/persistent-agent/server/env-source.ts",
      "content": "export function getPersistentAgentAppEnv() {\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/persistent-agent/server/env-source.ts"
    },
    {
      "path": "registry/persistent-agent/lib/persistent-agent/server/env.ts",
      "content": "import { getPersistentAgentAppEnv } 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_PERSISTENT_AGENT_CHAT_MODEL = \"openai/gpt-5-mini\";\nconst missingDatabaseUrlIssue =\n  \"DATABASE_URL is missing. Add it only when you want Postgres-backed persistent-agent chat storage.\";\nconst missingRedisUrlIssue =\n  \"REDIS_URL is missing. Add it only when you want Redis-backed resumable streams.\";\n\nexport type PersistentAgentEnv = AiGatewayEnvRecord;\n\nexport interface PersistentAgentConfig extends AiGatewayContractConfig {\n  databaseUrl: string | undefined;\n  redisUrl: string | undefined;\n}\n\nexport type PersistentAgentSetupState = AiGatewayContractSetupState<AiGatewaySetupConfig>;\n\nexport type PersistentAgentGateway = ReturnType<\n  typeof createAiGatewayFromContract\n>;\n\nconst persistentAgentContract = {\n  defaultChatModel: DEFAULT_PERSISTENT_AGENT_CHAT_MODEL,\n  missingApiKeyError:\n    \"Missing AI_GATEWAY_API_KEY. Add it to .env.local before using the persistent agent.\",\n  missingApiKeyIssue:\n    \"AI_GATEWAY_API_KEY is missing. The demo can render, but persistent-agent chat requests will fail until it is configured.\",\n} as const;\n\nexport function getPersistentAgentEnv(): PersistentAgentEnv {\n  return getPersistentAgentAppEnv();\n}\n\nexport function getPersistentAgentConfig(\n  env: PersistentAgentEnv = getPersistentAgentEnv()\n): PersistentAgentConfig {\n  return {\n    ...readAiGatewayContractConfig(env, persistentAgentContract),\n    databaseUrl: env.DATABASE_URL,\n    redisUrl: env.REDIS_URL,\n  };\n}\n\nexport function getPersistentAgentDatabaseConfig(\n  env: PersistentAgentEnv = getPersistentAgentEnv()\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 getPersistentAgentRedisConfig(\n  env: PersistentAgentEnv = getPersistentAgentEnv()\n) {\n  const redisUrl = env.REDIS_URL;\n\n  if (!redisUrl) {\n    throw new Error(missingRedisUrlIssue);\n  }\n\n  return { redisUrl };\n}\n\nexport function getPersistentAgentSetupState(\n  env: PersistentAgentEnv = getPersistentAgentEnv()\n): PersistentAgentSetupState {\n  return buildAiGatewayContractSetupState(env, {\n    ...persistentAgentContract,\n  });\n}\n\nexport function createPersistentAgentGateway(\n  env: PersistentAgentEnv = getPersistentAgentEnv()\n): PersistentAgentGateway {\n  return createAiGatewayFromContract(env, persistentAgentContract);\n}\n",
      "type": "registry:lib",
      "target": "@lib/persistent-agent/server/env.ts"
    },
    {
      "path": "registry/persistent-agent/lib/persistent-agent/server/portable-store.ts",
      "content": "import type { UIMessage } from \"ai\";\n\nimport {\n  getPersistentAgentChatNotFoundError,\n  persistentAgentCleanupRetentionDays,\n  type PersistentAgentChatPersistence,\n  type PersistentAgentChatRecord,\n  type PersistentAgentChatSession,\n} from \"./chat-store\";\n\ninterface PortablePersistentAgentState {\n  chats: Map<string, PersistentAgentChatRecord>;\n  messagesByChatId: Map<string, UIMessage[]>;\n}\n\nconst globalStore = globalThis as typeof globalThis & {\n  __persistentAgentPortableStore?: PortablePersistentAgentState;\n};\n\nfunction getPortableState(): PortablePersistentAgentState {\n  globalStore.__persistentAgentPortableStore ??= {\n    chats: new Map(),\n    messagesByChatId: new Map(),\n  };\n\n  return globalStore.__persistentAgentPortableStore;\n}\n\nfunction nowIsoString() {\n  return new Date().toISOString();\n}\n\nfunction cloneRecord<T>(value: T): T {\n  return structuredClone(value);\n}\n\nfunction getMessageText(message: UIMessage) {\n  return message.parts\n    .map((part) => (part.type === \"text\" ? part.text : \"\"))\n    .filter((part) => part.length > 0)\n    .join(\" \")\n    .replace(/\\s+/g, \" \")\n    .trim();\n}\n\nfunction buildChatTitle(message: UIMessage) {\n  const text = getMessageText(message);\n\n  if (text.length === 0) {\n    return \"New chat\";\n  }\n\n  return text.slice(0, 72);\n}\n\nfunction normalizeMessageCreatedAt(message: UIMessage, fallback: string) {\n  const metadata = message.metadata as { createdAt?: string } | undefined;\n\n  if (!metadata?.createdAt) {\n    return {\n      ...message,\n      metadata: {\n        ...((message.metadata as Record<string, unknown> | undefined) ?? {}),\n        createdAt: fallback,\n      },\n    };\n  }\n\n  const createdAt = new Date(metadata.createdAt);\n\n  return {\n    ...message,\n    metadata: {\n      ...((message.metadata as Record<string, unknown> | undefined) ?? {}),\n      createdAt: Number.isNaN(createdAt.valueOf())\n        ? fallback\n        : createdAt.toISOString(),\n    },\n  };\n}\n\nfunction upsertMessage(messages: UIMessage[], nextMessage: UIMessage) {\n  const existingIndex = messages.findIndex(\n    (message) => message.id === nextMessage.id\n  );\n\n  if (existingIndex === -1) {\n    return [...messages, nextMessage];\n  }\n\n  return messages.map((message, index) =>\n    index === existingIndex ? nextMessage : message\n  );\n}\n\nfunction assertChatForVisitor(\n  chat: PersistentAgentChatRecord | undefined,\n  chatId: string,\n  visitorId: string\n) {\n  if (!chat || chat.visitorId !== visitorId) {\n    throw new Error(getPersistentAgentChatNotFoundError(chatId));\n  }\n\n  return chat;\n}\n\nexport function createPortablePersistentAgentChatPersistence(): PersistentAgentChatPersistence {\n  const state = getPortableState();\n\n  return {\n    async listChatsForVisitor(visitorId) {\n      return [...state.chats.values()]\n        .filter((chat) => chat.visitorId === visitorId)\n        .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))\n        .map(cloneRecord);\n    },\n    async loadChatSession(chatId, visitorId): Promise<PersistentAgentChatSession | null> {\n      const chat = state.chats.get(chatId);\n\n      if (!chat || chat.visitorId !== visitorId) {\n        return null;\n      }\n\n      return {\n        chat: cloneRecord(chat),\n        messages: cloneRecord(state.messagesByChatId.get(chatId) ?? []),\n      };\n    },\n    async saveIncomingUserMessage(input) {\n      const timestamp = nowIsoString();\n      const existingChat = state.chats.get(input.chatId);\n      const title = buildChatTitle(input.message);\n\n      if (existingChat && existingChat.visitorId !== input.visitorId) {\n        throw new Error(getPersistentAgentChatNotFoundError(input.chatId));\n      }\n\n      const chat: PersistentAgentChatRecord = existingChat\n        ? {\n            ...existingChat,\n            activeStreamId: null,\n            title:\n              existingChat.title === \"New chat\" && title !== \"New chat\"\n                ? title\n                : existingChat.title,\n            updatedAt: timestamp,\n          }\n        : {\n            activeStreamId: null,\n            createdAt: timestamp,\n            id: input.chatId,\n            title,\n            updatedAt: timestamp,\n            visitorId: input.visitorId,\n          };\n\n      state.chats.set(input.chatId, chat);\n      state.messagesByChatId.set(\n        input.chatId,\n        upsertMessage(\n          state.messagesByChatId.get(input.chatId) ?? [],\n          normalizeMessageCreatedAt(input.message, timestamp)\n        )\n      );\n    },\n    async saveFinishedMessages(input) {\n      const timestamp = nowIsoString();\n      const chat = assertChatForVisitor(\n        state.chats.get(input.chatId),\n        input.chatId,\n        input.visitorId\n      );\n      const messages = input.messages.reduce(\n        (currentMessages, message) =>\n          upsertMessage(\n            currentMessages,\n            normalizeMessageCreatedAt(message, timestamp)\n          ),\n        state.messagesByChatId.get(input.chatId) ?? []\n      );\n\n      state.messagesByChatId.set(input.chatId, messages);\n      state.chats.set(input.chatId, {\n        ...chat,\n        activeStreamId: null,\n        updatedAt: timestamp,\n      });\n    },\n    async setActiveStream(input) {\n      const chat = assertChatForVisitor(\n        state.chats.get(input.chatId),\n        input.chatId,\n        input.visitorId\n      );\n\n      state.chats.set(input.chatId, {\n        ...chat,\n        activeStreamId: input.activeStreamId,\n        updatedAt: nowIsoString(),\n      });\n    },\n    async cleanupExpiredChats(input) {\n      const now = input?.now ?? new Date();\n      const expiresBefore = new Date(\n        now.getTime() -\n          persistentAgentCleanupRetentionDays * 24 * 60 * 60 * 1000\n      );\n      let deletedChats = 0;\n\n      for (const chat of state.chats.values()) {\n        if (new Date(chat.updatedAt) >= expiresBefore) {\n          continue;\n        }\n\n        state.chats.delete(chat.id);\n        state.messagesByChatId.delete(chat.id);\n        deletedChats += 1;\n      }\n\n      return {\n        deletedChats,\n        expiresBefore: expiresBefore.toISOString(),\n      };\n    },\n  };\n}\n",
      "type": "registry:lib",
      "target": "@lib/persistent-agent/server/portable-store.ts"
    },
    {
      "path": "registry/persistent-agent/lib/persistent-agent/server/runtime.ts",
      "content": "import {\n  consumeStream,\n  convertToModelMessages,\n  createIdGenerator,\n  generateId,\n  streamText,\n  UI_MESSAGE_STREAM_HEADERS,\n  type UIMessage,\n} from \"ai\";\nimport Redis from \"ioredis\";\nimport { after } from \"next/dist/server/after\";\nimport { createResumableStreamContext } from \"resumable-stream/ioredis\";\n\nimport {\n  createPersistentAgentGateway,\n  getPersistentAgentConfig,\n  getPersistentAgentEnv,\n  getPersistentAgentRedisConfig,\n  getPersistentAgentSetupState,\n  type PersistentAgentEnv,\n} from \"./env\";\nimport {\n  createPersistentAgentChatStore,\n  getPersistentAgentChatNotFoundError,\n  persistentAgentCleanupCronScheduleUtc,\n  persistentAgentCleanupRetentionDays,\n} from \"./chat-store\";\n\nconst invalidRequestBodyError =\n  'Expected a JSON body with a non-empty \"id\" string and a \"message\" object.';\n\nconst persistentAgentSystemPrompt = [\n  \"You are the Persistent Agent demo in an AI SDK examples workspace.\",\n  \"Answer like an experienced product engineer explaining architecture decisions.\",\n  \"Keep replies concrete and concise.\",\n].join(\" \");\n\ninterface PersistentAgentRequestBody {\n  id?: string;\n  message?: UIMessage;\n}\n\nexport interface PersistentAgentRuntimeState {\n  chatModel: string;\n  cleanupRetentionDays: number;\n  cleanupScheduleUtc: string;\n  isChatAvailable: boolean;\n  nodeVersion: string;\n  persistenceLabel: string;\n  resumeLabel: string;\n  resumeRequiresRedis: boolean;\n  setupMessage: string | null;\n  statusLabel: \"Ready\" | \"Setup required\";\n}\n\nexport function getPersistentAgentRuntimeState(\n  env: PersistentAgentEnv = getPersistentAgentEnv()\n): PersistentAgentRuntimeState {\n  const setup = getPersistentAgentSetupState(env);\n\n  return {\n    chatModel: setup.config.chatModel,\n    cleanupRetentionDays: persistentAgentCleanupRetentionDays,\n    cleanupScheduleUtc: persistentAgentCleanupCronScheduleUtc,\n    isChatAvailable: setup.isReady,\n    nodeVersion: setup.nodeVersion,\n    persistenceLabel: env.DATABASE_URL ? \"Postgres\" : \"In-memory\",\n    resumeLabel: env.REDIS_URL ? \"Redis resume\" : \"Local stream\",\n    resumeRequiresRedis: Boolean(env.REDIS_URL),\n    setupMessage: setup.issues.length > 0 ? setup.issues.join(\" \") : null,\n    statusLabel: setup.isReady ? \"Ready\" : \"Setup required\",\n  };\n}\n\nfunction readRequestBody(body: unknown) {\n  const { id, message } = (body ?? {}) as PersistentAgentRequestBody;\n\n  if (\n    typeof id !== \"string\" ||\n    id.trim().length === 0 ||\n    !message ||\n    typeof message !== \"object\"\n  ) {\n    throw new Error(invalidRequestBodyError);\n  }\n\n  return {\n    id: id.trim(),\n    message,\n  };\n}\n\nlet persistentAgentStreamContext:\n  | ReturnType<typeof createResumableStreamContext>\n  | undefined;\nlet persistentAgentStreamPublisher: Redis | undefined;\nlet persistentAgentStreamSubscriber: Redis | undefined;\n\nfunction getStreamContext(env: PersistentAgentEnv) {\n  const { redisUrl } = getPersistentAgentRedisConfig(env);\n\n  if (!persistentAgentStreamPublisher) {\n    persistentAgentStreamPublisher = new Redis(redisUrl, {\n      enableReadyCheck: false,\n    });\n  }\n\n  if (!persistentAgentStreamSubscriber) {\n    persistentAgentStreamSubscriber = new Redis(redisUrl, {\n      enableReadyCheck: false,\n    });\n  }\n\n  persistentAgentStreamContext ??= createResumableStreamContext({\n    publisher: persistentAgentStreamPublisher,\n    subscriber: persistentAgentStreamSubscriber,\n    waitUntil: after,\n  });\n\n  return persistentAgentStreamContext;\n}\n\nexport async function handlePersistentAgentChatRequest(\n  request: Request,\n  viewer: { visitorId: string },\n  env: PersistentAgentEnv = getPersistentAgentEnv()\n) {\n  const runtimeState = getPersistentAgentRuntimeState(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: \"Expected a valid JSON request body.\",\n      },\n      { status: 400 }\n    );\n  }\n\n  let input: { id: string; message: UIMessage };\n\n  try {\n    input = readRequestBody(body);\n  } catch (error) {\n    if (error instanceof Error && error.message === invalidRequestBodyError) {\n      return Response.json(\n        {\n          error: error.message,\n        },\n        { status: 400 }\n      );\n    }\n\n    throw error;\n  }\n\n  const store = createPersistentAgentChatStore();\n  const existingSession = await store.loadChatSession(\n    input.id,\n    viewer.visitorId\n  );\n  const originalMessages = existingSession\n    ? [...existingSession.messages, input.message]\n    : [input.message];\n\n  try {\n    await store.saveIncomingUserMessage({\n      chatId: input.id,\n      message: input.message,\n      visitorId: viewer.visitorId,\n    });\n  } catch (error) {\n    if (\n      error instanceof Error &&\n      error.message === getPersistentAgentChatNotFoundError(input.id)\n    ) {\n      return Response.json(\n        {\n          error: error.message,\n        },\n        { status: 404 }\n      );\n    }\n\n    throw error;\n  }\n\n  const gateway = createPersistentAgentGateway(env);\n  const { chatModel } = getPersistentAgentConfig(env);\n  const result = streamText({\n    model: gateway(chatModel),\n    messages: await convertToModelMessages(originalMessages),\n    system: persistentAgentSystemPrompt,\n  });\n  const shouldUseRedisResume = Boolean(env.REDIS_URL);\n\n  return result.toUIMessageStreamResponse({\n    consumeSseStream: shouldUseRedisResume\n      ? async ({ stream }) => {\n          const streamId = generateId();\n          await getStreamContext(env).createNewResumableStream(\n            streamId,\n            () => stream\n          );\n          await store.setActiveStream({\n            activeStreamId: streamId,\n            chatId: input.id,\n            visitorId: viewer.visitorId,\n          });\n        }\n      : consumeStream,\n    generateMessageId: createIdGenerator({\n      prefix: \"pa-msg\",\n      size: 16,\n    }),\n    onFinish: async ({ isAborted, messages }) => {\n      if (isAborted) {\n        return;\n      }\n\n      await store.saveFinishedMessages({\n        chatId: input.id,\n        messages,\n        visitorId: viewer.visitorId,\n      });\n    },\n    originalMessages,\n  });\n}\n\nexport async function handlePersistentAgentStreamResumeRequest(\n  chatId: string,\n  viewer: { visitorId: string },\n  env: PersistentAgentEnv = getPersistentAgentEnv()\n) {\n  const session = await createPersistentAgentChatStore().loadChatSession(\n    chatId,\n    viewer.visitorId\n  );\n\n  if (!session) {\n    return Response.json(\n      {\n        error: getPersistentAgentChatNotFoundError(chatId),\n      },\n      { status: 404 }\n    );\n  }\n\n  if (!session.chat.activeStreamId) {\n    return new Response(null, { status: 204 });\n  }\n\n  if (!env.REDIS_URL) {\n    return new Response(null, { status: 204 });\n  }\n\n  return new Response(\n    await getStreamContext(env).resumeExistingStream(session.chat.activeStreamId),\n    {\n      headers: UI_MESSAGE_STREAM_HEADERS,\n    }\n  );\n}\n\nexport async function handlePersistentAgentSessionRequest(\n  chatId: string,\n  viewer: { visitorId: string }\n) {\n  const session = await createPersistentAgentChatStore().loadChatSession(\n    chatId,\n    viewer.visitorId\n  );\n\n  if (!session) {\n    return Response.json(\n      {\n        error: getPersistentAgentChatNotFoundError(chatId),\n      },\n      { status: 404 }\n    );\n  }\n\n  return Response.json(session);\n}\n",
      "type": "registry:lib",
      "target": "@lib/persistent-agent/server/runtime.ts"
    },
    {
      "path": "registry/persistent-agent/lib/persistent-agent/server/schema.ts",
      "content": "import {\n  foreignKey,\n  index,\n  jsonb,\n  pgTable,\n  text,\n  timestamp,\n  uniqueIndex,\n  uuid,\n  varchar,\n} from \"drizzle-orm/pg-core\";\n\nexport const persistentAgentChats = pgTable(\n  \"persistent_agent_chats\",\n  {\n    id: uuid(\"id\").defaultRandom().primaryKey(),\n    visitorId: varchar(\"visitor_id\", { length: 191 }).notNull(),\n    title: text(\"title\").notNull(),\n    activeStreamId: varchar(\"active_stream_id\", { length: 191 }),\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    visitorUpdatedIndex: index(\"persistent_agent_chats_visitor_updated_idx\").on(\n      table.visitorId,\n      table.updatedAt\n    ),\n  })\n);\n\nexport const persistentAgentMessages = pgTable(\n  \"persistent_agent_messages\",\n  {\n    id: uuid(\"id\").defaultRandom().primaryKey(),\n    chatId: uuid(\"chat_id\").notNull(),\n    messageId: varchar(\"message_id\", { length: 191 }).notNull(),\n    role: varchar(\"role\", { length: 32 }).notNull(),\n    parts: jsonb(\"parts\").notNull(),\n    attachments: jsonb(\"attachments\").notNull(),\n    createdAt: timestamp(\"created_at\", { withTimezone: true })\n      .defaultNow()\n      .notNull(),\n  },\n  (table) => ({\n    chatCreatedIndex: index(\"persistent_agent_messages_chat_created_idx\").on(\n      table.chatId,\n      table.createdAt\n    ),\n    chatMessageIdIndex: uniqueIndex(\n      \"persistent_agent_messages_chat_message_id_idx\"\n    ).on(table.chatId, table.messageId),\n    chatForeignKey: foreignKey({\n      columns: [table.chatId],\n      foreignColumns: [persistentAgentChats.id],\n      name: \"persistent_agent_messages_chat_fk\",\n    }).onDelete(\"cascade\"),\n  })\n);\n\nexport const persistentAgentSchema = {\n  persistentAgentChats,\n  persistentAgentMessages,\n} as const;\n",
      "type": "registry:lib",
      "target": "@lib/persistent-agent/server/schema.ts"
    },
    {
      "path": "registry/persistent-agent/lib/persistent-agent/server/session.ts",
      "content": "import { cookies } from \"next/headers\";\n\nimport {\n  createPersistentAgentChatStore,\n  type PersistentAgentChatRecord,\n  type PersistentAgentChatSession,\n} from \"./chat-store\";\nimport { persistentAgentVisitorCookieName } from \"./viewer-context\";\n\nexport interface PersistentAgentScreenData {\n  draftChatId: string;\n  initialSession: PersistentAgentChatSession | null;\n  recentChats: PersistentAgentChatRecord[];\n  visitorId: string | null;\n}\n\nexport async function loadPersistentAgentScreenData(input: {\n  chatId?: string | null;\n}) {\n  const draftChatId = crypto.randomUUID();\n  const cookieStore = await cookies();\n  const visitorId =\n    cookieStore.get(persistentAgentVisitorCookieName)?.value ?? null;\n\n  if (!visitorId) {\n    return {\n      draftChatId,\n      initialSession: null,\n      recentChats: [],\n      visitorId: null,\n    } satisfies PersistentAgentScreenData;\n  }\n\n  const store = createPersistentAgentChatStore();\n  const [initialSession, recentChats] = await Promise.all([\n    input.chatId\n      ? store.loadChatSession(input.chatId, visitorId)\n      : Promise.resolve(null),\n    store.listChatsForVisitor(visitorId),\n  ]);\n\n  return {\n    draftChatId,\n    initialSession,\n    recentChats,\n    visitorId,\n  } satisfies PersistentAgentScreenData;\n}\n",
      "type": "registry:lib",
      "target": "@lib/persistent-agent/server/session.ts"
    },
    {
      "path": "registry/persistent-agent/lib/persistent-agent/server/viewer-context.ts",
      "content": "export const persistentAgentVisitorCookieName = \"pa_visitor_id\";\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 readPersistentAgentVisitorIdFromCookieHeader(\n  cookieHeader: string | null\n) {\n  const cookies = parseCookieHeader(cookieHeader);\n  const visitorId = cookies[persistentAgentVisitorCookieName];\n\n  return typeof visitorId === \"string\" && visitorId.trim().length > 0\n    ? visitorId.trim()\n    : null;\n}\n\nexport function getOrCreatePersistentAgentVisitorId(request: Request) {\n  const visitorId = readPersistentAgentVisitorIdFromCookieHeader(\n    request.headers.get(\"cookie\")\n  );\n\n  if (visitorId) {\n    return {\n      shouldSetCookie: false,\n      visitorId,\n    };\n  }\n\n  return {\n    shouldSetCookie: true,\n    visitorId: crypto.randomUUID(),\n  };\n}\n\nexport function buildPersistentAgentVisitorCookie(visitorId: string) {\n  return `${persistentAgentVisitorCookieName}=${encodeURIComponent(visitorId)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${60 * 60 * 24 * 30}`;\n}\n",
      "type": "registry:lib",
      "target": "@lib/persistent-agent/server/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-5-mini"
  },
  "docs": "Install into a Next.js App Router project initialized with shadcn/ui. This item adds the persistent chat pages, route-backed streaming APIs, visitor cookie contract, in-memory chat storage by default, optional Postgres-backed chat storage, optional Redis resumable stream support, and the AI Gateway env vars required for the browser happy path.",
  "type": "registry:block"
}
