> ## Documentation Index
> Fetch the complete documentation index at: https://docs.waniwani.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Sessions

> How the SDK derives a session id from MCP _meta and attaches it to events.

<Note>
  **Platform feature.** Requires `WANIWANI_API_KEY`. Works whether your MCP server is self-hosted or on Managed Hosting. [About the Platform](/sdk/platform/overview).
</Note>

A session represents one conversation between a user and an MCP client. Events are grouped into a session by a `sessionId` that the SDK extracts from the MCP request metadata (`_meta`).

You do not create sessions. You pass `meta: extra._meta` on every `track()` call, and the SDK does the correlation.

## How session correlation works

The SDK reads the first non-empty string value it finds under these keys of `_meta`, in order:

1. `waniwani/sessionId`
2. `openai/sessionId`
3. `openai/session`
4. `sessionId`
5. `conversationId`

The matched value becomes the `correlation.sessionId` field on the outgoing V2 envelope. The SDK also extracts `requestId`, `traceId`, `correlationId`, and `externalUserId` from `_meta` using the same approach. Source is derived from the session key (`openai/sessionId` -> `chatgpt`, `waniwani/sessionId` -> `chatbar`).

## The rule

<Check>
  Always pass `meta: extra._meta` when tracking events from inside a tool handler.
</Check>

```ts theme={null}
server.registerTool(
  "get_quote",
  { /* config */ },
  async ({ amount, currency }, extra) => {
    // ... run your business logic

    await wani.track({
      event: "quote.succeeded",
      properties: { amount, currency },
      meta: extra._meta,
    });

    return { content: [{ type: "text", text: "Quoted" }] };
  },
);
```

If you omit `meta` and do not set `sessionId` explicitly, the envelope ships without a session id and the dashboard cannot group it with the rest of the conversation.

## Where `_meta` lives

| MCP library                 | Location                                                                                |
| --------------------------- | --------------------------------------------------------------------------------------- |
| `@modelcontextprotocol/sdk` | `request.params._meta` (on the raw request), surfaced as `extra._meta` in tool handlers |
| `@vercel/mcp-handler`       | `extra._meta`                                                                           |

Wherever you see `extra._meta` in these docs, substitute the equivalent for your runtime.

## The scoped client inside tools and flows

When a server is wrapped with `withWaniwani(server)`, each tool invocation receives a request-scoped Waniwani client on `extra["waniwani/client"]`. The scoped client is also surfaced as `context.waniwani` inside `createTool` handlers and as `waniwani` inside flow nodes. Its `track()` and `identify()` methods merge the current request's `_meta` automatically, so you do not pass it yourself.

```ts theme={null}
import { createTool } from "@waniwani/sdk/mcp";
import { z } from "zod";

createTool(
  {
    id: "get_quote",
    title: "Get quote",
    description: "Return a price quote",
    inputSchema: { amount: z.number() },
  },
  async ({ amount }, context) => {
    // context.waniwani is the scoped client, meta is already attached
    await context.waniwani?.track({
      event: "quote.succeeded",
      properties: { amount, currency: "USD" },
    });
    return { text: `Quoted ${amount}` };
  },
);
```

Inside a flow node:

```ts theme={null}
.addNode({
  id: "record",
  run: async ({ state, waniwani }) => {
    await waniwani?.track({
      event: "quote.succeeded",
      properties: { amount: state.amount },
    });
    return {};
  },
})
```

See [Flows](/sdk/flows/overview) for the full node context.

## Manual session ids

For background jobs, backfills, or tests where `_meta` is not available, you can set `sessionId` explicitly:

```ts theme={null}
await wani.track({
  event: "purchase.completed",
  properties: { amount: 4999, currency: "USD" },
  sessionId: "session_01HX...",
});
```

<Warning>
  Use manual `sessionId` only when there is no `_meta` to pass. Inside tool handlers, always prefer `meta: extra._meta`.
</Warning>

## Session lifecycle

The Waniwani backend emits `session.started` automatically the first time it sees a new `sessionId`. You do not send it yourself, and `session.started` in the SDK's `EventType` union exists only to type the ingestion envelope.

Sessions do not have a hard close. The backend treats them as closed after an idle window. If the user comes back, they either resume the same session (within the window) or start a fresh one, depending on what the host client sends in `_meta`.
