@maximem/synap-js-sdk 0.5.9 → 0.5.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/anish/maximemai/maximem_synap/synap/sdk/js/dist/index.cjs","../src/transport/endpoints.ts","../src/transport/retry.ts","../src/transport/keepalive.ts","../src/scoping.ts","../src/transport/http.ts","../src/context/ranked.ts","../src/context/fetch.ts","../src/context/normalize.ts","../src/context/flatten.ts","../src/memories/interface.ts","../src/util/validators.ts","../src/agent-model.ts","../src/conversation/interface.ts","../src/user/interface.ts","../src/credits/interface.ts","../src/cache/interface.ts","../src/instance/interface.ts","../src/version.ts","../src/registry.ts","../src/cache/short-term-store.ts","../src/cache/turn-store.ts","../src/cache/history-search.ts","../src/context/overlay.ts","../src/context/local-prompt.ts","../src/context/user-summary.ts","../src/context/anticipated.ts","../src/context/assembled.ts","../src/context/unified.ts","../src/tool/as-tool.ts","../src/client.ts"],"names":["ranked","COLLECTIONS"],"mappings":"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A;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACA,EAAA;AACF;AAEA;AACE,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACA,EAAA;AACF;AAEA;AACE,EAAA;AACF;AAEA;AACE,EAAA;AACA,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AACE,MAAA;AACE,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AAAO,MAAA;AAEX,IAAA;AACA,IAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAMM;AACA;AACA;AAOC;AACL,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AACE,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACF,IAAA;AACA,IAAA;AACE,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACE,QAAA;AACA,QAAA;AAA0E,MAAA;AAE5E,MAAA;AACA,MAAA;AACF,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACE,MAAA;AACA,MAAA;AACF,IAAA;AACA,IAAA;AACA,IAAA;AACE,MAAA;AACF,IAAA;AACE,MAAA;AACA,MAAA;AACF,IAAA;AACA,IAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAEA;AACE,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACE,MAAA;AACA,MAAA;AACA,MAAA;AACF,IAAA;AACA,IAAA;AACE,MAAA;AACA,MAAA;AACA,MAAA;AACF,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAcA;AACE,EAAA;A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","file":"/Users/anish/maximemai/maximem_synap/synap/sdk/js/dist/index.cjs","sourcesContent":[null,"/**\n * AUTOGENERATED FROM synap/sdk/CONTRACT/endpoints.yaml -- DO NOT EDIT.\n * Regenerate with synap/sdk/scripts/sync-contract.sh.\n *\n * The /api/v1 vs /v1 prefix split is intentional and load-bearing. See the\n * contract file: normalising it 404s every affected call in production.\n */\n\nexport interface EndpointSpec {\n readonly method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';\n readonly path: string;\n /**\n * Whether a retry after an ambiguous failure is safe.\n *\n * False here means a retry can double-ingest and therefore double-bill.\n * See defect D / gotcha G-L in the migration plan.\n */\n readonly idempotent: boolean;\n}\n\nexport const ENDPOINTS = {\n whoami: { method: \"GET\", path: \"/api/v1/auth/whoami\", idempotent: true },\n memories_create: { method: \"POST\", path: \"/api/v1/memories/create\", idempotent: false },\n memories_batch: { method: \"POST\", path: \"/api/v1/memories/batch\", idempotent: false },\n memories_upload: { method: \"POST\", path: \"/api/v1/memories/upload\", idempotent: false },\n memories_status: { method: \"GET\", path: \"/api/v1/memories/status/{ingestion_id}\", idempotent: true },\n memories_get: { method: \"GET\", path: \"/api/v1/memories/{memory_id}\", idempotent: true },\n memories_update: { method: \"PATCH\", path: \"/api/v1/memories/{memory_id}\", idempotent: false },\n memories_delete: { method: \"DELETE\", path: \"/api/v1/memories/{memory_id}\", idempotent: true },\n memories_forget: { method: \"POST\", path: \"/api/v1/memories/forget\", idempotent: false },\n context_fetch_user: { method: \"POST\", path: \"/v1/context/user/fetch\", idempotent: true },\n context_fetch_customer: { method: \"POST\", path: \"/v1/context/customer/fetch\", idempotent: true },\n context_fetch_client: { method: \"POST\", path: \"/v1/context/client/fetch\", idempotent: true },\n context_fetch_conversation: { method: \"POST\", path: \"/v1/context/conversation/fetch\", idempotent: true },\n context_for_prompt: { method: \"GET\", path: \"/v1/conversations/{conversation_id}/context-for-prompt\", idempotent: true },\n conversation_turns: { method: \"GET\", path: \"/v1/conversations/{conversation_id}/messages\", idempotent: true },\n conversations_ingest: { method: \"POST\", path: \"/v1/conversations/ingest\", idempotent: false },\n conversations_messages: { method: \"POST\", path: \"/v1/conversations/messages\", idempotent: false },\n conversations_messages_batch: { method: \"POST\", path: \"/v1/conversations/messages/batch\", idempotent: false },\n conversations_compact: { method: \"POST\", path: \"/v1/conversations/compact\", idempotent: false },\n conversations_compacted: { method: \"GET\", path: \"/v1/conversations/{conversation_id}/compacted\", idempotent: true },\n conversations_compaction_status: { method: \"GET\", path: \"/v1/conversations/{conversation_id}/compaction/status\", idempotent: true },\n user_profile: { method: \"GET\", path: \"/v1/users/{user_id}/profile\", idempotent: true },\n credits_balance: { method: \"GET\", path: \"/v1/credits/balance\", idempotent: true },\n credits_ledger: { method: \"GET\", path: \"/v1/credits/ledger\", idempotent: true },\n credits_estimate: { method: \"POST\", path: \"/v1/credits/estimate\", idempotent: true },\n credits_redeem: { method: \"POST\", path: \"/v1/credits/redeem\", idempotent: false },\n telemetry_batch: { method: \"POST\", path: \"/v1/telemetry/batch\", idempotent: false },\n} as const satisfies Record<string, EndpointSpec>;\n\nexport type EndpointName = keyof typeof ENDPOINTS;\n\n/** Substitute `{placeholder}` segments. Values are URL-encoded.\n *\n * Encoding matters: an unencoded id containing `/` or `?` silently\n * retargets the request at a different route.\n */\nexport function resolvePath(\n name: EndpointName,\n params: Readonly<Record<string, string | number>> = {},\n): string {\n return ENDPOINTS[name].path.replace(/\\{(\\w+)\\}/g, (_m, key: string) => {\n const value = params[key];\n if (value === undefined || value === null || value === '') {\n throw new Error(\n `Endpoint ${name} requires path parameter '${key}'.`,\n );\n }\n return encodeURIComponent(String(value));\n });\n}\n","/**\n * Retry policy, mirroring `maximem_synap/resilience/retry.py` plus the\n * idempotency gate added there for defect D.\n *\n * ## Why idempotency is part of the retry decision\n *\n * The obvious retry rule is \"retry transient errors\". That rule double-bills.\n *\n * `POST /api/v1/memories/create` is metered ingestion. If the request reached\n * the server, was processed, and the *response* was lost, a retry ingests the\n * same content twice and bills for it twice. The client cannot tell that case\n * apart from \"the request never arrived\" by looking at the error type alone.\n *\n * So retries are gated on two independent questions:\n *\n * 1. Is the error transient? (worth retrying at all)\n * 2. Is the outcome *knowable*? (safe to retry a non-idempotent call)\n *\n * A connection error means the request provably never reached the application,\n * so the outcome is known: nothing happened. A read timeout means the request\n * may well have been processed. That is the ambiguous case, and for a\n * non-idempotent method it is where we stop.\n *\n * The residual window -- server accepted the request, began processing, then\n * died before responding -- cannot be closed from the client at all. Closing it\n * requires a server-honoured idempotency key on the ingest route. Until that\n * exists, this policy narrows the window rather than eliminating it.\n */\n\nimport { RateLimitError, SynapError, isSynapError } from '../errors.js';\n\n/** HTTP methods that are idempotent by specification. */\nconst IDEMPOTENT_METHODS: ReadonlySet<string> = new Set([\n 'GET',\n 'HEAD',\n 'OPTIONS',\n 'PUT',\n 'DELETE',\n]);\n\nexport interface RetryPolicy {\n maxAttempts: number;\n /** Base delay in seconds. */\n backoffBase: number;\n /** Delay cap in seconds. */\n backoffMax: number;\n /** Randomise the delay to avoid a thundering herd. */\n backoffJitter: boolean;\n}\n\nexport const DEFAULT_RETRY_POLICY: RetryPolicy = {\n maxAttempts: 3,\n backoffBase: 1.0,\n backoffMax: 10.0,\n backoffJitter: true,\n};\n\nexport interface RetryDecisionInput {\n error: unknown;\n /** 1-based. */\n attempt: number;\n policy: RetryPolicy;\n method: string;\n /**\n * True when the request may have been processed by the server despite the\n * error. Read timeouts and mid-flight disconnects are ambiguous; connection\n * errors and definitive HTTP status responses are not.\n */\n outcomeUnknown: boolean;\n /** Overrides idempotency for a route, e.g. a read-only POST. */\n idempotent?: boolean;\n}\n\nexport interface RetryDecision {\n retry: boolean;\n /** Seconds to wait before the next attempt. */\n delaySeconds: number;\n reason: string;\n}\n\nexport function shouldRetry(input: RetryDecisionInput): RetryDecision {\n const { error, attempt, policy, method, outcomeUnknown } = input;\n\n if (attempt >= policy.maxAttempts) {\n return { retry: false, delaySeconds: 0, reason: 'max_attempts_exhausted' };\n }\n\n if (!isSynapError(error) || !(error as SynapError).transient) {\n return { retry: false, delaySeconds: 0, reason: 'not_transient' };\n }\n\n const idempotent = input.idempotent ?? IDEMPOTENT_METHODS.has(method.toUpperCase());\n if (outcomeUnknown && !idempotent) {\n // The request may have been processed. For ingestion that means a retry\n // would store and bill twice. Surfacing the error is the cheaper mistake.\n return {\n retry: false,\n delaySeconds: 0,\n reason: 'non_idempotent_outcome_unknown',\n };\n }\n\n return {\n retry: true,\n delaySeconds: backoffDelay(error, attempt, policy),\n reason: 'transient_retryable',\n };\n}\n\n/** Exponential backoff, honouring Retry-After when the server sent one. */\nexport function backoffDelay(error: unknown, attempt: number, policy: RetryPolicy): number {\n if (error instanceof RateLimitError && error.retryAfterSeconds != null) {\n // The server told us when to come back. Guessing shorter just gets us\n // rejected again and burns quota.\n return Math.min(error.retryAfterSeconds, policy.backoffMax);\n }\n const exponential = policy.backoffBase * 2 ** (attempt - 1);\n const capped = Math.min(exponential, policy.backoffMax);\n if (!policy.backoffJitter) return capped;\n // Full jitter. Equal-jitter still synchronises a fleet after a shared outage.\n return Math.random() * capped;\n}\n\nexport const _IDEMPOTENT_METHODS = IDEMPOTENT_METHODS;\n","/**\n * Connection reuse for the HTTP transport.\n *\n * ## Why this file exists at all\n *\n * Node's global `fetch` keeps an idle socket for roughly 4 seconds. Our real\n * call pattern is one fetch per conversation turn, often minutes apart, so\n * essentially every call would pay a full connection setup. Through the CDN to\n * the origin that was measured at 600-950ms.\n *\n * The Python SDK already solved this: `keepalive_expiry=300s` plus a 240s\n * `/health` heartbeat that keeps the socket warm for as long as the client\n * lives. Porting the SDK without porting this would ship a \"native and faster\"\n * release that is measurably *slower* than the Python-subprocess wrapper it\n * replaces, on every call, from day one. That is gotcha G-A.\n *\n * ## Why it is optional and lazily loaded\n *\n * `undici` is Node-only. Edge runtimes and Workers have their own connection\n * pooling and no `dispatcher` option, so there the correct behavior is to use\n * global `fetch` unchanged. Importing undici eagerly would break those builds\n * at bundle time, so it is behind a lazy `import()` and every failure path\n * degrades to plain `fetch` rather than throwing.\n */\n\n/** Mirrors HTTPTransport.KEEPALIVE_EXPIRY_SECONDS in the Python SDK. */\nexport const KEEPALIVE_EXPIRY_MS = 300_000;\n/** Mirrors HTTPTransport.HEARTBEAT_INTERVAL_SECONDS. */\nexport const HEARTBEAT_INTERVAL_MS = 240_000;\nexport const MAX_CONNECTIONS = 20;\n\nexport interface KeepAliveOptions {\n keepAliveExpiryMs?: number;\n maxConnections?: number;\n}\n\n/** An undici Dispatcher, typed loosely so undici is not a hard type dependency. */\nexport type Dispatcher = object;\n\nlet cached: Dispatcher | null | undefined;\n\n/** True when we are on a runtime that can host an undici dispatcher. */\nexport function supportsDispatcher(): boolean {\n return (\n typeof process !== 'undefined' &&\n process.versions?.node !== undefined &&\n // Edge builds of Next expose a partial `process` shim, but no real Node.\n typeof (globalThis as { EdgeRuntime?: unknown }).EdgeRuntime === 'undefined'\n );\n}\n\n/**\n * Build (once) a dispatcher with Python-equivalent keep-alive.\n *\n * Returns null on any runtime where that is not possible, in which case the\n * caller should simply omit the dispatcher.\n */\nexport async function getKeepAliveDispatcher(\n options: KeepAliveOptions = {},\n): Promise<Dispatcher | null> {\n if (cached !== undefined) return cached;\n if (!supportsDispatcher()) {\n cached = null;\n return cached;\n }\n try {\n // Bundlers must not try to follow this statically on Edge builds.\n const undici = (await import(/* @vite-ignore */ 'undici')) as {\n Agent: new (opts: Record<string, unknown>) => Dispatcher;\n };\n cached = new undici.Agent({\n keepAliveTimeout: options.keepAliveExpiryMs ?? KEEPALIVE_EXPIRY_MS,\n // undici will not honour a keepAliveTimeout above this ceiling.\n keepAliveMaxTimeout: options.keepAliveExpiryMs ?? KEEPALIVE_EXPIRY_MS,\n connections: options.maxConnections ?? MAX_CONNECTIONS,\n });\n } catch {\n // undici is an optionalDependency. Missing or unloadable is not fatal:\n // global fetch still works, just with a short-lived pool.\n cached = null;\n }\n return cached;\n}\n\n/** Test seam. */\nexport function _resetDispatcherCache(): void {\n cached = undefined;\n}\n\n/**\n * Keep an idle connection warm.\n *\n * The timer is `unref`'d. Without that the Node event loop stays alive for as\n * long as the interval exists and any CLI built on this SDK hangs forever\n * instead of exiting. Python has no equivalent symptom, so no parity test would\n * ever catch it: that is gotcha G-J.\n */\nexport class Heartbeat {\n private timer: ReturnType<typeof setInterval> | null = null;\n\n constructor(\n private readonly ping: () => void | Promise<void>,\n private readonly intervalMs: number = HEARTBEAT_INTERVAL_MS,\n ) {}\n\n start(): void {\n if (this.timer !== null) return;\n // 0 disables, matching the Python transport's convention.\n if (this.intervalMs <= 0) return;\n if (typeof setInterval !== 'function') return;\n\n this.timer = setInterval(() => {\n // A failed heartbeat is not an error the caller should ever see. The\n // next real request will surface a genuine connectivity problem.\n void Promise.resolve(this.ping()).catch(() => {});\n }, this.intervalMs);\n\n (this.timer as { unref?: () => void }).unref?.();\n }\n\n stop(): void {\n if (this.timer !== null) {\n clearInterval(this.timer);\n this.timer = null;\n }\n }\n\n get running(): boolean {\n return this.timer !== null;\n }\n}\n","/**\n * The B2C identifier contract, client side.\n *\n * B2C send user_id, and nothing else. customer_id is not accepted.\n * B2B customer_id is required.\n *\n * The server is authoritative and rejects independently. This exists so the\n * failure happens at the call site, with a message that says what to do,\n * instead of as a round trip that returns an empty result.\n *\n * That last part is the whole reason. Sending a customer_id on a B2C instance\n * used to produce no exception anywhere: the write was filed under the\n * customer, the read asked for the user, and both returned success. One client\n * ran 4,634 consecutive empty fetches across seven days without a single error\n * to look at. An SDK that stays quiet about a misuse it can see is not being\n * permissive, it is hiding the bug.\n */\nimport { InvalidInputError } from './errors.js';\n\nexport const B2C_ISOLATION = 'equals_customer';\n\n/**\n * Throw if this call breaks the contract.\n *\n * `isolation` is what `GET /api/v1/auth/whoami` reported. **undefined means the\n * server did not tell us**, which is true of any deployment older than that\n * field, and it must mean \"do nothing\". Guessing B2C there would make this SDK\n * refuse a B2B client's mandatory field against every server not yet upgraded,\n * a far worse failure than the one it prevents.\n */\nexport function checkCustomerId(\n isolation: string | undefined,\n customerId: unknown,\n where: string,\n instanceId?: string,\n): void {\n if (customerId === undefined || customerId === null || customerId === '') return;\n if (isolation !== B2C_ISOLATION) return;\n throw new InvalidInputError(\n `${where}: customer_id is not accepted on this instance` +\n `${instanceId ? ` (${instanceId})` : ''}, which is B2C ` +\n `(user_context_isolation='${B2C_ISOLATION}'). Send user_id only; it is the ` +\n `whole identity, and the server files and reads your data under it. ` +\n `Received customer_id=${JSON.stringify(customerId)}.`,\n );\n}\n","/**\n * Native HTTP transport.\n *\n * Port of `maximem_synap/transport/http_client.py`. Uses global `fetch` so the\n * same code runs on Node, Edge, Workers and Bun, with an optional undici\n * dispatcher on Node to restore Python's connection reuse (see keepalive.ts).\n */\n\nimport {\n AgentUnavailableError,\n AuthenticationError,\n ContextNotFoundError,\n ConflictError,\n TranscriptConflictError,\n InsufficientCreditsError,\n InvalidInputError,\n NetworkTimeoutError,\n RateLimitError,\n ServiceUnavailableError,\n SynapError,\n} from '../errors.js';\nimport { newCorrelationId } from '../util/correlation.js';\nimport { ENDPOINTS, resolvePath, type EndpointName } from './endpoints.js';\nimport {\n DEFAULT_RETRY_POLICY,\n shouldRetry,\n type RetryPolicy,\n} from './retry.js';\nimport {\n getKeepAliveDispatcher,\n Heartbeat,\n HEARTBEAT_INTERVAL_MS,\n type KeepAliveOptions,\n} from './keepalive.js';\nimport { checkCustomerId } from '../scoping.js';\n\nexport const DEFAULT_BASE_URL = 'https://synap-cloud-prod.maximem.ai';\n\nexport interface TimeoutConfig {\n /** Seconds. Mirrors TimeoutConfig in the Python SDK. */\n connect: number;\n read: number;\n write: number;\n /**\n * gRPC stream idle timeout. Not used by the HTTP transport; the stream\n * client reads it. Present so a config object round-trips between the two\n * SDKs, and because Python's TimeoutConfig has it.\n */\n streamIdle: number;\n}\n\nexport const DEFAULT_TIMEOUTS: TimeoutConfig = {\n connect: 5.0, read: 30.0, write: 10.0, streamIdle: 60.0,\n};\n\nexport interface Credentials {\n apiKey: string;\n clientId: string;\n instanceId: string;\n}\n\nexport interface HttpTransportOptions {\n credentials: Credentials;\n baseUrl?: string;\n timeouts?: Partial<TimeoutConfig>;\n /** `null` disables retries entirely, as Python's `retry_policy=None` does. */\n retryPolicy?: Partial<RetryPolicy> | null;\n keepAlive?: KeepAliveOptions & { heartbeatIntervalMs?: number };\n userAgent?: string;\n /** Injected in tests. Defaults to global fetch. */\n fetchImpl?: typeof fetch;\n}\n\nexport interface RequestOptions {\n pathParams?: Record<string, string | number>;\n query?: Record<string, string | number | boolean | undefined>;\n body?: unknown;\n /**\n * Send `body` as-is instead of JSON-encoding it, and drop the JSON\n * Content-Type so the runtime can set `multipart/form-data` with its own\n * boundary. Setting Content-Type by hand for FormData produces a body the\n * server cannot parse, because the boundary will not match.\n */\n rawBody?: boolean;\n correlationId?: string;\n /** Overrides the endpoint's declared idempotency. */\n idempotent?: boolean;\n signal?: AbortSignal;\n}\n\ninterface AttemptOutcome {\n error: unknown;\n /** True when the server may have processed the request despite the failure. */\n outcomeUnknown: boolean;\n}\n\nexport class HttpTransport {\n readonly baseUrl: string;\n // Mutable so `configure()` can adjust them before the first request, and so\n // `initialize()` can write back the identity resolved from whoami.\n timeouts: TimeoutConfig;\n retryPolicy: RetryPolicy;\n\n #credentials: Credentials;\n private readonly userAgent: string;\n private readonly fetchImpl: typeof fetch;\n private readonly keepAliveOptions: KeepAliveOptions;\n private readonly heartbeat: Heartbeat;\n private closed = false;\n\n constructor(options: HttpTransportOptions) {\n this.#credentials = options.credentials;\n // Trailing slash would produce `//v1/...`, which some proxies 404.\n this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, '');\n this.timeouts = { ...DEFAULT_TIMEOUTS, ...options.timeouts };\n this.retryPolicy = options.retryPolicy === null\n ? { ...DEFAULT_RETRY_POLICY, maxAttempts: 1 }\n : { ...DEFAULT_RETRY_POLICY, ...options.retryPolicy };\n this.userAgent = options.userAgent ?? '@maximem/synap-js-sdk';\n this.fetchImpl = options.fetchImpl ?? globalThis.fetch.bind(globalThis);\n this.keepAliveOptions = options.keepAlive ?? {};\n this.heartbeat = new Heartbeat(\n () => this.pingHealth(),\n options.keepAlive?.heartbeatIntervalMs ?? HEARTBEAT_INTERVAL_MS,\n );\n }\n\n /** Start keeping the connection warm. Opt-in: it is pointless in serverless. */\n startHeartbeat(): void {\n if (!this.closed) this.heartbeat.start();\n }\n\n /**\n * Release timers. Safe to call more than once.\n *\n * This is the supported shutdown path, deliberately explicit rather than\n * hooked to `process.on('exit')`: those handlers leak a listener per client,\n * never fire in Lambda, and `process` does not exist in Workers (gotcha G-I).\n */\n /**\n * A COPY of the current credentials.\n *\n * A copy, not the live object: handing out the internal reference is how\n * `client.transport.credentials.apiKey` became readable and mutable from\n * outside in the first place.\n */\n currentCredentials(): Credentials {\n return { ...this.#credentials };\n }\n\n /** Write back identity resolved from `GET /api/v1/auth/whoami`. */\n updateCredentials(patch: Partial<Credentials>): void {\n this.#credentials = { ...this.#credentials, ...patch };\n }\n\n /**\n * The instance's scoping mode, from whoami. Lives here rather than on the\n * client because every interface factory already receives the transport,\n * and it is per-client, so a module-level global would be wrong the moment a\n * process holds two clients.\n *\n * undefined means the server did not report it, which is true of any\n * deployment older than the field, and must mean \"enforce nothing\".\n */\n userContextIsolation: string | undefined = undefined;\n\n /** Refuse a customer_id on a B2C instance, at the call site. */\n checkCustomerId(customerId: unknown, where: string): void {\n checkCustomerId(\n this.userContextIsolation, customerId, where, this.#credentials.instanceId,\n );\n }\n\n setTimeouts(timeouts: Partial<TimeoutConfig>): void {\n this.timeouts = { ...this.timeouts, ...timeouts };\n }\n\n /**\n * `null` DISABLES retries, matching Python.\n *\n * Python's `SDKConfig(retry_policy=None)` leaves `self.retry_policy` unset and\n * `max_attempts` falls to 1 (http_client.py:180). We restored the default\n * policy instead, so a caller following the \"Disabling Retries\" documentation\n * got three attempts and no way to turn them off.\n */\n setRetryPolicy(policy: Partial<RetryPolicy> | null): void {\n this.retryPolicy = policy === null\n ? { ...DEFAULT_RETRY_POLICY, maxAttempts: 1 }\n : { ...this.retryPolicy, ...policy };\n }\n\n close(): void {\n this.closed = true;\n this.heartbeat.stop();\n }\n\n async request<T = unknown>(name: EndpointName, options: RequestOptions = {}): Promise<T> {\n const spec = ENDPOINTS[name];\n const correlationId = options.correlationId ?? newCorrelationId();\n const idempotent = options.idempotent ?? spec.idempotent;\n\n let attempt = 0;\n let last: AttemptOutcome | null = null;\n\n while (attempt < this.retryPolicy.maxAttempts) {\n attempt += 1;\n try {\n return await this.attempt<T>(name, options, correlationId);\n } catch (error) {\n last = { error, outcomeUnknown: isOutcomeUnknown(error) };\n const decision = shouldRetry({\n error,\n attempt,\n policy: this.retryPolicy,\n method: spec.method,\n outcomeUnknown: last.outcomeUnknown,\n idempotent,\n });\n if (!decision.retry) throw error;\n await sleep(decision.delaySeconds * 1000, options.signal);\n }\n }\n throw last?.error ?? new SynapError('Request failed with no recorded error');\n }\n\n private async attempt<T>(\n name: EndpointName,\n options: RequestOptions,\n correlationId: string,\n ): Promise<T> {\n const spec = ENDPOINTS[name];\n const url = this.buildUrl(resolvePath(name, options.pathParams), options.query);\n\n const dispatcher = await getKeepAliveDispatcher(this.keepAliveOptions);\n const signal = withTimeout(this.timeouts.read * 1000, options.signal);\n\n const init: RequestInit = {\n method: spec.method,\n headers: this.buildHeaders(correlationId, { json: options.rawBody !== true }),\n signal: signal.signal,\n };\n if (options.body !== undefined && spec.method !== 'GET') {\n // `BodyInit` is a DOM-lib type and this package does not include that lib,\n // so the field's own type is used instead of naming it.\n init.body = options.rawBody === true\n ? (options.body as NonNullable<RequestInit['body']>)\n : JSON.stringify(options.body);\n }\n // `dispatcher` is a Node/undici extension. Its ambient type is only\n // present when undici's types are installed, so it is set structurally\n // rather than declared -- otherwise this file fails to compile for anyone\n // who has not installed the optional dependency.\n if (dispatcher) (init as Record<string, unknown>)['dispatcher'] = dispatcher;\n\n let response: Response;\n try {\n response = await this.fetchImpl(url, init);\n } catch (error) {\n throw toTransportError(error, correlationId);\n } finally {\n signal.dispose();\n }\n\n if (!response.ok) {\n throw await toHttpError(response, correlationId);\n }\n\n if (response.status === 204) return undefined as T;\n const text = await response.text();\n if (!text) return undefined as T;\n try {\n return JSON.parse(text) as T;\n } catch {\n throw new SynapError(`Malformed JSON in response from ${url}`, { correlationId });\n }\n }\n\n private buildUrl(path: string, query?: RequestOptions['query']): string {\n const url = new URL(this.baseUrl + path);\n for (const [k, v] of Object.entries(query ?? {})) {\n if (v !== undefined) url.searchParams.set(k, String(v));\n }\n return url.toString();\n }\n\n private buildHeaders(\n correlationId: string,\n options: { json?: boolean } = {},\n ): Record<string, string> {\n return {\n Authorization: `Bearer ${this.#credentials.apiKey}`,\n 'X-Client-ID': this.#credentials.clientId,\n 'X-Instance-ID': this.#credentials.instanceId,\n 'X-Correlation-ID': correlationId,\n ...(options.json === false ? {} : { 'Content-Type': 'application/json' }),\n 'User-Agent': this.userAgent,\n };\n }\n\n /** Unauthenticated and unmetered, exactly like the Python heartbeat. */\n private async pingHealth(): Promise<void> {\n const signal = withTimeout(10_000);\n try {\n const dispatcher = await getKeepAliveDispatcher(this.keepAliveOptions);\n const init: RequestInit = { signal: signal.signal };\n if (dispatcher) (init as Record<string, unknown>)['dispatcher'] = dispatcher;\n await this.fetchImpl(`${this.baseUrl}/health`, init);\n } finally {\n signal.dispose();\n }\n }\n}\n\n// ─── Error classification ─────────────────────────────────────────────────────\n\n/**\n * Whether the server may have processed the request despite this error.\n *\n * This is the input to the double-billing gate in retry.ts, so the bias is\n * deliberate: anything we cannot positively rule out counts as ambiguous.\n */\nexport function isOutcomeUnknown(error: unknown): boolean {\n // A definitive HTTP status means the server answered. Nothing ambiguous.\n if (error instanceof RateLimitError) return false;\n if (error instanceof ServiceUnavailableError) return false;\n if (error instanceof AuthenticationError) return false;\n if (error instanceof InvalidInputError) return false;\n if (error instanceof ConflictError) return false;\n if (error instanceof InsufficientCreditsError) return false;\n if (error instanceof ContextNotFoundError) return false;\n\n // Timeouts are the ambiguous case: the request may have been handled and\n // only the response lost.\n if (error instanceof NetworkTimeoutError) return true;\n\n // Connection-level failures mean the request never reached the application.\n if (isConnectionRefusal(error)) return false;\n\n return true;\n}\n\nfunction isConnectionRefusal(error: unknown): boolean {\n const code = (error as { cause?: { code?: string }; code?: string })?.cause?.code\n ?? (error as { code?: string })?.code;\n return (\n code === 'ECONNREFUSED' ||\n code === 'ENOTFOUND' ||\n code === 'EAI_AGAIN' ||\n code === 'ECONNRESET' ||\n code === 'EHOSTUNREACH'\n );\n}\n\nfunction toTransportError(error: unknown, correlationId: string): SynapError {\n if (error instanceof SynapError) return error;\n const name = (error as { name?: string })?.name;\n if (name === 'AbortError' || name === 'TimeoutError') {\n return new NetworkTimeoutError('Request timed out', { correlationId, cause: error });\n }\n return new ServiceUnavailableError(\n `Network request failed: ${(error as Error)?.message ?? String(error)}`,\n { correlationId, cause: error },\n );\n}\n\n/**\n * Values the credit gate sends as the body's `error` field. Only these are read\n * as a reason, so an unrelated 429 carrying some other `error` string is not\n * mistaken for a credit stop. Mirrors `_CREDIT_ERRORS` in `http_client.py`.\n */\nconst CREDIT_ERRORS = new Set(['insufficient_credits', 'trial_limit_reached']);\n\n/**\n * The credit gate's structured body, however the server nested it.\n *\n * The gate raises an HTTPException, so FastAPI wraps its payload in `detail`;\n * the burn-service path returns the same fields at the top level. Reading only\n * the top level (what this transport did) meant the gate's balance, reason and\n * URLs never reached the caller.\n */\nfunction creditPayload(\n parsed: Record<string, unknown>,\n detailObject: Record<string, unknown> | null,\n): Record<string, unknown> {\n return detailObject ?? parsed;\n}\n\n/** Why the request was refused, or null if this was not a credit stop. */\nfunction creditReason(payload: Record<string, unknown>): string | null {\n const reason = stringOrNull(payload['reason']);\n if (reason !== null) return reason;\n const error = stringOrNull(payload['error']);\n return error !== null && CREDIT_ERRORS.has(error) ? error : null;\n}\n\nasync function toHttpError(response: Response, correlationId: string): Promise<SynapError> {\n const body = await response.text().catch(() => '');\n let parsed: Record<string, unknown> = {};\n try {\n parsed = JSON.parse(body) as Record<string, unknown>;\n } catch {\n /* body is not JSON; the raw text is still used in the message */\n }\n // `detail` is a string on most routes but a structured object on the ones\n // that need a machine-readable code: {\"detail\": {\"code\": ..., \"message\": ...}}.\n // Casting it straight to string rendered those as \"[object Object]\".\n const rawDetail = parsed['detail'] ?? parsed['message'] ?? body ?? '';\n const detailObject =\n typeof rawDetail === 'object' && rawDetail !== null\n ? (rawDetail as Record<string, unknown>)\n : null;\n const detail =\n detailObject !== null\n ? String(detailObject['message'] ?? JSON.stringify(detailObject))\n : String(rawDetail);\n const message = `HTTP ${response.status}: ${detail || response.statusText}`;\n const opts = { correlationId };\n\n switch (response.status) {\n case 400:\n case 422:\n return new InvalidInputError(message, opts);\n case 401:\n case 403:\n return new AuthenticationError(message, opts);\n case 404:\n return new ContextNotFoundError(message, opts);\n case 402: {\n // The server sends `minimum_required_credits` (see\n // synap/cloud/application/credits/enforcement.py). We read\n // `required_credits` for a while, a key that never exists, so\n // `requiredCredits` was always null. `recovery_url` and `redeem_url`\n // were dropped entirely, leaving no way to surface a top-up link.\n const credit = creditPayload(parsed, detailObject);\n return new InsufficientCreditsError(message, {\n ...opts,\n balanceCredits: numberOrNull(credit['balance_credits']),\n requiredCredits:\n numberOrNull(credit['minimum_required_credits']) ??\n numberOrNull(credit['required_credits']),\n recoveryUrl: stringOrNull(credit['recovery_url']),\n redeemUrl: stringOrNull(credit['redeem_url']),\n reason: creditReason(credit),\n manageUrl: stringOrNull(credit['manage_url']),\n });\n }\n case 409:\n // Conflict is PERMANENT and never retried. Python discriminates on the\n // structured body so a transcript-immutability conflict becomes\n // TranscriptConflictError and anything else (e.g. compact()'s \"already\n // in progress\") stays the generic ConflictError. Without this, a\n // customer's `catch (e) { if (e instanceof TranscriptConflictError) }`\n // never fires in JS while the identical Python code works.\n if (detailObject !== null && detailObject['code'] === 'transcript_conflict') {\n return new TranscriptConflictError(message, opts);\n }\n return new ConflictError(message, opts);\n case 429: {\n // Either an ordinary rate limit or a credit stop that time or a plan\n // change clears: the spent Trial cap (`trial_limit_reached`) and a paid\n // plan whose subscription lapsed (`subscription_inactive`). The credit\n // fields are absent on the plain rate limit.\n const header = response.headers.get('retry-after');\n const retryAfter = header !== null && header.trim() !== '' ? Number(header) : null;\n const credit = creditPayload(parsed, detailObject);\n return new RateLimitError(message, {\n ...opts,\n retryAfterSeconds: retryAfter !== null && Number.isFinite(retryAfter) ? retryAfter : null,\n reason: creditReason(credit),\n upgradeUrl: stringOrNull(credit['upgrade_url']),\n manageUrl: stringOrNull(credit['manage_url']),\n });\n }\n case 503:\n return new ServiceUnavailableError(message, opts);\n case 504:\n return new AgentUnavailableError(message, opts);\n default:\n if (response.status >= 500) return new ServiceUnavailableError(message, opts);\n return new SynapError(message, opts);\n }\n}\n\nfunction numberOrNull(v: unknown): number | null {\n return typeof v === 'number' && Number.isFinite(v) ? v : null;\n}\n\nfunction stringOrNull(v: unknown): string | null {\n return typeof v === 'string' && v !== '' ? v : null;\n}\n\n// ─── Utilities ────────────────────────────────────────────────────────────────\n\ninterface DisposableSignal {\n signal: AbortSignal;\n dispose(): void;\n}\n\n/**\n * Compose a timeout with a caller-supplied signal.\n *\n * Hand-rolled rather than `AbortSignal.timeout` + `AbortSignal.any` because\n * `any` is Node 20.3+ and this must also run on Workers and older Bun. The\n * timer is cleared in `dispose` so a fast response does not hold the event\n * loop open for the full timeout.\n */\nfunction withTimeout(ms: number, external?: AbortSignal): DisposableSignal {\n const controller = new AbortController();\n const onExternalAbort = () => controller.abort(external?.reason);\n const timer = setTimeout(() => {\n controller.abort(new NetworkTimeoutError(`Timed out after ${ms}ms`));\n }, ms);\n (timer as { unref?: () => void }).unref?.();\n\n if (external) {\n if (external.aborted) onExternalAbort();\n else external.addEventListener('abort', onExternalAbort, { once: true });\n }\n\n return {\n signal: controller.signal,\n dispose() {\n clearTimeout(timer);\n external?.removeEventListener('abort', onExternalAbort);\n },\n };\n}\n\n/**\n * Retry backoff.\n *\n * Deliberately NOT `unref`'d, unlike the heartbeat timer in keepalive.ts. That\n * distinction is the whole point: a heartbeat is background work and must not\n * hold the process open, but a backoff is FOREGROUND work that a caller is\n * awaiting. With it unref'd, a process whose only pending work was this sleep\n * exited during the backoff and the awaited request never settled -- Node\n * reports it as \"Detected unsettled top-level await\". Invisible in a server\n * with other traffic; fatal in a CLI or an idle serverless handler.\n */\nfunction sleep(ms: number, signal?: AbortSignal): Promise<void> {\n if (ms <= 0) return Promise.resolve();\n return new Promise((resolve, reject) => {\n const timer = setTimeout(() => {\n signal?.removeEventListener('abort', onAbort);\n resolve();\n }, ms);\n function onAbort() {\n clearTimeout(timer);\n reject(signal?.reason ?? new Error('Aborted'));\n }\n signal?.addEventListener('abort', onAbort, { once: true });\n });\n}\n","/**\n * `response_shape: \"ranked\"`: one list in the order the server ranked it.\n *\n * Ports the ranked half of Python's `models/context.py` (`rank_items`,\n * `_wire_rank_score`, `_wire_category` and the ranked branch of\n * `ContextResponse.from_cloud_response`). The cases are pinned in\n * `CONTRACT/conformance/ranked_shape.json`, captured from Python.\n *\n * Opt-in. A by-type fetch, the default, is returned exactly as the server sent\n * it, so nothing here touches a caller who does not ask for the ranked shape.\n */\n\nimport type { Json, RawContext, RawContextItem } from './types.js';\n\n/** The two shapes a context fetch can ask for. */\nexport const RESPONSE_SHAPES = ['by_type', 'ranked'] as const;\nexport type ResponseShape = (typeof RESPONSE_SHAPES)[number];\n\n/** The memory types, in the order the by-type response lists them. */\nexport const MEMORY_TYPES = [\n 'facts',\n 'preferences',\n 'episodes',\n 'emotions',\n 'temporal_events',\n] as const;\n\n/**\n * The type of a chunk in a ranked response: a quote from a source document,\n * not an extracted memory. A by-type response files it under `facts`.\n */\nexport const EXCERPT_CATEGORY = 'excerpts';\n\n/**\n * An item's rank score: `metadata.rank_score` on the HTTP response, a\n * top-level `rank_score` on a prefetched (gRPC) item. `null` when neither is a\n * number, which is how a server that predates ranking reads.\n */\nexport function wireRankScore(item: RawContextItem): number | null {\n const md = item.metadata;\n let raw: unknown = md !== null && typeof md === 'object' ? (md as Json)['rank_score'] : undefined;\n if (raw === undefined || raw === null) raw = item['rank_score'];\n return typeof raw === 'number' && Number.isFinite(raw) ? raw : null;\n}\n\n/**\n * An item's type. A chunk is an excerpt whatever list it came in: a server or\n * a prefetched bundle that predates ranking files it under facts.\n */\nexport function wireCategory(item: RawContextItem, fallback = ''): string {\n if (item.source === 'chunks') return EXCERPT_CATEGORY;\n const category = item['category'];\n if (typeof category === 'string' && category !== '') return category;\n const contextType = item['context_type'];\n if (typeof contextType === 'string' && contextType !== '') return contextType;\n return fallback;\n}\n\n/** Highest score first; items without one after, in the order they came. */\nfunction byRankScore(a: RawContextItem, b: RawContextItem): number {\n const sa = wireRankScore(a);\n const sb = wireRankScore(b);\n if (sa === null && sb === null) return 0;\n if (sa === null) return 1;\n if (sb === null) return -1;\n return sb - sa;\n}\n\n/**\n * Merge per-type item lists into one list in rank order, numbered from 1.\n *\n * For a server that predates `response_shape` (it returned the five lists) and\n * for a prefetched cache hit. Each returned item is a copy carrying `rank`,\n * `category` and `id` (a prefetched item names it `item_id`), so the input is\n * not changed. `Array.prototype.sort` is stable, like Python's.\n */\nexport function rankItems(byType: RawContext): RawContextItem[] {\n const flat: RawContextItem[] = [];\n // The five lists only: a raw context also carries other arrays\n // (`conversations` in summary mode), and none of them are memories.\n for (const type of MEMORY_TYPES) {\n const list = (byType as Json)[type];\n if (!Array.isArray(list)) continue;\n for (const it of list as RawContextItem[]) {\n if (it === null || typeof it !== 'object') continue;\n flat.push({\n ...it,\n category: wireCategory(it, type),\n id: (it.id || it.item_id || '') as string,\n });\n }\n }\n flat.sort(byRankScore);\n return flat.map((it, n) => ({ ...it, rank: n + 1 }));\n}\n\n/**\n * The raw context in the shape the caller asked for.\n *\n * \"ranked\": `items` in rank order (the server's own list, or one rebuilt by\n * rank score when the server or the cache predates the parameter), the five\n * typed lists as views of those items, and `excerpts` for the chunks. A\n * response that already carries `items` is read the same way even when the\n * caller did not ask, as Python does. Anything else is returned untouched.\n */\nexport function applyResponseShape(raw: RawContext, shape: string = 'by_type'): RawContext {\n const wire = (raw as Json)['items'];\n if (shape !== 'ranked' && !Array.isArray(wire)) return raw;\n const source: RawContextItem[] = Array.isArray(wire)\n ? (wire as RawContextItem[])\n : rankItems(raw);\n const items = source.map((it) => ({\n ...it,\n id: (it.id || it.item_id || '') as string,\n category: wireCategory(it),\n }));\n const out: RawContext = { ...raw, items };\n for (const type of MEMORY_TYPES) {\n out[type] = items.filter((it) => it['category'] === type);\n }\n (out as Json)['excerpts'] = items.filter((it) => it['category'] === EXCERPT_CATEGORY);\n return out;\n}\n\n/** How each type is named on a line of the ranked prompt list. */\nconst RANKED_LABELS: Record<string, string> = {\n facts: 'fact',\n preferences: 'preference',\n episodes: 'episode',\n emotions: 'emotion',\n temporal_events: 'temporal event',\n [EXCERPT_CATEGORY]: 'excerpt',\n};\n\n/** The label printed in front of a ranked item: `fact`, `temporal event`, ... */\nexport function rankedLabel(category: string): string {\n return RANKED_LABELS[category] ?? (category !== '' ? category : 'item');\n}\n\n/** Shared by the ranked merge: the same ordering Python's `merge` sorts with. */\nexport function sortByRankScore(items: RawContextItem[]): RawContextItem[] {\n return [...items].sort(byRankScore);\n}\n","/**\n * Scope-aware context fetching.\n *\n * Scopes are client > customer > user. `conversation_id` is NOT a tier: it\n * groups turns within a scope. Passing it narrows an existing scope rather\n * than selecting a different one.\n */\n\nimport type { EndpointName } from '../transport/endpoints.js';\nimport type { HttpTransport } from '../transport/http.js';\nimport { InvalidInputError } from '../errors.js';\nimport { RESPONSE_SHAPES, applyResponseShape } from './ranked.js';\nimport type { FetchOptions, Json, RawContext } from './types.js';\n\nexport type Scope = 'user' | 'customer' | 'client' | 'conversation';\n\nconst ENDPOINT_BY_SCOPE: Record<Scope, EndpointName> = {\n user: 'context_fetch_user',\n customer: 'context_fetch_customer',\n client: 'context_fetch_client',\n conversation: 'context_fetch_conversation',\n};\n\n/** Reads either spelling of an option. Callers depend on both. */\nfunction opt<T>(options: FetchOptions, snake: keyof FetchOptions, camel: keyof FetchOptions): T | undefined {\n const a = options[snake];\n if (a !== undefined) return a as T;\n const b = options[camel];\n return b === undefined ? undefined : (b as T);\n}\n\nexport interface BuiltRequest {\n endpoint: EndpointName;\n body: Json;\n}\n\nexport function buildFetchRequest(scope: Scope, options: FetchOptions): BuiltRequest {\n const userId = opt<string>(options, 'user_id', 'userId');\n const customerId = opt<string>(options, 'customer_id', 'customerId');\n const conversationId = opt<string>(options, 'conversation_id', 'conversationId');\n const searchQuery = opt<string[]>(options, 'search_query', 'searchQuery');\n const maxResults = opt<number>(options, 'max_results', 'maxResults') ?? 10;\n const precisionLevel = opt<string>(options, 'precision_level', 'precisionLevel') ?? 'high';\n const queryDate = opt<string>(options, 'query_date', 'queryDate');\n const perQuery = opt<number>(options, 'max_results_per_query', 'maxResultsPerQuery');\n const responseShape = responseShapeOf(options);\n const mode = options.mode ?? 'fast';\n\n if (searchQuery !== undefined && !Array.isArray(searchQuery)) {\n throw new InvalidInputError('search_query must be an array when provided');\n }\n if (options.types !== undefined && !Array.isArray(options.types)) {\n throw new InvalidInputError('types must be an array when provided');\n }\n\n // Field-for-field identical to what the Python controllers post, because the\n // server is tuned against that payload and Python is the reference\n // implementation. In particular:\n // - conversation_id is ALWAYS present, null when unset, not omitted\n // - mode always defaults to \"fast\"\n // - precision_level is sent ONLY when it differs from \"high\"\n // - query_date is sent ONLY when the caller supplied one\n // - max_results_per_query is sent ONLY when the caller supplied one\n // - response_shape is sent ONLY when \"ranked\"\n // - include_conversation_context is sent ONLY when false\n // Sending an extra key, or omitting one Python always sends, is exactly the\n // kind of difference that shows up as \"works in Python, empty in JS\".\n const body: Json = {\n conversation_id: conversationId ?? null,\n search_query: searchQuery ?? [],\n max_results: maxResults,\n types: options.types ?? ['all'],\n mode,\n };\n if (precisionLevel !== 'high') body['precision_level'] = precisionLevel;\n // Omitted at its default, for the same reason precision_level is.\n if (queryDate) body['query_date'] = queryDate;\n // Omitted when unset, and `0` counts as unset, as in Python.\n if (perQuery) body['max_results_per_query'] = perQuery;\n if (responseShape !== 'by_type') body['response_shape'] = responseShape;\n if (options.include_conversation_context === false) {\n body['include_conversation_context'] = false;\n }\n\n switch (scope) {\n case 'user': {\n if (!userId) throw new InvalidInputError('user_id is required');\n body['user_id'] = userId;\n\n const scopePath = opt<Record<string, string>>(options, 'scope_path', 'scopePath');\n // Only sent when supplied, so a caller who does not use custom levels\n // sends exactly the body they sent before.\n if (scopePath !== undefined) body['scope'] = scopePath;\n\n const contextMode = opt<string>(options, 'context_mode', 'contextMode') ?? 'in-conversation';\n if (!VALID_CONTEXT_MODES.includes(contextMode)) {\n throw new InvalidInputError(\n `Invalid context_mode '${contextMode}'. Must be one of: ${VALID_CONTEXT_MODES.join(', ')}`,\n );\n }\n const lastN = opt<number>(options, 'last_n_conversations', 'lastNConversations') ?? 1;\n if (!Number.isInteger(lastN)) {\n throw new InvalidInputError('last_n_conversations must be an integer');\n }\n if (lastN < 0 || lastN > 20) {\n throw new InvalidInputError('last_n_conversations must be between 0 and 20');\n }\n\n // Conditional, exactly as Python does it: the summary-mode keys are sent\n // ONLY in summary mode, so an in-conversation fetch is byte-identical to\n // one that never knew about them.\n if (contextMode === 'conversation-summary') {\n body['context_mode'] = contextMode;\n body['include_profile'] = opt<boolean>(options, 'include_profile', 'includeProfile') ?? true;\n body['last_n_conversations'] = lastN;\n }\n // Python sends this only when not None. It is passed through the same\n // way rather than defaulted to '', so the server sees the same request\n // from either SDK.\n if (customerId !== undefined) body['customer_id'] = customerId;\n break;\n }\n break;\n case 'customer':\n if (!customerId) throw new InvalidInputError('customer_id is required');\n body['customer_id'] = customerId;\n break;\n case 'conversation':\n if (!conversationId) throw new InvalidInputError('conversation_id is required');\n if (userId !== undefined) body['user_id'] = userId;\n if (customerId !== undefined) body['customer_id'] = customerId;\n break;\n case 'client':\n break;\n }\n\n return { endpoint: ENDPOINT_BY_SCOPE[scope], body };\n}\n\n/**\n * Hooks that let the client serve a fetch locally and report the outcome.\n *\n * Passed in rather than imported so this module stays free of the cache and\n * the stream: it is the one place every scope's fetch goes through, and it\n * should not grow a dependency on either.\n */\nexport interface AnticipatedServe {\n response: RawContext;\n servedItemIds: string[];\n bundleId: string;\n /**\n * A semantic match below the sure bar. The server is asked too, and its\n * answer is used if it arrives within `raceDeadlineMs`. Python:\n * `_race_borderline`.\n */\n borderline?: boolean;\n raceDeadlineMs?: number;\n}\n\nexport interface FetchHooks {\n /** Returns a response to short-circuit the network, or null to continue. */\n beforeFetch?: (\n scope: Scope,\n options: FetchOptions,\n ) => AnticipatedServe | null | Promise<AnticipatedServe | null>;\n /** Called after a cache hit is served, for the learning-loop event. */\n onServedFromCache?: (\n scope: Scope,\n options: FetchOptions,\n served: { servedItemIds: string[]; bundleId: string },\n ) => void;\n /** Called after EVERY fetch resolves, cache or network, for the audit event. */\n onAssembled?: (\n scope: Scope,\n options: FetchOptions,\n response: RawContext,\n startedAt: number,\n ) => void;\n}\n\nconst VALID_CONTEXT_MODES = ['in-conversation', 'conversation-summary'];\n\n/** The shape asked for, validated as Python validates it, before any request. */\nexport function responseShapeOf(options: FetchOptions): string {\n const shape = opt<string>(options, 'response_shape', 'responseShape') ?? 'by_type';\n if (!(RESPONSE_SHAPES as readonly string[]).includes(shape)) {\n throw new InvalidInputError(\n `Invalid response_shape '${shape}'. Must be one of: ${RESPONSE_SHAPES.join(', ')}`,\n );\n }\n return shape;\n}\n\n/** Fetches and returns the RAW snake_case context, as the namespaced surface does. */\nexport async function fetchContext(\n transport: HttpTransport,\n scope: Scope,\n options: FetchOptions = {},\n hooks: FetchHooks = {},\n): Promise<RawContext> {\n // Anticipation cache FIRST, as Python does on all four scope fetches. Without\n // this the stream's bundles are never read and every fetch is a billed round\n // trip.\n const startedAt = Date.now();\n\n // Before the anticipation cache, not after: a cached hit would otherwise let\n // an illegal shape through silently, which is the exact failure mode this\n // contract exists to remove.\n transport.checkCustomerId(\n opt<string>(options, 'customer_id', 'customerId'),\n `${scope}.context.fetch`,\n );\n\n // Validated before the cache, like customer_id above, so a bad value fails\n // the same way whether or not a prefetched bundle would have answered.\n const responseShape = responseShapeOf(options);\n\n const network = async (): Promise<RawContext> => {\n const { endpoint, body } = buildFetchRequest(scope, options);\n const result = await transport.request<Json>(endpoint, { body });\n // The wrapper returned `result.context || {}`; preserve that exactly.\n return applyResponseShape((result?.['context'] as RawContext) ?? {}, responseShape);\n };\n\n // Awaited: matching by meaning embeds the query with the local model first.\n const anticipated = (await hooks.beforeFetch?.(scope, options)) ?? null;\n if (anticipated !== null && anticipated.borderline) {\n // A borderline match races the server: its answer if in time, the cached\n // one if not. A late server call finishes unobserved.\n const pending = network();\n pending.catch(() => undefined);\n let timer: ReturnType<typeof setTimeout> | undefined;\n const late = new Promise<null>((resolve) => {\n timer = setTimeout(() => resolve(null), anticipated.raceDeadlineMs ?? 250);\n });\n const won = await Promise.race([pending.then((r) => r, () => null), late]);\n if (timer !== undefined) clearTimeout(timer);\n if (won !== null) {\n hooks.onAssembled?.(scope, options, won, startedAt);\n return won;\n }\n }\n if (anticipated !== null) {\n hooks.onServedFromCache?.(scope, options, anticipated);\n hooks.onAssembled?.(scope, options, anticipated.response, startedAt);\n // A hit is items picked from several prefetched bundles, so a ranked hit\n // is ordered by each item's own rank_score.\n return applyResponseShape(anticipated.response, responseShape);\n }\n\n const response = await network();\n hooks.onAssembled?.(scope, options, response, startedAt);\n return response;\n}\n","/**\n * camelCase normalisers for the legacy top-level methods.\n *\n * A faithful port of the normalisers in the removed `src/synap-client.js`,\n * including their quirks:\n *\n * - `score` and `source` on a memory item are passed through WITHOUT a\n * default, so `undefined` survives. Adding `?? 0` here would change the\n * shape consumers already branch on.\n * - `strength` falls back to `confidence`, `summary` falls back to `content`,\n * `intensity` falls back to `confidence`. Those fallbacks exist because the\n * server does not always send the specific field.\n * - `pickDefined` treats `null` as a real value and only skips `undefined`,\n * so an explicit server `null` is preserved rather than replaced.\n *\n * See gotcha G-B: this shape is one of two live shapes and must not converge\n * with the raw one before 2.0.\n */\n\nimport type {\n ConversationContext, ContextMetadata, Emotion, Episode, Fact, Json,\n NormalisedContext, Preference, RawContext, RawContextItem,\n RawConversationContext, RetrievalHealth, TemporalEvent, TemporalFields,\n} from './types.js';\n\n/** Returns the first argument that is not `undefined`. `null` counts as a value. */\nexport function pickDefined<T>(...values: (T | undefined)[]): T | undefined {\n for (const v of values) if (v !== undefined) return v;\n return undefined;\n}\n\nfunction pick<T>(...values: (T | undefined)[]): T {\n return pickDefined(...values) as T;\n}\n\nconst obj = (v: unknown): Json => (v as Json) ?? {};\n\n/** Always an array, so a caller can group by it without a null check. */\nfunction matchedQueries(item: RawContextItem = {}): string[] {\n const raw = item['matchedQueries'] ?? item.matched_queries;\n return Array.isArray(raw) ? (raw as string[]) : [];\n}\n\nfunction normalizeTemporalFields(item: RawContextItem = {}): TemporalFields {\n return {\n eventDate: pick(item['eventDate'] as string, item.event_date, null),\n eventEnd: pick(item['eventEnd'] as string, item.event_end as string, null),\n eventPrecision: pick(item['eventPrecision'] as string, item.event_precision as string, null),\n saidAt: pick(item['saidAt'] as string, item.said_at, null),\n validUntil: pick(item['validUntil'] as string, item.valid_until, null),\n temporalCategory: pick(item['temporalCategory'] as string, item.temporal_category, null),\n temporalConfidence: pick(item['temporalConfidence'] as number, item.temporal_confidence, 0),\n };\n}\n\nexport function normalizeFact(item: RawContextItem = {}): Fact {\n return {\n id: item.id || '',\n content: item.content || '',\n confidence: pick(item.confidence, 0),\n source: item.source || '',\n extractedAt: pick(item['extractedAt'] as string, item.extracted_at, null),\n metadata: obj(item.metadata),\n ...normalizeTemporalFields(item),\n matchedQueries: matchedQueries(item),\n };\n}\n\nexport function normalizePreference(item: RawContextItem = {}): Preference {\n return {\n id: item.id || '',\n category: (item['category'] as string) || '',\n content: item.content || '',\n strength: pick(item['strength'] as number, item.confidence, 0),\n source: item.source || '',\n extractedAt: pick(item['extractedAt'] as string, item.extracted_at, null),\n metadata: obj(item.metadata),\n ...normalizeTemporalFields(item),\n matchedQueries: matchedQueries(item),\n };\n}\n\nexport function normalizeEpisode(item: RawContextItem = {}): Episode {\n return {\n id: item.id || '',\n summary: pick(item['summary'] as string, item.content, ''),\n occurredAt: pick(item['occurredAt'] as string, item['occurred_at'] as string, null),\n significance: pick(item['significance'] as number, item.confidence, 0),\n participants: (item['participants'] as unknown[]) || [],\n metadata: obj(item.metadata),\n ...normalizeTemporalFields(item),\n matchedQueries: matchedQueries(item),\n };\n}\n\nexport function normalizeEmotion(item: RawContextItem = {}): Emotion {\n return {\n id: item.id || '',\n emotionType: pick(item['emotionType'] as string, item['emotion_type'] as string, ''),\n intensity: pick(item['intensity'] as number, item.confidence, 0),\n detectedAt: pick(item['detectedAt'] as string, item['detected_at'] as string, null),\n context: (item['context'] as string) || '',\n metadata: obj(item.metadata),\n ...normalizeTemporalFields(item),\n matchedQueries: matchedQueries(item),\n };\n}\n\nexport function normalizeTemporalEvent(item: RawContextItem = {}): TemporalEvent {\n return {\n id: item.id || '',\n content: item.content || '',\n eventDate: pick(item['eventDate'] as string, item.event_date, null),\n eventEnd: pick(item['eventEnd'] as string, item.event_end as string, null),\n eventPrecision: pick(item['eventPrecision'] as string, item.event_precision as string, null),\n saidAt: pick(item['saidAt'] as string, item.said_at, null),\n validUntil: pick(item['validUntil'] as string, item.valid_until, null),\n temporalCategory: pick(item['temporalCategory'] as string, item.temporal_category, ''),\n temporalConfidence: pick(item['temporalConfidence'] as number, item.temporal_confidence, 0),\n confidence: pick(item.confidence, 0),\n source: item.source || '',\n extractedAt: pick(item['extractedAt'] as string, item.extracted_at, null),\n metadata: obj(item.metadata),\n matchedQueries: matchedQueries(item),\n };\n}\n\nexport function normalizeContextMetadata(metadata: Json = {}): ContextMetadata {\n return {\n correlationId: pick(metadata['correlationId'] as string, metadata['correlation_id'] as string, ''),\n ttlSeconds: pick(metadata['ttlSeconds'] as number, metadata['ttl_seconds'] as number, 0),\n source: (metadata['source'] as string) || 'unknown',\n retrievedAt: pick(metadata['retrievedAt'] as string, metadata['retrieved_at'] as string, null),\n compactionApplied: pick(\n metadata['compactionApplied'] as boolean,\n metadata['compaction_applied'] as boolean,\n null,\n ),\n };\n}\n\nexport function normalizeConversationContext(\n value: RawConversationContext | null | undefined,\n): ConversationContext | null {\n if (!value) return null;\n return {\n summary: value.summary || null,\n currentState: pick(value['currentState'] as Json, value.current_state, {}),\n keyExtractions: pick(value['keyExtractions'] as Json, value.key_extractions, {}),\n recentTurns: pick(value['recentTurns'] as unknown[], value.recent_turns, []),\n compactionId: pick(value['compactionId'] as string, value.compaction_id, null),\n compactedAt: pick(value['compactedAt'] as string, value.compacted_at, null),\n conversationId: pick(value['conversationId'] as string, value.conversation_id, null),\n };\n}\n\nfunction normalizeRetrievalHealth(raw: unknown): RetrievalHealth | null {\n if (!raw || typeof raw !== 'object') return null;\n const r = raw as Json;\n const warnings = r['warnings'];\n return {\n degraded: r['degraded'] === undefined ? true : Boolean(r['degraded']),\n level: (r['level'] as string) ?? null,\n code: (r['code'] as string) ?? null,\n warnings: Array.isArray(warnings) ? warnings.map((w) => String(w)) : [],\n };\n}\n\n/** Accepts either a bare context or a `{ context: ... }` envelope, as the wrapper did. */\nexport function normalizeContextResponse(result: Json = {}): NormalisedContext {\n const context = (result['context'] as RawContext) ?? (result as RawContext);\n return {\n facts: (context.facts || []).map((i) => normalizeFact(i)),\n preferences: (context.preferences || []).map((i) => normalizePreference(i)),\n episodes: (context.episodes || []).map((i) => normalizeEpisode(i)),\n emotions: (context.emotions || []).map((i) => normalizeEmotion(i)),\n temporalEvents: (\n pick(context['temporalEvents'] as RawContextItem[], context.temporal_events, []) || []\n ).map((i) => normalizeTemporalEvent(i)),\n conversationContext: normalizeConversationContext(\n pick(\n context['conversationContext'] as RawConversationContext,\n context.conversation_context,\n null,\n ),\n ),\n metadata: normalizeContextMetadata(obj(context.metadata)),\n // `retrieval` is a SIBLING of `context` in the body, so it is read off the\n // envelope first and only then off a bare context. Reading it from\n // `context` alone is how the Python SDK dropped it.\n retrieval: normalizeRetrievalHealth(\n pick((result as Json)['retrieval'] as Json, (context as Json)['retrieval'] as Json, null),\n ),\n rawResponse: pick(context['rawResponse'] as Json, context['raw_response'] as Json, {}),\n // Only for a ranked fetch, so a by-type result has exactly the keys it had.\n ...(Array.isArray(context.items)\n ? { items: context.items, excerpts: Array.isArray(context.excerpts) ? context.excerpts : [] }\n : {}),\n };\n}\n","/**\n * Flatten typed context collections into one memory list.\n *\n * A faithful port of `flatten_context_items` from the 0.3.x bridge, which is\n * what `searchMemory` and `getMemories` returned. The per-type asymmetries are\n * deliberate and are reproduced exactly:\n *\n * - `memory` comes from a different field per type: fact/preference/temporal\n * event use `content`, episode uses `summary`, emotion uses `context`.\n * - `score` likewise: fact -> confidence, preference -> strength,\n * episode -> significance, emotion -> intensity, temporal event ->\n * temporal_confidence.\n * - **episode and emotion carry no `source` key at all.** The bridge simply\n * did not set one. Adding it would be an improvement and a shape change, so\n * it stays absent.\n * - Order is facts, preferences, episodes, emotions, temporal events.\n */\n\nimport type { Json, RawContext, RawContextItem } from './types.js';\n\nexport interface FlatMemory {\n id: string;\n memory: string;\n score: number;\n /** Absent for episodes and emotions, matching the 0.3.x bridge. */\n source?: string;\n metadata: Json;\n context_type: 'fact' | 'preference' | 'episode' | 'emotion' | 'temporal_event';\n event_date: string | null;\n /** See `TemporalFields.saidAt` in ./types. */\n said_at: string | null;\n valid_until: string | null;\n temporal_category: string | null;\n temporal_confidence: number;\n}\n\nconst s = (v: unknown): string => (typeof v === 'string' ? v : v == null ? '' : String(v));\nconst n = (v: unknown, d = 0): number => (typeof v === 'number' ? v : d);\nconst o = (v: unknown): Json => (v as Json) ?? {};\n/** The bridge stringified datetimes and mapped falsy to null. */\nconst dt = (v: unknown): string | null => (v ? String(v) : null);\n\nfunction temporal(item: RawContextItem) {\n return {\n event_date: dt(item['event_date']),\n said_at: dt(item['said_at']),\n valid_until: dt(item['valid_until']),\n temporal_category: (item['temporal_category'] as string | null) ?? null,\n temporal_confidence: n(item['temporal_confidence'], 0),\n };\n}\n\nexport function flattenContextItems(context: RawContext): FlatMemory[] {\n const items: FlatMemory[] = [];\n\n for (const f of context.facts ?? []) {\n items.push({\n id: s(f['id']), memory: s(f['content']), score: n(f['confidence']),\n source: s(f['source']), metadata: o(f['metadata']),\n context_type: 'fact', ...temporal(f),\n });\n }\n for (const p of context.preferences ?? []) {\n items.push({\n id: s(p['id']), memory: s(p['content']), score: n(p['strength']),\n source: s(p['source']), metadata: o(p['metadata']),\n context_type: 'preference', ...temporal(p),\n });\n }\n for (const e of context.episodes ?? []) {\n // No `source`: the bridge did not set one for episodes.\n items.push({\n id: s(e['id']), memory: s(e['summary']), score: n(e['significance']),\n metadata: o(e['metadata']), context_type: 'episode', ...temporal(e),\n });\n }\n for (const m of context.emotions ?? []) {\n // No `source` here either, and `memory` comes from `context`, not `content`.\n items.push({\n id: s(m['id']), memory: s(m['context']), score: n(m['intensity']),\n metadata: o(m['metadata']), context_type: 'emotion', ...temporal(m),\n });\n }\n for (const t of context.temporal_events ?? []) {\n items.push({\n id: s(t['id']), memory: s(t['content']), score: n(t['temporal_confidence']),\n source: s(t['source']), metadata: o(t['metadata']),\n context_type: 'temporal_event', ...temporal(t),\n });\n }\n\n return items;\n}\n","/**\n * Memories namespace. Mirrors `maximem_synap.memories.interface.MemoriesInterface`.\n *\n * ## The request body is built the way Python builds it, on purpose\n *\n * Python constructs a pydantic `CreateMemoryRequest` and posts\n * `model_dump(mode=\"json\")`, which emits **every** field, nulls included, with\n * defaults applied. It is tempting to omit unset keys instead and send a\n * tidier body. Do not: the server sees a different request, and the difference\n * that matters most is `document_type`.\n *\n * `document_type` defaults to `\"ai-chat-conversation\"`. Leaving it off is not\n * neutral, because the extraction path is type-dependent and the\n * `\"document\"` type extracts dramatically less from the same text. A body that\n * merely looks cleaner is how you get \"the JS SDK stores fewer memories than\n * the Python one from identical input\", which is slow and miserable to\n * diagnose.\n */\n\nimport { InvalidInputError } from '../errors.js';\nimport type { HttpTransport } from '../transport/http.js';\nimport type { Json } from '../context/types.js';\n\n/**\n * What `new Blob([...])` accepts here.\n *\n * Derived from the Blob constructor rather than written out, because `BlobPart`\n * lives in the DOM lib, which this package deliberately does not include, and\n * Node's own Blob accepts a slightly narrower set than the DOM's. Deriving it\n * means this keeps compiling if either definition shifts.\n */\ntype BlobSource = NonNullable<ConstructorParameters<typeof Blob>[0]>[number];\n\n/** Mirrors the Python DocumentType enum. */\nexport const DOCUMENT_TYPES = [\n 'ai-chat-conversation', 'document', 'email', 'pdf', 'image', 'audio', 'meeting-transcript',\n] as const;\nexport type DocumentType = (typeof DOCUMENT_TYPES)[number];\n\n/** Mirrors the Python IngestMode enum. */\nexport const INGEST_MODES = ['fast', 'long-range'] as const;\nexport type IngestMode = (typeof INGEST_MODES)[number];\n\nexport const DEFAULT_DOCUMENT_TYPE: DocumentType = 'ai-chat-conversation';\nexport const DEFAULT_INGEST_MODE: IngestMode = 'long-range';\n\nexport interface CreateMemoryOptions {\n document: string;\n user_id?: string | null;\n userId?: string | null;\n customer_id?: string | null;\n customerId?: string | null;\n document_type?: DocumentType | string;\n documentType?: DocumentType | string;\n document_id?: string | null;\n documentId?: string | null;\n /** ISO-8601 string or Date. Serialised the way Python serialises a datetime. */\n document_created_at?: string | Date | null;\n documentCreatedAt?: string | Date | null;\n mode?: IngestMode | string;\n metadata?: Json;\n /**\n * The level this memory belongs to, named in full, one entry per level down\n * to it: `{ customer: \"acme\", team: \"payments\", user: \"dana\" }`.\n *\n * Only needed by an account whose scope ladder has a level between its\n * customer and its user levels. `user_id` and `customer_id` cannot say which\n * one a write belongs to, so on such a ladder the server refuses the write\n * rather than guessing: a wrong guess at or above the customer level crosses\n * a tenant boundary.\n *\n * Omit it and nothing changes. Sending one to an account without nested\n * scoping is an error rather than ignored, because a caller who asks to be\n * narrowed and is ignored gets widened instead.\n */\n scope?: Record<string, string> | null;\n}\n\n/**\n * Mirrors Python's `CreateMemoryResponse`.\n *\n * The first four fields are required there (Pydantic would raise if the server\n * omitted one), so they are required here too. Declaring `ingestion_id` optional\n * made the commonest two-line pattern in the docs fail to compile under strict\n * TypeScript: `wait_for_completion(result.ingestion_id)` cannot take\n * `string | undefined`.\n */\nexport interface CreateMemoryResult {\n ingestion_id: string;\n document_id: string;\n status: string;\n /** ISO-8601. Python types this as a datetime. */\n queued_at: string;\n error_message?: string | null;\n memories_created?: number;\n [key: string]: unknown;\n}\n\nfunction either<T>(o: Record<string, unknown>, snake: string, camel: string): T | undefined {\n const a = o[snake];\n if (a !== undefined) return a as T;\n const b = o[camel];\n return b === undefined ? undefined : (b as T);\n}\n\nfunction isoOrNull(v: string | Date | null | undefined): string | null {\n if (v === undefined || v === null) return null;\n return v instanceof Date ? v.toISOString() : v;\n}\n\n/** Byte-for-byte equivalent of Python's `CreateMemoryRequest.model_dump(mode=\"json\")`. */\nexport function buildCreateBody(options: CreateMemoryOptions): Json {\n if (!options.document) throw new InvalidInputError('document is required');\n\n const o = options as unknown as Record<string, unknown>;\n const documentType = either<string>(o, 'document_type', 'documentType') ?? DEFAULT_DOCUMENT_TYPE;\n const mode = (options.mode as string) ?? DEFAULT_INGEST_MODE;\n\n // Validated client-side because Python's enum coercion raises before any\n // request is sent. A silent pass-through would turn a typo into a server\n // error, or worse, a silently different extraction path.\n if (!(DOCUMENT_TYPES as readonly string[]).includes(documentType)) {\n throw new InvalidInputError(\n `Invalid document_type '${documentType}'. Expected one of: ${DOCUMENT_TYPES.join(', ')}`,\n );\n }\n if (!(INGEST_MODES as readonly string[]).includes(mode)) {\n throw new InvalidInputError(\n `Invalid mode '${mode}'. Expected one of: ${INGEST_MODES.join(', ')}`,\n );\n }\n\n // Key order matches the pydantic model's field order. Not semantically\n // required, but it makes a captured-body diff against Python readable.\n return {\n document: options.document,\n document_type: documentType,\n document_id: either<string>(o, 'document_id', 'documentId') ?? null,\n document_created_at: isoOrNull(\n either<string | Date>(o, 'document_created_at', 'documentCreatedAt'),\n ),\n user_id: either<string>(o, 'user_id', 'userId') ?? null,\n customer_id: either<string>(o, 'customer_id', 'customerId') ?? null,\n // null, never `{}`. An empty path is not absence: the server treats one as\n // an error, because a caller asking to be narrowed and being ignored gets\n // widened instead.\n scope: options.scope ?? null,\n mode,\n metadata: options.metadata ?? {},\n };\n}\n\n/**\n * Result of `memories.batch_create`, mirroring Python's `BatchCreateResponse`.\n * It was `Json`, so `results` came back as `unknown` and could not be iterated.\n */\nexport interface BatchCreateResult {\n batch_id: string;\n total: number;\n succeeded: number;\n failed: number;\n results: CreateMemoryResult[];\n [key: string]: unknown;\n}\n\nexport interface BatchCreateOptions {\n documents: CreateMemoryOptions[];\n fail_fast?: boolean;\n failFast?: boolean;\n}\n\n/** Mirrors the Python ForgetMode enum. */\nexport const FORGET_MODES = ['preview', 'direct'] as const;\nexport type ForgetMode = (typeof FORGET_MODES)[number];\n\nexport const FORGET_DESCRIPTION_MIN = 3;\nexport const FORGET_DESCRIPTION_MAX = 500;\n\nexport interface ForgetOptions {\n /** What to forget, in the user's own words (3 to 500 characters). */\n description: string;\n user_id?: string | null;\n userId?: string | null;\n customer_id?: string | null;\n customerId?: string | null;\n /**\n * The level to search, named in full, one entry per level down to it:\n * `{ customer: \"acme\", team: \"payments\", user: \"dana\" }`. Only needed when\n * your scope ladder has a level between customer and user.\n */\n scope?: Record<string, string> | null;\n /** `\"preview\"` (default) or `\"direct\"`. See `MemoriesInterface.forget`. */\n mode?: ForgetMode | string;\n}\n\n/** A memory the description matched. Mirrors Python's `ForgetCandidate`. */\nexport interface ForgetCandidate {\n memory_id: string;\n content: string;\n memory_type?: string | null;\n /** `\"memory\"`, or where a copy lives: chunk, sentence, entity, triple, profile, candidate. */\n store?: string;\n}\n\nexport type ForgetStatus = 'nothing_matches' | 'preview' | 'deleted' | 'partial';\n\n/** Result of `memories.forget`. Mirrors Python's `ForgetResponse`. */\nexport interface ForgetResult {\n status: ForgetStatus;\n request_id: string | null;\n candidates: ForgetCandidate[];\n deleted_ids: string[];\n failed_ids: string[];\n still_present_in: string[];\n may_be_incomplete: boolean;\n}\n\n/** The body Python builds for `memories.forget`, key order included. */\nexport function buildForgetBody(options: ForgetOptions): Json {\n const description = options.description;\n if (\n typeof description !== 'string' ||\n description.length < FORGET_DESCRIPTION_MIN ||\n description.length > FORGET_DESCRIPTION_MAX\n ) {\n throw new InvalidInputError(\n `description must be between ${FORGET_DESCRIPTION_MIN} and ${FORGET_DESCRIPTION_MAX} characters`,\n );\n }\n const mode = (options.mode as string | undefined) ?? 'preview';\n if (!(FORGET_MODES as readonly string[]).includes(mode)) {\n throw new InvalidInputError(\n `Invalid mode '${mode}'. Expected one of: ${FORGET_MODES.join(', ')}`,\n );\n }\n const o = options as unknown as Record<string, unknown>;\n return {\n description,\n user_id: either<string>(o, 'user_id', 'userId') ?? null,\n customer_id: either<string>(o, 'customer_id', 'customerId') ?? null,\n scope: options.scope ?? null,\n mode,\n };\n}\n\nexport interface MemoriesInterface {\n create(options: CreateMemoryOptions): Promise<CreateMemoryResult>;\n batch_create(options: BatchCreateOptions): Promise<BatchCreateResult>;\n create_from_file(options: CreateFromFileOptions): Promise<CreateMemoryResult>;\n get(memoryId: string): Promise<Json>;\n update(options: UpdateMemoryOptions): Promise<Json>;\n delete(memoryId: string): Promise<Json>;\n /**\n * Forget memories by describing them in the user's own words. Use it when\n * you do not hold memory ids; to delete one memory by id, use `delete()`.\n *\n * The call must be scoped to exactly one user: pass `user_id`, plus\n * `customer_id` on a B2B instance. Otherwise the server answers 400, raised\n * as `InvalidInputError`.\n *\n * Modes:\n * - `\"preview\"` (default): files a pending request that a person approves in\n * the dashboard under Deletion requests. Deletes nothing. `candidates`\n * lists what matched.\n * - `\"direct\"`: finds and soft-deletes in one call. Not idempotent as a\n * request: a repeat is harmless and finds nothing new, but it is a new\n * request.\n *\n * `status` is `\"nothing_matches\"`, `\"preview\"`, `\"deleted\"` or `\"partial\"`.\n * `\"partial\"` means the memories are hidden but a store named in\n * `still_present_in` may still hold the text, so do not tell an end user it\n * was removed unless `status === \"deleted\"`. `failed_ids` lists memories that\n * could not be deleted; `may_be_incomplete` is true when the match may not\n * have covered everything.\n *\n * Errors: `InvalidInputError` for 400/422 or a bad `description`/`mode`\n * caught before sending; `ServiceUnavailableError` for 503 (the matcher was\n * unavailable and nothing was deleted, so trying again is safe; the\n * transport applies its normal retry policy first); `RateLimitError` for 429.\n */\n forget(options: ForgetOptions): Promise<ForgetResult>;\n status(ingestionId: string): Promise<Json>;\n wait_for_completion(ingestionId: string, options?: WaitOptions): Promise<Json>;\n}\n\n/**\n * Options for `create_from_file`.\n *\n * Exactly one of `file_path`, `file`, or `text` must be supplied, matching\n * Python. `file_path` reads from disk and so is Node-only; `file` takes a\n * Blob/File/Uint8Array and works anywhere, which is the portable route for\n * Edge and Workers.\n */\nexport interface CreateFromFileOptions {\n user_id?: string;\n userId?: string;\n customer_id?: string;\n customerId?: string;\n /** \"b2b\" or \"b2c\". Defaults to \"b2c\", as in Python. */\n relationship_type?: string;\n relationshipType?: string;\n /** Node-only: read this path from disk. Uses a lazy `node:fs` import. */\n file_path?: string;\n filePath?: string;\n /** Portable alternative to `file_path`. Pass `filename` alongside a raw buffer. */\n file?: Blob | Uint8Array | ArrayBuffer;\n filename?: string;\n /** Ingest raw text instead of a file. */\n text?: string;\n document_type?: string;\n documentType?: string;\n mode?: IngestMode | string;\n metadata?: Json;\n /**\n * The level this memory belongs to, named in full, one entry per level down\n * to it: `{ customer: \"acme\", team: \"payments\", user: \"dana\" }`.\n *\n * Only needed by an account whose scope ladder has a level between its\n * customer and its user levels. `user_id` and `customer_id` cannot say which\n * one a write belongs to, so on such a ladder the server refuses the write\n * rather than guessing: a wrong guess at or above the customer level crosses\n * a tenant boundary.\n *\n * Omit it and nothing changes. Sending one to an account without nested\n * scoping is an error rather than ignored, because a caller who asks to be\n * narrowed and is ignored gets widened instead.\n */\n scope?: Record<string, string> | null;\n}\n\nexport interface UpdateMemoryOptions {\n memory_id?: string;\n memoryId?: string;\n document: string;\n merge_strategy?: string;\n mergeStrategy?: string;\n document_type?: string;\n documentType?: string;\n metadata?: Json;\n}\n\nexport interface WaitOptions {\n /** Seconds. Matches Python's default. */\n timeout_seconds?: number;\n timeoutSeconds?: number;\n poll_interval_seconds?: number;\n pollIntervalSeconds?: number;\n signal?: AbortSignal;\n}\n\nconst TERMINAL_STATUSES = new Set(['completed', 'failed', 'error', 'cancelled']);\n\nexport function createMemoriesInterface(\n transport: HttpTransport,\n // Called after a forget that deleted something, so the caller can stop serving\n // that user's cached context. Mirrors Python's `_evict_forgotten`.\n onForgotten?: (ids: { userId: string | null; customerId: string | null }) => void,\n): MemoriesInterface {\n return {\n async create(options) {\n const o = options as unknown as Record<string, unknown>;\n transport.checkCustomerId(o['customer_id'] ?? o['customerId'], 'memories.create');\n return transport.request<CreateMemoryResult>('memories_create', { body: buildCreateBody(options) });\n },\n\n async batch_create(options) {\n const docs = options.documents;\n if (!Array.isArray(docs) || docs.length === 0) {\n throw new InvalidInputError('documents must be a non-empty array');\n }\n const failFast = options.fail_fast ?? options.failFast ?? false;\n return transport.request<BatchCreateResult>('memories_batch', {\n body: { documents: docs.map(buildCreateBody), fail_fast: failFast },\n });\n },\n\n async create_from_file(options) {\n const userId = options.user_id ?? options.userId;\n const customerId = options.customer_id ?? options.customerId;\n if (!userId) throw new InvalidInputError('user_id is required');\n // Not required: rejected on B2C, so requiring it made the documented\n // default combination impossible to call.\n transport.checkCustomerId(customerId, 'memories.create_from_file');\n\n const filePath = options.file_path ?? options.filePath;\n const sources = [filePath, options.file, options.text].filter((v) => v !== undefined);\n if (sources.length === 0) {\n // Python raises ValueError here. There is no JS equivalent, and every\n // other validation in this SDK raises InvalidInputError, so the\n // taxonomy wins over a literal transcription of the exception type.\n throw new InvalidInputError('One of file_path, file, or text must be provided.');\n }\n if (sources.length > 1) {\n throw new InvalidInputError(\n 'Provide exactly one of file_path, file, or text, not several.',\n );\n }\n\n const form = new FormData();\n form.set('user_id', userId);\n form.set('customer_id', customerId);\n form.set('relationship_type', options.relationship_type ?? options.relationshipType ?? 'b2c');\n form.set('mode', String(options.mode ?? DEFAULT_INGEST_MODE));\n\n // Python appends these only when truthy, so an empty document_type or an\n // empty metadata object is absent from the form rather than present-empty.\n const documentType = options.document_type ?? options.documentType;\n if (documentType) form.set('document_type', String(documentType));\n if (options.metadata !== undefined && Object.keys(options.metadata).length > 0) {\n form.set('metadata', JSON.stringify(options.metadata));\n }\n\n if (filePath !== undefined) {\n // Lazy, so importing the SDK on Edge or in a Worker stays safe: a\n // top-level `node:fs` import fails the BUILD there, not the call.\n const { readFile } = await import('node:fs/promises');\n const { basename } = await import('node:path');\n const bytes = await readFile(filePath);\n form.set('file', new Blob([bytes as unknown as BlobSource]), basename(filePath));\n } else if (options.file !== undefined) {\n const blob = options.file instanceof Blob\n ? options.file\n : new Blob([options.file as unknown as BlobSource]);\n // Python defaults the name to \"upload\" when `filename` is omitted.\n form.set('file', blob, options.filename ?? 'upload');\n } else {\n form.set('text', String(options.text));\n }\n\n return transport.request<CreateMemoryResult>('memories_upload', {\n body: form,\n rawBody: true,\n });\n },\n\n async get(memoryId) {\n if (!memoryId) throw new InvalidInputError('memory_id is required');\n return transport.request<Json>('memories_get', { pathParams: { memory_id: memoryId } });\n },\n\n async update(options) {\n const memoryId = options.memory_id ?? options.memoryId;\n if (!memoryId) throw new InvalidInputError('memory_id is required');\n if (!options.document) throw new InvalidInputError('document is required');\n const body: Json = {\n document: options.document,\n merge_strategy: options.merge_strategy ?? options.mergeStrategy ?? 'smart-merge',\n };\n const dt = options.document_type ?? options.documentType;\n if (dt !== undefined) body['document_type'] = dt;\n if (options.metadata !== undefined) body['metadata'] = options.metadata;\n // PUT, so idempotent by verb: safe to retry even on an ambiguous failure.\n return transport.request<Json>('memories_update', {\n pathParams: { memory_id: memoryId },\n body,\n });\n },\n\n async delete(memoryId) {\n // The 0.3.x wrapper let you delete \"all of this user's memories\" from a\n // process-local id list, which deleted nothing in serverless while\n // reporting success. Deliberately not ported.\n if (!memoryId) {\n throw new InvalidInputError(\n 'memory_id is required. Deleting by user is no longer supported: the ' +\n 'previous implementation tracked ids in process memory, so it silently ' +\n 'deleted nothing in any serverless or multi-process deployment while ' +\n 'reporting success.',\n );\n }\n return transport.request<Json>('memories_delete', { pathParams: { memory_id: memoryId } });\n },\n\n async forget(options) {\n const o = options as unknown as Record<string, unknown>;\n transport.checkCustomerId(o['customer_id'] ?? o['customerId'], 'memories.forget');\n const body = buildForgetBody(options) as Record<string, unknown>;\n const result = await transport.request<ForgetResult>('memories_forget', { body: body as Json });\n // A preview deletes nothing, so it evicts nothing. Never let a cache\n // problem turn a successful delete into an error.\n // A forget can remove only copies (a profile entry, a chunk) and so return no\n // memory ids; those are served from the same caches.\n const removed = !!result?.deleted_ids?.length\n || result?.status === 'deleted' || result?.status === 'partial';\n if (removed && onForgotten) {\n try {\n onForgotten({\n userId: (body['user_id'] as string | null) ?? null,\n customerId: (body['customer_id'] as string | null) ?? null,\n });\n } catch { /* eviction is best effort */ }\n }\n return result;\n },\n\n async status(ingestionId) {\n if (!ingestionId) throw new InvalidInputError('ingestion_id is required');\n return transport.request<Json>('memories_status', {\n pathParams: { ingestion_id: ingestionId },\n });\n },\n\n async wait_for_completion(ingestionId, options = {}) {\n if (!ingestionId) throw new InvalidInputError('ingestion_id is required');\n const timeoutMs =\n (options.timeout_seconds ?? options.timeoutSeconds ?? 300) * 1000;\n const pollMs =\n (options.poll_interval_seconds ?? options.pollIntervalSeconds ?? 2) * 1000;\n\n const deadline = Date.now() + timeoutMs;\n let last: Json = {};\n for (;;) {\n last = await this.status(ingestionId);\n const status = String(last['status'] ?? '').toLowerCase();\n if (TERMINAL_STATUSES.has(status)) return last;\n if (Date.now() >= deadline) {\n // Python returns the last status rather than raising, so a caller\n // that only checks `.status` behaves identically in both SDKs.\n return last;\n }\n await sleep(Math.min(pollMs, Math.max(0, deadline - Date.now())), options.signal);\n }\n },\n };\n}\n\nfunction sleep(ms: number, signal?: AbortSignal): Promise<void> {\n if (ms <= 0) return Promise.resolve();\n return new Promise((resolve, reject) => {\n const timer = setTimeout(() => {\n signal?.removeEventListener('abort', onAbort);\n resolve();\n }, ms);\n // NOT unref'd. This is the poll interval inside wait_for_completion, which\n // a caller is awaiting: foreground work. Unref'ing it let a process whose\n // only pending work was this sleep exit mid-poll, so an awaited\n // wait_for_completion never settled. Node calls it \"Detected unsettled\n // top-level await\". The unref'd timers in this SDK are the two heartbeats\n // and the request watchdog, all of which are genuinely background.\n function onAbort() {\n clearTimeout(timer);\n reject(signal?.reason ?? new Error('Aborted'));\n }\n signal?.addEventListener('abort', onAbort, { once: true });\n });\n}\n","/**\n * Client-side identifier validation, mirroring Python's\n * ``maximem_synap/utils/validators.py``.\n *\n * These raise the *specific* ``InvalidInputError`` subtypes so callers can\n * catch them directly (``catch (e) { if (e instanceof InvalidConversationIdError) }``)\n * instead of the generic parent. Validation happens before any network call,\n * so a malformed id fails locally rather than becoming a billed round trip.\n *\n * Both errors were exported from day one and thrown at zero sites until this\n * module existed, which meant a typo'd conversation id reached the server and\n * came back as a generic 4xx.\n */\n\nimport { InvalidConversationIdError, InvalidInstanceIdError } from '../errors.js';\n\n// Instance ids are issued as `inst_` followed by 16 hex characters.\nconst INSTANCE_ID_RE = /^inst_[0-9a-fA-F]{16}$/;\n\n// Accepts any RFC 4122 shape. Deliberately laxer than a version-specific\n// check: Python defers to `uuid.UUID(...)`, which likewise accepts any\n// variant, and rejecting a valid-but-unexpected version here would diverge.\nconst UUID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;\n\n/**\n * Throw `InvalidConversationIdError` if a non-empty `conversationId` is not a\n * UUID string.\n *\n * Empty / null / undefined values pass through untouched: callers that require\n * a conversation id surface that separately, so this only rejects a clearly\n * malformed value such as `\"conv_123\"`.\n */\nexport function validateConversationId(conversationId: unknown): void {\n if (conversationId === undefined || conversationId === null || conversationId === '') return;\n if (typeof conversationId !== 'string' || !UUID_RE.test(conversationId)) {\n // Message text matches Python's `f\"Invalid conversation ID: {id}\"` byte for byte.\n throw new InvalidConversationIdError(`Invalid conversation ID: ${String(conversationId)}`);\n }\n}\n\n/**\n * Throw `InvalidInstanceIdError` if a non-empty `instanceId` is not in the\n * `inst_<hex16>` format.\n *\n * Empty values pass through: the instance id is normally resolved from the API\n * key during `initialize()`, so only an explicitly provided malformed id is\n * rejected.\n */\nexport function validateInstanceId(instanceId: unknown): void {\n if (instanceId === undefined || instanceId === null || instanceId === '') return;\n if (typeof instanceId !== 'string' || !INSTANCE_ID_RE.test(instanceId)) {\n throw new InvalidInstanceIdError(`Invalid instance ID: ${String(instanceId)}`);\n }\n}\n","/**\n * Which model the agent runs on, sent with each conversation turn.\n *\n * The server sizes compaction to the agent's context window: it compacts at\n * about 11.7% of the window (15K tokens on a 128K model) and caps the\n * compacted record at half that. It reads the window from the latest turn's\n * metadata, under `agent_model` and `agent_context_window`. A model name is\n * enough: the server has a model -> window table. Give `context_window` only\n * for a model the table would not know, or to override it.\n *\n * Precedence, highest first: the value passed on the call, the client option\n * (`agent_model` / `agent_context_window`), a value already in the caller's\n * own metadata. With none of them the server assumes 128K. Mirrors Python's\n * `agent_model.py`.\n */\nexport const AGENT_MODEL_KEY = 'agent_model';\nexport const AGENT_CONTEXT_WINDOW_KEY = 'agent_context_window';\n\nexport interface AgentModel {\n model?: string | undefined;\n contextWindow?: number | undefined;\n}\n\n/** The per-call `model` / `context_window` (either spelling) off an options object. */\nexport function agentModelOf(options: unknown): AgentModel {\n const o = (options ?? {}) as Record<string, unknown>;\n const window = o['context_window'] ?? o['contextWindow'];\n return {\n model: typeof o['model'] === 'string' ? (o['model'] as string) : undefined,\n contextWindow: typeof window === 'number' ? window : undefined,\n };\n}\n\n/**\n * `metadata` with the agent's model folded in. `asStrings` for the gRPC\n * stream, whose metadata is a string map. Returns the input unchanged when\n * there is nothing to add.\n */\nexport function withAgentModel<T extends Record<string, unknown>>(\n metadata: T | undefined,\n perCall: AgentModel,\n defaults: AgentModel,\n asStrings = false,\n): T | undefined {\n const model = perCall.model || defaults.model;\n const window = perCall.contextWindow || defaults.contextWindow;\n if (!model && !window) return metadata;\n const out: Record<string, unknown> = { ...(metadata ?? {}) };\n if (model) out[AGENT_MODEL_KEY] = String(model);\n if (window) out[AGENT_CONTEXT_WINDOW_KEY] = asStrings ? String(Math.trunc(window)) : Math.trunc(window);\n return out as T;\n}\n","/**\n * Conversation namespace. Mirrors `ConversationInterface` and\n * `ConversationContextInterface` in the Python SDK.\n */\n\nimport { InvalidInputError } from '../errors.js';\nimport { validateConversationId } from '../util/validators.js';\nimport type { HttpTransport } from '../transport/http.js';\nimport { fetchContext } from '../context/fetch.js';\nimport { agentModelOf, withAgentModel, type AgentModel } from '../agent-model.js';\nimport type { FetchOptions, Json, RawContext } from '../context/types.js';\n\nexport interface RecordMessageOptions {\n conversation_id?: string;\n conversationId?: string;\n role: string;\n content: string;\n user_id?: string;\n userId?: string;\n customer_id?: string;\n customerId?: string;\n session_id?: string;\n sessionId?: string;\n metadata?: Json;\n /**\n * The rung this conversation belongs to, named in full, e.g.\n * `{ tenant: 'acme', practice: 'high_street' }`.\n *\n * Only relevant to a client whose ladder is deeper than the default three\n * rungs (root, tenant, person). On the default ladder `customer_id` and\n * `user_id` already say everything a path could.\n *\n * Sent as `scope` on the wire, matching Python's `scope_path`. Sending it\n * once per conversation is enough: the server fills a missing path and never\n * overwrites one.\n */\n scope_path?: Record<string, string>;\n scopePath?: Record<string, string>;\n /**\n * The model the agent runs on, e.g. `'gpt-4o'`. Overrides the client's\n * `agent_model` for this turn. The server sizes compaction to this model's\n * context window. Python's `model=`.\n */\n model?: string;\n /** The agent model's context window in tokens, for a model the server would not know. */\n context_window?: number;\n contextWindow?: number;\n}\n\nexport interface TranscriptTurn {\n role: string;\n content: string;\n [key: string]: unknown;\n}\n\nexport interface IngestTranscriptOptions {\n conversation_id?: string;\n conversationId?: string;\n user_id?: string;\n userId?: string;\n transcript: string | TranscriptTurn[];\n customer_id?: string;\n customerId?: string;\n conversation_type?: string;\n conversationType?: string;\n analysis?: Json;\n metadata?: Json;\n started_at?: string | Date;\n startedAt?: string | Date;\n ended_at?: string | Date;\n endedAt?: string | Date;\n /**\n * The rung this conversation belongs to, named in full, e.g.\n * `{ tenant: 'acme', practice: 'high_street' }`.\n *\n * Only relevant to a client whose ladder is deeper than the default three\n * rungs (root, tenant, person). On the default ladder `customer_id` and\n * `user_id` already say everything a path could.\n *\n * Sent as `scope` on the wire, matching Python's `scope_path`. Sending it\n * once per conversation is enough: the server fills a missing path and never\n * overwrites one.\n */\n scope_path?: Record<string, string>;\n scopePath?: Record<string, string>;\n}\n\n/**\n * Compaction levels, mirroring Python's `CompactionLevel` enum. The JS SDK\n * exported no equivalent, so `compaction_level` was an untyped `string` and a\n * typo reached the server instead of the compiler.\n */\nexport const COMPACTION_LEVELS = [\n 'low', 'medium', 'high', 'adaptive', 'aggressive', 'balanced', 'conservative',\n] as const;\n\nexport type CompactionLevel = (typeof COMPACTION_LEVELS)[number];\n\nexport interface CompactOptions {\n conversation_id?: string;\n conversationId?: string;\n strategy?: string;\n compaction_level?: CompactionLevel | string;\n compactionLevel?: CompactionLevel | string;\n target_tokens?: number;\n targetTokens?: number;\n force?: boolean;\n}\n\nfunction either<T>(o: Record<string, unknown>, snake: string, camel: string): T | undefined {\n const a = o[snake];\n if (a !== undefined) return a as T;\n const b = o[camel];\n return b === undefined ? undefined : (b as T);\n}\n\nfunction iso(v: string | Date | undefined): string | undefined {\n if (v === undefined) return undefined;\n return v instanceof Date ? v.toISOString() : v;\n}\n\nexport function buildRecordMessageBody(options: RecordMessageOptions): Json {\n const o = options as unknown as Record<string, unknown>;\n const conversationId = either<string>(o, 'conversation_id', 'conversationId');\n const userId = either<string>(o, 'user_id', 'userId');\n const customerId = either<string>(o, 'customer_id', 'customerId');\n\n if (!conversationId) throw new InvalidInputError('conversation_id is required');\n // Python validates the format here (and in record_messages_batch, which\n // reuses this builder per message). Note it deliberately does NOT validate\n // in ingest_transcript.\n validateConversationId(conversationId);\n if (!options.role) throw new InvalidInputError('role is required');\n if (options.content === undefined || options.content === null) {\n throw new InvalidInputError('content is required');\n }\n if (!userId) throw new InvalidInputError('user_id is required');\n // customer_id is NOT required. A B2C instance rejects one, so demanding it\n // here made the only correct B2C shape impossible to express.\n // checked by the caller, which holds the transport; see createConversationNamespace\n\n const body: Json = {\n conversation_id: conversationId,\n role: options.role,\n content: options.content,\n user_id: userId,\n customer_id: customerId,\n // Python always sends this, defaulting to {}.\n metadata: options.metadata ?? {},\n };\n const sessionId = either<string>(o, 'session_id', 'sessionId');\n if (sessionId !== undefined) body['session_id'] = sessionId;\n // `scope` on the wire, `scope_path` in the SDK, the same split Python makes.\n // Omitted when unset so a client on the default ladder sends exactly the\n // body it sends today.\n const scopePath = either<Record<string, string>>(o, 'scope_path', 'scopePath');\n if (scopePath !== undefined) body['scope'] = scopePath;\n return body;\n}\n\nexport function buildIngestTranscriptBody(options: IngestTranscriptOptions): Json {\n const o = options as unknown as Record<string, unknown>;\n const conversationId = either<string>(o, 'conversation_id', 'conversationId');\n const userId = either<string>(o, 'user_id', 'userId');\n if (!conversationId) throw new InvalidInputError('conversation_id is required');\n if (!userId) throw new InvalidInputError('user_id is required');\n if (options.transcript === undefined || options.transcript === null) {\n throw new InvalidInputError('transcript is required');\n }\n\n const body: Json = {\n conversation_id: conversationId,\n user_id: userId,\n transcript: options.transcript,\n };\n // Unset optionals are omitted so the server applies its own defaults, which\n // is what Python does here (and notably NOT what it does for memories.create).\n const customerId = either<string>(o, 'customer_id', 'customerId');\n const conversationType = either<string>(o, 'conversation_type', 'conversationType');\n const startedAt = iso(either<string | Date>(o, 'started_at', 'startedAt'));\n const endedAt = iso(either<string | Date>(o, 'ended_at', 'endedAt'));\n if (customerId !== undefined) body['customer_id'] = customerId;\n if (conversationType !== undefined) body['conversation_type'] = conversationType;\n if (options.analysis !== undefined) body['analysis'] = options.analysis;\n if (options.metadata !== undefined) body['metadata'] = options.metadata;\n if (startedAt !== undefined) body['started_at'] = startedAt;\n if (endedAt !== undefined) body['ended_at'] = endedAt;\n // Python 0.4.10 sends this on `ingest_transcript` and JS did not, so a\n // TypeScript client on a deep ladder had no route that could name a rung.\n const scopePath = either<Record<string, string>>(o, 'scope_path', 'scopePath');\n if (scopePath !== undefined) body['scope'] = scopePath;\n return body;\n}\n\nexport function buildCompactBody(options: CompactOptions): Json {\n const o = options as unknown as Record<string, unknown>;\n const conversationId = either<string>(o, 'conversation_id', 'conversationId');\n if (!conversationId) throw new InvalidInputError('conversation_id is required');\n validateConversationId(conversationId);\n\n const body: Json = { conversation_id: conversationId, force: options.force ?? false };\n const strategy = options.strategy ?? either<string>(o, 'compaction_level', 'compactionLevel');\n const targetTokens = either<number>(o, 'target_tokens', 'targetTokens');\n if (strategy !== undefined) body['strategy'] = strategy;\n if (targetTokens !== undefined) body['target_tokens'] = targetTokens;\n return body;\n}\n\n/**\n * Result of `conversation.ingest_transcript`, mirroring Python's\n * `TranscriptIngestResponse`. It was `Json` here, so `ingestion_id` came back\n * as `unknown` and could not be handed to `wait_for_completion` without a cast.\n */\nexport interface TranscriptIngestResult {\n conversation_id: string;\n /** The raw client-supplied id, echoed back. */\n external_conversation_id: string;\n /** Always set on a 2xx, including the `duplicate` branch. */\n ingestion_id: string;\n status: 'queued' | 'duplicate';\n turns_recorded: number;\n summary_status: 'in_progress' | 'already_compacted' | 'skipped';\n /** ISO-8601. */\n queued_at: string;\n [key: string]: unknown;\n}\n\nexport interface ConversationNamespace {\n record_message(options: RecordMessageOptions): Promise<Json>;\n record_messages_batch(messages: RecordMessageOptions[]): Promise<Json>;\n ingest_transcript(options: IngestTranscriptOptions): Promise<TranscriptIngestResult>;\n context: {\n fetch(options?: FetchOptions): Promise<RawContext>;\n get_context_for_prompt(options?: {\n conversation_id?: string;\n conversationId?: string;\n style?: string;\n /** Search the earlier turns locally after a compaction (default true). */\n search_history?: boolean;\n searchHistory?: boolean;\n /** What to search for; defaults to the latest user message. */\n query?: string;\n }): Promise<Json>;\n compact(options: CompactOptions): Promise<Json>;\n get_compacted(options: { conversation_id?: string; conversationId?: string; version?: number; format?: string }): Promise<Json | null>;\n get_compaction_status(options: { conversation_id?: string; conversationId?: string } | string): Promise<Json>;\n subscribe_to_compaction_updates(\n conversationId: string,\n callback: CompactionCallback,\n ): () => void;\n unsubscribe_all_compaction_updates(conversationId?: string): number;\n };\n}\n\nexport type CompactionCallback = (bundle: Json) => unknown;\n\n/**\n * Registry of per-conversation compaction subscribers.\n *\n * Split out from the namespace so the instance stream can dispatch into it\n * without the two namespaces importing each other.\n */\nexport class CompactionSubscribers {\n readonly #byConversation = new Map<string, CompactionCallback[]>();\n\n add(conversationId: string, callback: CompactionCallback): () => void {\n const list = this.#byConversation.get(conversationId);\n if (list === undefined) this.#byConversation.set(conversationId, [callback]);\n else list.push(callback);\n\n let unsubscribed = false;\n return () => {\n // Idempotent, matching Python: calling the thunk twice is a no-op.\n if (unsubscribed) return;\n unsubscribed = true;\n const current = this.#byConversation.get(conversationId);\n if (current === undefined) return;\n const at = current.indexOf(callback);\n if (at !== -1) current.splice(at, 1);\n if (current.length === 0) this.#byConversation.delete(conversationId);\n };\n }\n\n removeAll(conversationId?: string): number {\n if (conversationId === undefined) {\n let count = 0;\n for (const list of this.#byConversation.values()) count += list.length;\n this.#byConversation.clear();\n return count;\n }\n const list = this.#byConversation.get(conversationId);\n if (list === undefined) return 0;\n this.#byConversation.delete(conversationId);\n return list.length;\n }\n\n /** Fires subscribers in registration order. A throwing listener must not break the stream reader. */\n dispatch(conversationId: string, bundle: Json, onError: (e: unknown) => void): void {\n // Copy first: a callback that unsubscribes itself would otherwise mutate\n // the array mid-iteration and skip the next listener.\n for (const callback of [...(this.#byConversation.get(conversationId) ?? [])]) {\n try {\n const result = callback(bundle) as unknown;\n // An async callback returns a promise; swallow its rejection the way\n // Python swallows one from an asyncio task.\n if (result instanceof Promise) result.catch(onError);\n } catch (error) {\n onError(error);\n }\n }\n }\n}\n\n/**\n * Pair each message a batch write stored with the server's row for it.\n *\n * The server's `results` holds the messages that were stored, in input order,\n * and leaves the failed ones out; those are in `errors`, each with the `index`\n * it had in the request. Buffering every body, or pairing `results` against\n * the whole input by position, puts a message the server refused into the\n * caller's next prompt.\n *\n * Returns nothing when the two cannot be lined up, as Python does. A row with\n * no `message_id` is treated as not stored.\n */\nexport function storedBatchRows(\n bodies: Array<Record<string, unknown>>,\n result: unknown,\n): Array<[Record<string, unknown>, Record<string, unknown>]> {\n const r = (result ?? {}) as Record<string, unknown>;\n const results = Array.isArray(r['results']) ? (r['results'] as Array<Record<string, unknown>>) : [];\n const errors = Array.isArray(r['errors']) ? (r['errors'] as Array<Record<string, unknown>>) : [];\n const failed = new Set(errors.map((e) => e?.['index']));\n const stored = bodies.filter((_, index) => !failed.has(index));\n if (stored.length !== results.length) return [];\n const pairs: Array<[Record<string, unknown>, Record<string, unknown>]> = [];\n stored.forEach((body, i) => {\n const row = results[i];\n if (row && typeof row === 'object' && row['message_id']) pairs.push([body, row]);\n });\n return pairs;\n}\n\n/**\n * Called after a successful write with the ids that were written for.\n *\n * Backs `SYNAP_SDK_CACHE_INVALIDATE_ON_WRITE`. The cloud invalidates its own\n * Redis caches on ingest, but that stops at the process boundary: without this\n * the in-SDK anticipation cache keeps serving the pre-write view for the rest\n * of its TTL. Python calls its equivalent from these same three write paths.\n */\nexport type WriteInvalidator = (ids: {\n userId?: string | undefined;\n customerId?: string | undefined;\n}) => void;\n\n/**\n * Called with each turn a write path sends, so it can be buffered locally.\n *\n * This is what makes the next fetch see the turn that was just recorded, ahead\n * of the server having persisted and compacted it.\n */\nexport type TurnRecorder = (turn: {\n conversationId: string;\n role: string;\n content: string;\n timestamp?: string | undefined;\n /** The server's id for the turn, from the write's response. */\n messageId?: string | undefined;\n /**\n * The conversation's message count after this write, as the server reported\n * it. This is what tells the local store whether it holds the whole\n * conversation. Absent from a server that predates it.\n */\n serverMessageCount?: number | null | undefined;\n /** Other writes this process still has outstanding for the conversation. */\n inFlight?: number | undefined;\n}) => void;\n\nexport function createConversationNamespace(\n transport: HttpTransport,\n subscribers: CompactionSubscribers = new CompactionSubscribers(),\n onWrite: WriteInvalidator = () => {},\n onTurn: TurnRecorder = () => {},\n /**\n * Applied to a conversation-scoped response before it is returned.\n *\n * `scopePath` is carried because one of the things this hook does is splice\n * in a cached user summary, and a summary prefetched at one rung must not be\n * spliced into a response for a caller standing at another. Python reads the\n * path off the fetch arguments at that call site; here the call site is a\n * callback, so the path has to travel with it or the rung check is silently\n * skipped on this one path.\n */\n onFetched: (\n response: RawContext,\n conversationId: string | undefined,\n userId: string | undefined,\n scopePath: Record<string, string> | undefined,\n ) => void = () => {},\n /**\n * SDK-authoritative short-term context. Returns a locally rendered response\n * when the flag is on AND the store is warm for this conversation, which\n * skips the cloud round trip entirely; null falls through to the network.\n */\n localShortTerm: {\n prompt: (conversationId: string, style: string) => Json | null;\n compacted: (conversationId: string, format: string) => Json | null;\n /**\n * Handed the server's `/context-for-prompt` answer after the local store\n * could not answer, so the store can be brought back in step.\n */\n reseed?: (conversationId: string, serverResponse: Json) => void;\n /**\n * The response with the earlier turns that best match `query` appended,\n * found by a local search of the turn archive. Python:\n * `MaximemSynapSDK._with_history_excerpts`.\n */\n historyExcerpts?: (conversationId: string, response: Json, query: string | undefined) => Promise<Json>;\n } = { prompt: () => null, compacted: () => null },\n /** The client's `agent_model` / `agent_context_window`, folded into each turn's metadata. */\n agentModel: () => AgentModel = () => ({}),\n): ConversationNamespace {\n // conversation_id -> REST writes sent and not yet buffered locally. Lets the\n // local store tell two of this process's own concurrent writes apart from a\n // second writer.\n const writesInFlight = new Map<string, number>();\n const withTurnModel = (body: Json, options: unknown): Json => {\n const metadata = withAgentModel(\n body['metadata'] as Record<string, unknown> | undefined, agentModelOf(options), agentModel(),\n );\n return metadata === undefined ? body : { ...body, metadata: metadata as Json };\n };\n const writeStarted = (conversationId: string): void => {\n writesInFlight.set(conversationId, (writesInFlight.get(conversationId) ?? 0) + 1);\n };\n const writeLanded = (conversationId: string): void => {\n const remaining = (writesInFlight.get(conversationId) ?? 0) - 1;\n if (remaining > 0) writesInFlight.set(conversationId, remaining);\n else writesInFlight.delete(conversationId);\n };\n const confirmation = (\n result: unknown,\n conversationId: string,\n ): { messageId?: string; serverMessageCount?: number | null; inFlight: number } => {\n const r = (result ?? {}) as Record<string, unknown>;\n const count = r['conversation_message_count'];\n return {\n ...(typeof r['message_id'] === 'string' ? { messageId: r['message_id'] } : {}),\n serverMessageCount: typeof count === 'number' ? count : null,\n inFlight: writesInFlight.get(conversationId) ?? 0,\n };\n };\n const convId = (v: { conversation_id?: string; conversationId?: string } | string): string => {\n const id = typeof v === 'string' ? v : (v.conversation_id ?? v.conversationId);\n if (!id) throw new InvalidInputError('conversation_id is required');\n // Covers context.fetch, get_compacted, get_compaction_status and\n // get_context_for_prompt: the four routes Python validates through this\n // same shape. ingest_transcript does not go through here, which is correct.\n validateConversationId(id);\n return id;\n };\n\n return {\n async record_message(options) {\n transport.checkCustomerId(\n (options as unknown as Record<string, unknown>)['customer_id'] ??\n (options as unknown as Record<string, unknown>)['customerId'],\n 'conversation.record_message',\n );\n const body = withTurnModel(buildRecordMessageBody(options), options);\n const conversationId = String(body['conversation_id'] ?? '');\n writeStarted(conversationId);\n let result: Json;\n try {\n result = await transport.request<Json>('conversations_messages', { body });\n } finally {\n writeLanded(conversationId);\n }\n // Buffer locally only AFTER the server accepted it. Recording a turn the\n // write failed on would surface a message that does not exist.\n onTurn({\n conversationId,\n role: String(body['role'] ?? 'user'),\n content: String(body['content'] ?? ''),\n timestamp: body['timestamp'] as string | undefined,\n ...confirmation(result, conversationId),\n });\n onWrite({\n userId: body['user_id'] as string | undefined,\n customerId: body['customer_id'] as string | undefined,\n });\n return result;\n },\n\n async record_messages_batch(messages) {\n if (!Array.isArray(messages) || messages.length === 0) {\n throw new InvalidInputError('messages must be a non-empty array');\n }\n // Every message checked BEFORE any is sent: a batch that fails on its\n // fifth message would otherwise be half-applied.\n for (const m of messages) {\n const mo = m as unknown as Record<string, unknown>;\n transport.checkCustomerId(\n mo['customer_id'] ?? mo['customerId'], 'conversation.record_messages_batch',\n );\n }\n const bodies = messages.map((m) => withTurnModel(buildRecordMessageBody(m), m));\n const result = await transport.request<Json>('conversations_messages_batch', {\n body: { messages: bodies },\n });\n // Buffer only what the server stored, each paired with its own row.\n for (const [body, row] of storedBatchRows(bodies, result)) {\n const conversationId = String(body['conversation_id'] ?? '');\n onTurn({\n conversationId,\n role: String(body['role'] ?? 'user'),\n content: String(body['content'] ?? ''),\n timestamp: body['timestamp'] as string | undefined,\n ...confirmation(row, conversationId),\n });\n }\n // Every distinct entity in the batch, not just the first message's.\n for (const body of bodies) {\n onWrite({\n userId: body['user_id'] as string | undefined,\n customerId: body['customer_id'] as string | undefined,\n });\n }\n return result;\n },\n\n async ingest_transcript(options) {\n const body = buildIngestTranscriptBody(options);\n const result = await transport.request<TranscriptIngestResult>('conversations_ingest', { body });\n onWrite({\n userId: body['user_id'] as string | undefined,\n customerId: body['customer_id'] as string | undefined,\n });\n return result;\n },\n\n context: {\n async fetch(options = {}) {\n // Conversation-scope fetch is one of Python's seven validation sites.\n // It does not route through convId() because the id travels in the\n // body rather than the path, so validate it explicitly.\n const id =\n (options as { conversation_id?: string; conversationId?: string }).conversation_id ??\n (options as { conversationId?: string }).conversationId;\n validateConversationId(id);\n const response = await fetchContext(transport, 'conversation', options);\n // Read-your-writes. This is the path where it matters most: a fast\n // follow-up on the same conversation would otherwise retrieve context\n // missing the turn just recorded.\n onFetched(\n response,\n id,\n (options as { user_id?: string; userId?: string }).user_id ??\n (options as { userId?: string }).userId,\n (options as { scope_path?: Record<string, string> }).scope_path ??\n (options as { scopePath?: Record<string, string> }).scopePath,\n );\n return response;\n },\n\n get_context_for_prompt: async (options = {}) => {\n const id = convId(options);\n const search = (options.search_history ?? options.searchHistory ?? true) && localShortTerm.historyExcerpts;\n const withHistory = async (response: Json): Promise<Json> => {\n if (!search) return response;\n try {\n return await localShortTerm.historyExcerpts!(id, response, options.query);\n } catch {\n return response; // never fail the prompt over its extras\n }\n };\n const local = localShortTerm.prompt(id, options.style ?? 'structured');\n if (local !== null) return withHistory(local);\n const result = await transport.request<Json>('context_for_prompt', {\n pathParams: { conversation_id: id },\n query: { style: options.style ?? 'structured' },\n });\n const response = (result?.['context_for_prompt'] as Json) ?? result ?? {};\n localShortTerm.reseed?.(id, response);\n return withHistory(response);\n },\n\n async compact(options) {\n return transport.request<Json>('conversations_compact', { body: buildCompactBody(options) });\n },\n\n get_compacted: async (options) => {\n const id = convId(options);\n // Python only takes the local path when no explicit version is asked\n // for: a pinned version is a request for a specific server artefact.\n if (options.version === undefined) {\n const local = localShortTerm.compacted(id, options.format ?? 'structured');\n if (local !== null) return local;\n }\n const query: Record<string, string | number> = { format: options.format ?? 'structured' };\n if (options.version !== undefined) query['version'] = options.version;\n try {\n return await transport.request<Json>('conversations_compacted', {\n pathParams: { conversation_id: id },\n query,\n });\n } catch (error) {\n // Python returns None when there is nothing compacted yet, rather\n // than surfacing a 404. Matching that keeps callers identical.\n if ((error as { code?: string }).code === 'context_not_found') return null;\n throw error;\n }\n },\n\n async get_compaction_status(options) {\n return transport.request<Json>('conversations_compaction_status', {\n pathParams: { conversation_id: convId(options) },\n });\n },\n\n /**\n * Fire `callback` whenever a `compaction_update` bundle arrives on the\n * Listen stream for this conversation.\n *\n * Requires an active `client.instance.listen()`; without it no bundles\n * arrive and the callback never fires. Registering before listening is\n * fine, which is why this does not throw when the stream is down.\n *\n * Returns an idempotent unsubscribe thunk, matching Python.\n */\n subscribe_to_compaction_updates(conversationId, callback) {\n if (!conversationId) throw new InvalidInputError('conversation_id is required');\n if (typeof callback !== 'function') throw new InvalidInputError('callback is required');\n return subscribers.add(conversationId, callback);\n },\n\n /** Remove subscribers for one conversation, or all of them. Returns the count removed. */\n unsubscribe_all_compaction_updates(conversationId?: string) {\n return subscribers.removeAll(conversationId);\n },\n },\n };\n}\n","/** User namespace. Mirrors `UserInterface` in the Python SDK. */\n\nimport { InvalidInputError } from '../errors.js';\nimport type { HttpTransport } from '../transport/http.js';\nimport { fetchContext, type FetchHooks } from '../context/fetch.js';\nimport type { FetchOptions, Json, RawContext } from '../context/types.js';\n\nexport interface UserNamespace {\n get_profile(options: { user_id?: string; userId?: string; customer_id?: string; customerId?: string } | string): Promise<Json>;\n context: { fetch(options?: FetchOptions): Promise<RawContext> };\n}\n\nexport function createUserNamespace(\n transport: HttpTransport,\n hooks: FetchHooks = {},\n): UserNamespace {\n return {\n async get_profile(options) {\n const userId = typeof options === 'string' ? options : (options.user_id ?? options.userId);\n if (!userId) throw new InvalidInputError('user_id is required');\n const customerId =\n typeof options === 'string' ? undefined : (options.customer_id ?? options.customerId);\n return transport.request<Json>('user_profile', {\n pathParams: { user_id: userId },\n // Sent only when provided, matching Python. Note the server returns\n // 500 rather than a clean error if it needs one and does not get it.\n ...(customerId !== undefined ? { query: { customer_id: customerId } } : {}),\n });\n },\n context: { async fetch(options = {}) { return fetchContext(transport, 'user', options, hooks); } },\n };\n}\n","/** Credits namespace. Mirrors `CreditsInterface` in the Python SDK. */\n\nimport { InvalidInputError } from '../errors.js';\nimport type { HttpTransport } from '../transport/http.js';\nimport type { Json } from '../context/types.js';\n\nexport interface EstimateOptions {\n metric_type?: string;\n metricType?: string;\n units?: number;\n item_count?: number;\n itemCount?: number;\n endpoint?: string;\n mode?: string;\n}\n\nexport interface CreditsNamespace {\n get_balance(): Promise<CreditBalance>;\n get_ledger(options?: LedgerOptions): Promise<CreditLedgerPage>;\n estimate(options: EstimateOptions): Promise<CreditEstimate>;\n redeem(code: string): Promise<RedeemResult>;\n}\n\n/**\n * Return types for the credits namespace, mirroring Python's dataclasses in\n * `maximem_synap/credits.py`. These were all `Json` (`Record<string, unknown>`)\n * before, so every field read was `unknown` and no documented example compiled.\n */\nexport interface CreditBucket {\n source_type: string;\n balance: number;\n /** ISO-8601, or null when the bucket does not expire. */\n expires_at: string | null;\n}\n\nexport interface CreditBalance {\n client_id: string;\n balance_credits: number;\n warning_low: boolean;\n buckets: CreditBucket[];\n [key: string]: unknown;\n}\n\nexport interface CreditLedgerEntry {\n ledger_id: string;\n entry_type: string;\n delta: number;\n metric_type: string | null;\n category: string | null;\n /** ISO-8601. */\n created_at: string;\n}\n\nexport interface CreditLedgerPage {\n entries: CreditLedgerEntry[];\n total: number;\n limit: number;\n offset: number;\n [key: string]: unknown;\n}\n\nexport interface CreditEstimate {\n credits_estimate: number;\n [key: string]: unknown;\n}\n\nexport interface RedeemResult {\n redemption_id: string;\n credits_granted: number;\n new_balance_credits: number;\n /** ISO-8601, or null when the granted credits do not expire. */\n expires_at: string | null;\n [key: string]: unknown;\n}\n\n/** Options for `credits.get_ledger`. Mirrors Python's keyword arguments. */\nexport interface LedgerOptions {\n /** Filter to one ledger entry type. */\n entry_type?: string;\n entryType?: string;\n /** Inclusive lower bound. ISO-8601 string or Date. Sent as `from`. */\n from_time?: string | Date;\n fromTime?: string | Date;\n /** Inclusive upper bound. ISO-8601 string or Date. Sent as `to`. */\n to_time?: string | Date;\n toTime?: string | Date;\n /** Defaults to 100, as in Python. */\n limit?: number;\n offset?: number;\n}\n\nfunction iso(v: string | Date): string {\n return v instanceof Date ? v.toISOString() : v;\n}\n\nexport function createCreditsNamespace(transport: HttpTransport): CreditsNamespace {\n return {\n async get_balance() { return transport.request<CreditBalance>('credits_balance'); },\n\n async get_ledger(options: LedgerOptions = {}) {\n // Mirrors Python's CreditsInterface.get_ledger: the three filters were\n // missing here entirely, and the default limit was 50 against Python's\n // 100, so the same call returned different pages in the two SDKs.\n const query: Record<string, string | number> = {\n limit: options.limit ?? 100,\n offset: options.offset ?? 0,\n };\n const entryType = options.entry_type ?? options.entryType;\n if (entryType !== undefined) query['entry_type'] = entryType;\n const from = options.from_time ?? options.fromTime;\n if (from !== undefined) query['from'] = iso(from);\n const to = options.to_time ?? options.toTime;\n if (to !== undefined) query['to'] = iso(to);\n return transport.request<CreditLedgerPage>('credits_ledger', { query });\n },\n\n async estimate(options) {\n return transport.request<CreditEstimate>('credits_estimate', {\n body: {\n metric_type: options.metric_type ?? options.metricType ?? null,\n units: options.units ?? null,\n item_count: options.item_count ?? options.itemCount ?? null,\n endpoint: options.endpoint ?? null,\n mode: options.mode ?? null,\n },\n });\n },\n\n async redeem(code) {\n if (!code) throw new InvalidInputError('code is required');\n // Non-idempotent by contract: redeeming twice consumes two codes.\n return transport.request<RedeemResult>('credits_redeem', { body: { code } });\n },\n };\n}\n","/**\n * Cache namespace. Mirrors `CacheInterface` in the Python SDK.\n *\n * This is a deliberately narrow facade over the AnticipationCache rather than\n * the cache object itself. Exposing the instance directly leaked internals\n * (`client.cache.now` was reachable) and gave JS a different surface from\n * Python for no benefit. The cache is still reachable as\n * `client.anticipationCache` for advanced use.\n */\n\nimport type { AnticipationCache } from '../context/anticipation-cache.js';\nimport { InvalidInputError } from '../errors.js';\n\nexport interface CacheStats {\n bundles: number;\n items: number;\n [key: string]: unknown;\n}\n\nexport interface CacheNamespace {\n clear(): void;\n clear_user(userId: string): void;\n clear_customer(customerId: string): void;\n stats(): CacheStats;\n}\n\nexport function createCacheNamespace(cache: AnticipationCache): CacheNamespace {\n return {\n clear: () => cache.clear(),\n\n clear_user: (userId) => {\n if (!userId) throw new InvalidInputError('user_id is required');\n cache.dropEntity(userId);\n },\n\n clear_customer: (customerId) => {\n if (!customerId) throw new InvalidInputError('customer_id is required');\n cache.dropEntity(customerId);\n },\n\n stats: () => ({ bundles: cache.size, items: cache.itemCount }),\n };\n}\n","/**\n * Instance namespace. Mirrors `InstanceInterface` in the Python SDK.\n *\n * Everything here rides the bidirectional gRPC Listen stream. The stream module\n * is loaded through a lazy `import()` so that importing this SDK on Edge or in\n * a Worker stays safe: grpc-js is built on `node:http2`/`node:net`/`node:tls`\n * and a static import fails the BUILD there, not the call.\n */\n\nimport { agentModelOf, withAgentModel, type AgentModel } from '../agent-model.js';\nimport {\n ListeningAlreadyActiveError, ListeningNotActiveError, SynapError,\n} from '../errors.js';\nimport type { AnticipationCache } from '../context/anticipation-cache.js';\nimport type { StreamCredentials, StreamState } from '../grpc/stream-client.js';\nimport { getEnv } from '../util/env.js';\nimport { newCorrelationId } from '../util/correlation.js';\n\nexport interface ListenOptions {\n /** Called with the attempt count each time the stream reconnects. */\n on_reconnect?: (attempt: number) => void;\n onReconnect?: (attempt: number) => void;\n /** Called with a reason string when the stream drops. */\n on_disconnect?: (reason: string) => void;\n onDisconnect?: (reason: string) => void;\n /**\n * Called for every bundle that arrives. Bundles are ALSO stored in the\n * anticipation cache automatically, so `fetch()` finds them without a round\n * trip; this callback is for observing, not for wiring up storage.\n */\n on_context?: (bundle: Record<string, unknown>) => void;\n onContext?: (bundle: Record<string, unknown>) => void;\n /** Overrides `SYNAP_GRPC_HOST`. */\n host?: string;\n /** Overrides `SYNAP_GRPC_PORT`. */\n port?: number;\n /** Overrides `SYNAP_GRPC_USE_TLS`. Plaintext only makes sense against localhost. */\n use_tls?: boolean;\n useTls?: boolean;\n}\n\nexport interface SendMessageOptions {\n content: string;\n role?: string;\n conversation_id?: string;\n conversationId?: string;\n user_id?: string;\n userId?: string;\n customer_id?: string;\n customerId?: string;\n session_id?: string;\n sessionId?: string;\n event_type?: string;\n eventType?: string;\n metadata?: Record<string, string>;\n /** The agent's model, as on `conversation.record_message`. Python's `model=`. */\n model?: string;\n context_window?: number;\n contextWindow?: number;\n /**\n * Report a tool call on the stream. Python's `send_message` takes both, and\n * the proto has `tool_name` / `tool_args_json`; the JS surface simply never\n * exposed them, so a tool call could not be reported from JavaScript.\n */\n tool_name?: string;\n toolName?: string;\n tool_args?: Record<string, unknown>;\n toolArgs?: Record<string, unknown>;\n /**\n * What a tool returned. Travels in its own proto field, not in `content`: a\n * result is not something a person said, and everything that filters on\n * messages reads `content`.\n */\n tool_result?: unknown;\n toolResult?: unknown;\n /** Ties a result to the call that asked for it. */\n tool_call_id?: string;\n toolCallId?: string;\n search_queries?: string[];\n searchQueries?: string[];\n context_types?: string[];\n contextTypes?: string[];\n}\n\nexport interface RecordThinkingOptions {\n content: string;\n /**\n * Ordinal of this thought within the current turn. Sent as a `step_index`\n * metadata key, which is how Python's `record_thinking` transmits it: the\n * proto has no dedicated field.\n */\n step_index?: number;\n stepIndex?: number;\n /** Free-form label for the kind of reasoning. Sent as `thought_type`. */\n thought_type?: string;\n thoughtType?: string;\n conversation_id?: string;\n conversationId?: string;\n user_id?: string;\n userId?: string;\n customer_id?: string;\n customerId?: string;\n session_id?: string;\n sessionId?: string;\n metadata?: Record<string, string>;\n}\n\nexport interface RecordToolCallOptions {\n tool_name?: string;\n toolName?: string;\n tool_args?: Record<string, unknown>;\n toolArgs?: Record<string, unknown>;\n tool_call_id?: string;\n toolCallId?: string;\n conversation_id?: string;\n conversationId?: string;\n user_id?: string;\n userId?: string;\n customer_id?: string;\n customerId?: string;\n session_id?: string;\n sessionId?: string;\n metadata?: Record<string, string>;\n}\n\nexport interface RecordToolResultOptions {\n /** JSON-encodable, or a plain string. A string is sent as itself. */\n result: unknown;\n tool_name?: string;\n toolName?: string;\n tool_call_id?: string;\n toolCallId?: string;\n conversation_id?: string;\n conversationId?: string;\n user_id?: string;\n userId?: string;\n customer_id?: string;\n customerId?: string;\n session_id?: string;\n sessionId?: string;\n metadata?: Record<string, string>;\n}\n\nexport interface InstanceNamespace {\n listen(options?: ListenOptions): Promise<void>;\n stop_listening(): Promise<void>;\n send_message(options: SendMessageOptions): Promise<void>;\n record_thinking(options: RecordThinkingOptions): Promise<void>;\n record_tool_call(options: RecordToolCallOptions): Promise<void>;\n record_tool_result(options: RecordToolResultOptions): Promise<void>;\n end_session(conversationId: string): Promise<void>;\n /** Property, not a method, matching Python's InstanceInterface.is_listening. */\n readonly is_listening: boolean;\n /** Not in Python: lets the compaction subscriptions share this one stream. */\n readonly _internal: InstanceInternals;\n}\n\n/** @internal Shared with the conversation namespace so both use one stream. */\nexport interface InstanceInternals {\n onCompactionUpdate(conversationId: string, bundle: Record<string, unknown>): void;\n /** Set by the client so a compaction can prune the locally buffered tail. */\n setCompactionApplier(apply: (bundle: Record<string, unknown>) => void): void;\n /**\n * Set by the client: called when `listen()` starts, which is when prefetched\n * bundles begin to arrive, so the local embedding model loads then.\n */\n setOnListen(handle: () => void): void;\n /**\n * Set by the client. Receives the server's message count for a conversation\n * after each streamed write has been stored.\n */\n setConversationStateHandler(\n handle: (conversationId: string, messageCount: number) => void,\n ): void;\n /** Learning-loop event, fired after a fetch is served from the cache. */\n sendContextUsed(event: Record<string, unknown>): void;\n /** Audit event, fired after every fetch resolves. */\n sendContextAssembled(event: Record<string, unknown>): void;\n setCompactionDispatcher(\n dispatch: (conversationId: string, bundle: Record<string, unknown>) => void,\n ): void;\n isListening(): boolean;\n}\n\ntype StreamClient = import('../grpc/stream-client.js').GrpcStreamClient;\n\nconst DEFAULT_GRPC_HOST = 'synap-cloud-prod.maximem.ai';\nconst DEFAULT_GRPC_PORT = 443;\n\nfunction envPort(): number | undefined {\n const raw = getEnv('SYNAP_GRPC_PORT');\n if (raw === undefined || raw === '') return undefined;\n const parsed = Number(raw);\n return Number.isFinite(parsed) ? parsed : undefined;\n}\n\nfunction envTls(): boolean | undefined {\n // SYNAP_GRPC_TLS is the 0.3.x wrapper's spelling. Honoured as a fallback,\n // because silently ignoring it turns a working plaintext deployment into one\n // that attempts TLS and cannot connect, with nothing pointing at the cause.\n const raw = getEnv('SYNAP_GRPC_USE_TLS') ?? getEnv('SYNAP_GRPC_TLS');\n if (raw === undefined || raw === '') return undefined;\n return !['0', 'false', 'no', 'off'].includes(raw.trim().toLowerCase());\n}\n\n/**\n * Fold `step_index` and `thought_type` into the metadata map.\n *\n * Python does exactly this (`sdk.py`, record_thinking): the proto carries no\n * dedicated fields, so the values ride in metadata under those two keys. A\n * caller-supplied metadata entry of the same name is not overwritten.\n */\nfunction thinkingMetadata(options: RecordThinkingOptions): Record<string, string> {\n const md: Record<string, string> = { ...(options.metadata ?? {}) };\n const step = options.step_index ?? options.stepIndex;\n if (step !== undefined && md['step_index'] === undefined) {\n md['step_index'] = String(step);\n }\n const kind = options.thought_type ?? options.thoughtType;\n if (kind && md['thought_type'] === undefined) {\n md['thought_type'] = kind;\n }\n return md;\n}\n\n/**\n * Event types the server has a reader for. Mirrors `KNOWN_EVENT_TYPES` in the\n * Python SDK. An unknown type falls through every classifier on the server into\n * UNKNOWN, where nothing reads it, so a typo used to cost the whole signal\n * without saying so.\n */\nconst KNOWN_EVENT_TYPES: ReadonlySet<string> = new Set([\n 'user_message',\n 'assistant_message',\n 'tool_call',\n 'tool_result',\n 'agent_thinking',\n 'context_request',\n 'context_fetch',\n 'session_start',\n 'session_end',\n]);\n\n/**\n * Serialise a tool result the way Python does: a string travels as itself,\n * anything else as JSON. `JSON.stringify` on a plain-text answer would wrap it\n * in quotes and the agent would read the quotes as part of the result.\n */\nfunction toolResultJson(result: unknown): string {\n if (result === undefined || result === null) return '';\n return typeof result === 'string' ? result : jsonOrText(result);\n}\n\n/**\n * JSON for whatever serialises, the value's text form for whatever does not.\n * Mirrors Python's `_json_or_text` in `sdk.py`.\n *\n * ⚠ This was a bare `JSON.stringify` and that lost events. It throws on a\n * circular object and on a `BigInt`, and it returns `undefined` rather than a\n * string for a function or a bare `undefined`. Every integration reports\n * through a helper that swallows exceptions on purpose, so a telemetry call can\n * never break somebody's agent loop, which means the throw went nowhere and the\n * whole tool event was silently never sent. Losing the exact shape of an object\n * is a far smaller loss than losing the event.\n */\nfunction jsonOrText(value: unknown): string {\n try {\n const out = JSON.stringify(\n value,\n (_k, v: unknown) => (typeof v === 'bigint' ? String(v) : v),\n );\n // `undefined` for a function, a symbol, or `undefined` itself.\n return out === undefined ? String(value) : out;\n } catch {\n // A cycle, or a `toJSON` of the caller's that throws.\n try {\n return String(value);\n } catch {\n return '';\n }\n }\n}\n\nexport function createInstanceNamespace(\n credentials: () => StreamCredentials,\n cache: AnticipationCache,\n /** Buffers a stream-sent turn locally, same as the HTTP write paths. */\n onTurn?: (turn: { conversationId: string; role: string; content: string }) => void,\n /** The client's `agent_model` / `agent_context_window`, folded into each event's metadata. */\n agentModel: () => AgentModel = () => ({}),\n /** Keeps a reported tool result in the local turn archive (`archive_tool_result`). */\n onToolResult?: (r: { conversationId: string; result: unknown; toolName: string }) => void,\n): InstanceNamespace {\n let client: StreamClient | null = null;\n // conversationId -> sessionId, for conversations this stream has opened a\n // session for. Cleared on reconnect: a session belongs to the stream that\n // carried it, and the server's new stream has never seen it.\n const openSessions = new Map<string, string>();\n let dispatchCompaction:\n | ((conversationId: string, bundle: Record<string, unknown>) => void)\n | null = null;\n\n let applyCompaction: ((bundle: Record<string, unknown>) => void) | null = null;\n let onListen: (() => void) | null = null;\n let onConversationState: ((conversationId: string, messageCount: number) => void) | null =\n null;\n\n const internals: InstanceInternals = {\n onCompactionUpdate(conversationId, bundle) {\n // Prune the local tail FIRST, then notify subscribers, so a subscriber\n // that immediately fetches sees the post-compaction view.\n applyCompaction?.(bundle);\n dispatchCompaction?.(conversationId, bundle);\n },\n setCompactionApplier(apply) {\n applyCompaction = apply;\n },\n setOnListen(handle) {\n onListen = handle;\n },\n setConversationStateHandler(handle) {\n onConversationState = handle;\n },\n sendContextUsed(event) {\n // Fire-and-forget: the stream drops it silently when disconnected, which\n // is correct for telemetry that must never affect the caller's turn.\n client?.sendContextUsed(event);\n },\n sendContextAssembled(event) {\n client?.sendContextAssembled(event);\n },\n setCompactionDispatcher(dispatch) {\n dispatchCompaction = dispatch;\n },\n isListening() {\n return client !== null && client.isConnected;\n },\n };\n\n /**\n * Open a session for this conversation if the stream has not yet.\n *\n * The developer never calls this. `session_start` is what makes the server\n * run its warm-up prefetch before the first message arrives, and asking every\n * caller to remember a lifecycle call is how the first turn of every\n * conversation stayed a cold fetch: neither SDK sent one, ever.\n *\n * Best effort. A session that cannot be opened must not stop the event that\n * triggered it, so the conversation stays unmarked and the next event retries.\n */\n function ensureSession(\n conversationId: string, userId: string, customerId: string,\n ): void {\n // Both ids, or no session. The server refuses a session_start with no\n // user_id, and writing one only says it went out, not that it was\n // accepted — so opening on an event with no user_id marked the\n // conversation as open, never retried, and left `end_session` referring to\n // a session the server never had. The next event that carries a user_id\n // opens it.\n if (!conversationId || !userId || client === null) return;\n if (openSessions.has(conversationId)) return;\n // The same helper the correlation ids use: a session id is an identifier\n // for tracing, never a security value, and that helper is the one that is\n // safe on Edge and Workers, where `node:crypto` breaks the build.\n const sessionId = newCorrelationId();\n try {\n client.sendSessionControl({\n action: 'start',\n session_id: sessionId,\n conversation_id: conversationId,\n ...(userId ? { user_id: userId } : {}),\n ...(customerId ? { customer_id: customerId } : {}),\n });\n openSessions.set(conversationId, sessionId);\n } catch {\n // Leave it unmarked so the next event opens it.\n }\n }\n\n const namespace: InstanceNamespace = {\n async listen(options: ListenOptions = {}) {\n if (client !== null) {\n // Python raises this from InstanceController when a second listen()\n // lands on an already-active stream.\n throw new ListeningAlreadyActiveError('Listening is already active on this instance');\n }\n\n try {\n onListen?.();\n } catch {\n /* loading the local model must never stop the stream */\n }\n const { GrpcStreamClient } = await import('../grpc/stream-client.js');\n const onContext = options.on_context ?? options.onContext;\n const onDisconnect = options.on_disconnect ?? options.onDisconnect;\n const onReconnect = options.on_reconnect ?? options.onReconnect;\n let reconnectAttempts = 0;\n\n // Assigned immediately below; the callbacks that read it only run later,\n // once the stream is live.\n let stream: InstanceType<typeof GrpcStreamClient> | null = null;\n\n const created = new GrpcStreamClient(credentials(), cache, {\n host: options.host ?? getEnv('SYNAP_GRPC_HOST') ?? DEFAULT_GRPC_HOST,\n port: options.port ?? envPort() ?? DEFAULT_GRPC_PORT,\n useTls: options.use_tls ?? options.useTls ?? envTls() ?? true,\n ...(onContext !== undefined ? { onContext } : {}),\n onCompactionUpdate: (conversationId, bundle) => {\n internals.onCompactionUpdate(conversationId, bundle);\n },\n onConversationState: (conversationId, messageCount) => {\n onConversationState?.(conversationId, messageCount);\n },\n onStateChange: (state: StreamState) => {\n // Any state change away from connected retires the sessions this\n // stream opened: the server's next stream has no memory of them, and\n // a client that believes one is open never warms up again.\n if (state !== 'connected') openSessions.clear();\n if (state === 'reconnecting') {\n reconnectAttempts += 1;\n onReconnect?.(reconnectAttempts);\n } else if (state === 'disconnected') {\n // A credit refusal names itself, the way Python's transport does,\n // so a handler can tell \"the server ran out of patience\" from\n // \"this account cannot pay for the stream\".\n const stopped = stream?.lastError ?? null;\n const reason = stopped === null\n ? 'stream disconnected'\n : `credit_stop:${(stopped as { reason?: string | null }).reason ?? 'unknown'}`;\n onDisconnect?.(reason);\n }\n },\n });\n stream = created;\n\n try {\n await created.connect();\n } catch (error) {\n // Leave `client` null so a retry is possible rather than being\n // rejected as \"already active\" forever.\n if (error instanceof SynapError) throw error;\n throw new SynapError(\n `Could not open the Synap anticipation stream: ${(error as Error)?.message ?? String(error)}`,\n { cause: error },\n );\n }\n client = created;\n },\n\n async stop_listening() {\n // Resolves rather than rejecting: stopping something that was never\n // started is a no-op in Python too, and a cleanup path that throws is\n // worse than useless in a finally block.\n if (client === null) return;\n // Close what this stream opened. A session that is never closed leaves\n // the turn it was in the middle of uncommitted on the server: that row is\n // written when the NEXT user message arrives, and the last turn of a\n // conversation has no next one.\n for (const conversationId of [...openSessions.keys()]) {\n await namespace.end_session(conversationId);\n }\n const active = client;\n client = null;\n await active.disconnect();\n },\n\n async end_session(conversationId: string) {\n const sessionId = openSessions.get(conversationId);\n if (sessionId === undefined || client === null) return;\n openSessions.delete(conversationId);\n try {\n client.sendSessionControl({\n action: 'end',\n session_id: sessionId,\n conversation_id: conversationId,\n });\n } catch {\n // A teardown path must not throw.\n }\n },\n\n async send_message(options: SendMessageOptions) {\n if (client === null) throw new ListeningNotActiveError('Listening is not active');\n if (options.content === undefined) {\n throw new ListeningNotActiveError('content is required');\n }\n const eventType = options.event_type ?? options.eventType ?? 'user_message';\n if (!KNOWN_EVENT_TYPES.has(eventType)) {\n throw new SynapError(\n `unknown event_type '${eventType}'. Expected one of: `\n + [...KNOWN_EVENT_TYPES].sort().join(', '),\n );\n }\n const conversationId = options.conversation_id ?? options.conversationId ?? '';\n ensureSession(\n conversationId,\n options.user_id ?? options.userId ?? '',\n options.customer_id ?? options.customerId ?? '',\n );\n // Only conversational turns belong in the local short-term buffer. A tool\n // call or a result is not a turn, and buffering one splices it into the\n // caller's own prompt as if somebody had said it. Python mirrors user and\n // assistant messages only; this side mirrored everything.\n if (eventType === 'user_message' || eventType === 'assistant_message') {\n onTurn?.({\n conversationId,\n role: options.role ?? 'user',\n content: options.content,\n });\n }\n client.sendConversationEvent({\n event_type: eventType,\n content: options.content,\n role: options.role ?? 'user',\n conversation_id: options.conversation_id ?? options.conversationId ?? '',\n user_id: options.user_id ?? options.userId ?? '',\n customer_id: options.customer_id ?? options.customerId ?? '',\n session_id: options.session_id ?? options.sessionId\n ?? openSessions.get(conversationId) ?? '',\n metadata: withAgentModel(options.metadata, agentModelOf(options), agentModel(), true) ?? {},\n timestamp_ms: Date.now(),\n tool_name: options.tool_name ?? options.toolName ?? '',\n // Python serialises tool_args with json.dumps into tool_args_json.\n tool_args_json: (() => {\n const a = options.tool_args ?? options.toolArgs;\n return a === undefined ? '' : jsonOrText(a);\n })(),\n tool_result_json: toolResultJson(options.tool_result ?? options.toolResult),\n tool_call_id: options.tool_call_id ?? options.toolCallId ?? '',\n search_queries: options.search_queries ?? options.searchQueries ?? [],\n context_types: options.context_types ?? options.contextTypes ?? [],\n });\n },\n\n async record_tool_call(options: RecordToolCallOptions) {\n // Prefer this over `send_message({ event_type: 'tool_call' })`. The event\n // carries both an event_type and a role and the two have to agree;\n // getting the role wrong was silent, and our own published example got it\n // wrong. Here you cannot: the method sets both.\n await namespace.send_message({\n content: '',\n role: 'assistant',\n event_type: 'tool_call',\n tool_name: options.tool_name ?? options.toolName ?? '',\n ...(options.tool_args ?? options.toolArgs\n ? { tool_args: options.tool_args ?? options.toolArgs }\n : {}),\n tool_call_id: options.tool_call_id ?? options.toolCallId ?? '',\n conversation_id: options.conversation_id ?? options.conversationId ?? '',\n user_id: options.user_id ?? options.userId ?? '',\n customer_id: options.customer_id ?? options.customerId ?? '',\n session_id: options.session_id ?? options.sessionId ?? '',\n metadata: options.metadata ?? {},\n });\n },\n\n async record_tool_result(options: RecordToolResultOptions) {\n // ⚠ A tool result is usually the caller's customer data. It is an\n // anticipation hint and is not written into long-term memory, but it does\n // reach us: send what the agent needs, not the whole row.\n const conversationId = options.conversation_id ?? options.conversationId ?? '';\n if (conversationId) {\n onToolResult?.({ conversationId, result: options.result, toolName: options.tool_name ?? options.toolName ?? '' });\n }\n await namespace.send_message({\n content: '',\n role: 'tool',\n event_type: 'tool_result',\n tool_result: options.result,\n tool_name: options.tool_name ?? options.toolName ?? '',\n tool_call_id: options.tool_call_id ?? options.toolCallId ?? '',\n conversation_id: options.conversation_id ?? options.conversationId ?? '',\n user_id: options.user_id ?? options.userId ?? '',\n customer_id: options.customer_id ?? options.customerId ?? '',\n session_id: options.session_id ?? options.sessionId ?? '',\n metadata: options.metadata ?? {},\n });\n },\n\n async record_thinking(options: RecordThinkingOptions) {\n if (client === null) throw new ListeningNotActiveError('Listening is not active');\n const conversationId = options.conversation_id ?? options.conversationId ?? '';\n ensureSession(\n conversationId,\n options.user_id ?? options.userId ?? '',\n options.customer_id ?? options.customerId ?? '',\n );\n client.sendConversationEvent({\n // Python emits reasoning as an `agent_thinking` conversation event.\n event_type: 'agent_thinking',\n content: options.content,\n // Deliberately no role. A thought is not the assistant's reply, and\n // this went out as 'assistant' from the day it shipped: the server\n // checked role before event_type, so every reasoning step any client\n // sent was filed as the final answer and the reasoning path never ran.\n // Sending no role is correct against both the fixed server and one that\n // has not been updated yet.\n role: '',\n conversation_id: options.conversation_id ?? options.conversationId ?? '',\n user_id: options.user_id ?? options.userId ?? '',\n customer_id: options.customer_id ?? options.customerId ?? '',\n session_id: options.session_id ?? options.sessionId ?? '',\n metadata: thinkingMetadata(options),\n timestamp_ms: Date.now(),\n });\n },\n\n get is_listening() {\n return client !== null && client.isConnected;\n },\n\n _internal: internals,\n };\n\n return namespace;\n}\n","/**\n * Kept in sync with package.json by the release workflow, in the\n * \"Sync SDK_VERSION to the resolved version\" step of publish-npm-js-sdk.yml.\n *\n * Until that step existed nothing kept it in sync, so this constant sat at\n * 1.0.0-beta.0 while the package shipped 0.4.x, and every request's User-Agent\n * and every telemetry event reported a version that was never released.\n *\n * ⚠ The workflow only covers a workflow release. 0.4.8 was published by hand\n * and shipped reporting 0.4.7, so every request from it carries a User-Agent\n * for a version it is not. `version-matches-package.test.ts` now fails the\n * build when the two disagree, which is the only thing that makes a manual\n * publish safe.\n */\nexport const SDK_VERSION = '0.5.9';\n","/**\n * Client registry: one shared client per identity, per process.\n *\n * Mirrors Python's `registry.py`. Without it, every `new SynapClient()` gets\n * its own anticipation cache, so a framework integration that constructs a\n * client per request or per agent never gets a cache hit and pays for a fresh\n * retrieval every time. Python has shared state here since 0.2; matching it is\n * the difference between a warm cache and a metered fetch on every turn.\n *\n * JavaScript reaches the same end differently. Python does\n * `self.__dict__ = existing.__dict__`, aliasing state between distinct\n * objects. A JS constructor can simply RETURN the existing instance, so the\n * two callers hold the same object rather than two objects sharing a state\n * bag. That is stricter than Python and avoids its `__dict__`-identity\n * comparisons entirely.\n */\n\n/** Kept structural so the registry does not depend on the client's type. */\nexport interface RegistrableClient {\n shutdown(): Promise<void>;\n}\n\nconst instances = new Map<string, RegistrableClient>();\n\n/**\n * Which registry slot an identity belongs in.\n *\n * `instanceId` keys the singleton when present, but it is usually empty at\n * construction and only resolved from the API key during `initialize()`.\n * Keying on it directly would put every `new SynapClient({ apiKey })` in one\n * process onto the single `''` slot, where the second would adopt the first's\n * credentials. So fall back to the credential that WILL resolve the identity,\n * stored as a truncated SHA-256 digest rather than plaintext, so a registry\n * dump or a log line cannot leak a key.\n *\n * `SYNAP_INSTANCE_ID` is deliberately not consulted: the client applies that\n * fallback after the lookup, and honouring it here would put two different\n * credentials back onto one slot whenever the variable happens to be set.\n */\nexport function buildRegistryKey(instanceId: string, apiKey: string | undefined): string {\n if (instanceId) return instanceId;\n const key = apiKey ?? '';\n // Nothing to key on. The client cannot initialise without a credential\n // anyway, so preserve the empty slot rather than inventing one.\n if (!key) return '';\n return `apikey:${digest(key)}`;\n}\n\n/**\n * 64-bit FNV-1a over the credential.\n *\n * Python uses SHA-256 here. This cannot: the digest is needed synchronously\n * inside a constructor, `node:crypto` is unavailable on Edge and Workers (and\n * a static import of it would fail those builds outright), and Web Crypto's\n * `subtle.digest` is async. So this is a non-cryptographic digest, chosen for\n * the one property that actually matters here: the raw key never becomes a Map\n * key that something could enumerate or log.\n *\n * It is not collision-resistant against an adversary, and it does not need to\n * be. A collision would make two clients share state, so the bound that\n * matters is accidental collision between the handful of distinct API keys\n * present in a single process, which at 64 bits is negligible. It is never\n * persisted, never sent anywhere, and never compared against anything outside\n * this module.\n */\nfunction digest(value: string): string {\n // FNV-1a, 64-bit. BigInt keeps the arithmetic exact; Number would lose\n // precision past 2^53 and collapse the space.\n const PRIME = 1099511628211n;\n const MASK = 0xffffffffffffffffn;\n let hash = 14695981039346656037n;\n for (let i = 0; i < value.length; i += 1) {\n hash ^= BigInt(value.charCodeAt(i));\n hash = (hash * PRIME) & MASK;\n }\n return hash.toString(16).padStart(16, '0');\n}\n\nexport function get(key: string): RegistrableClient | undefined {\n return instances.get(key);\n}\n\n/**\n * Claim `key` for `client`, or hand back whoever already holds it.\n *\n * A separate `get` then `set` would let two callers constructing the first\n * client for one identity both miss the lookup, with the second overwriting\n * the first. JavaScript's single-threaded execution makes that impossible\n * between two synchronous statements, so this is a single operation for\n * clarity rather than for locking: there is no equivalent of Python's mutex\n * to hold, and none is needed.\n *\n * Returns `undefined` when `client` won the slot, or the incumbent when not.\n */\nexport function registerIfAbsent(\n key: string,\n client: RegistrableClient,\n): RegistrableClient | undefined {\n const existing = instances.get(key);\n if (existing !== undefined) return existing;\n instances.set(key, client);\n return undefined;\n}\n\n/**\n * Point `key` at `client` as well, unless the slot is taken.\n *\n * Used once `initialize()` resolves the real instance id. The client was keyed\n * on its credential because the id did not exist yet, so a later\n * `new SynapClient({ instanceId })` for that same instance would miss and\n * build a second client: two anticipation caches and two Listen streams for\n * one instance. The credential slot is KEPT rather than moved, so constructing\n * by API key keeps returning the same client too.\n *\n * An occupied target is never clobbered. Evicting a live client is worse than\n * the duplication it would fix.\n */\nexport function aliasIfAbsent(key: string, client: RegistrableClient): boolean {\n if (!key || instances.has(key)) return false;\n instances.set(key, client);\n return true;\n}\n\n/**\n * Drop `key`, but only if it still points at `client`.\n *\n * A shutting-down client must not evict whoever currently holds its old slot.\n */\nexport function unregisterIfOwner(key: string, client: RegistrableClient): void {\n if (instances.get(key) === client) instances.delete(key);\n}\n\n/** Test seam. Never call this from library code. */\nexport function clear(): void {\n instances.clear();\n}\n\n/** Diagnostics only. */\nexport function size(): number {\n return instances.size;\n}\n","/**\n * Per-conversation short-term context, held locally.\n *\n * Mirrors Python's `cache/short_term_store.py`. This is what makes reads see\n * your own writes: a turn recorded a moment ago is buffered here and spliced\n * onto the next fetch, so a fast follow-up does not retrieve context that omits\n * what the user just said. Python's own comment on the overlay puts it plainly:\n * the pre-compaction window \"is exactly when the playground's fast follow-ups\n * happen.\"\n *\n * LRU by conversation, with an absolute age cap. Everything here is per process\n * and holds only what this process wrote, so nothing is authoritative: the\n * server's view wins for the summary fields, and this only ever contributes the\n * verbatim tail.\n *\n * Holding an entry is not the same as holding the conversation. A process that\n * restarted, evicted the entry, or shares the conversation with another process\n * has only the turns it recorded itself. So the server reports how many\n * messages it holds, on every write and on every compaction bundle, and\n * `isComplete()` says whether the entry can answer on its own. When it cannot,\n * the caller asks the server and hands the answer to `reseed`.\n */\n\nimport type { TurnStore } from './turn-store.js';\n\nconst DEFAULT_MAX_CONVERSATIONS = 100;\n/** 12 hours, as in Python. */\nconst DEFAULT_MAX_AGE_MS = 12 * 60 * 60 * 1000;\n\nexport interface ShortTermTurn {\n role: string;\n content: string;\n /** ISO-8601. */\n timestamp: string;\n /** The server's id for the turn, when the write returned one. */\n message_id?: string;\n}\n\n/**\n * How every v3 context_block begins (server: compaction/v3/render.py). A\n * summary that begins this way IS the block, which is how a REST-fed entry is\n * recognised. Same constant as Python's `CONTEXT_BLOCK_PREFIX`.\n */\nexport const CONTEXT_BLOCK_PREFIX = '## Conversation so far (compacted';\n\n/** The v3 block in a conversation_context payload, or null (v2 server). */\nexport function contextBlockOf(cc: Record<string, unknown>): string | null {\n const block = cc['context_block'];\n if (typeof block === 'string' && block) return block;\n const summary = cc['summary'];\n if (typeof summary === 'string' && summary.startsWith(CONTEXT_BLOCK_PREFIX)) return summary;\n return null;\n}\n\nexport interface ShortTermEntry {\n conversationId: string;\n summary: string | null;\n /**\n * Compaction v3: the server's one framed block, printed as received and\n * once. When set, the paragraphs are not used (the server sends them empty).\n */\n contextBlock: string | null;\n factualParagraph: string | null;\n conversationalParagraph: string | null;\n currentState: Record<string, unknown>;\n keyExtractions: Record<string, unknown>;\n compactionId: string | null;\n compactedAt: string | null;\n endTimestamp: string | null;\n recentTurns: ShortTermTurn[];\n lastActivityAt: number;\n // --- Completeness bookkeeping. See `isComplete`. ---\n /** Messages the cached summary covers, from the start of the conversation. */\n compactedMessageCount: number;\n /** The highest message count the server has reported. null = never heard. */\n serverMessageCount: number | null;\n /**\n * The server has confirmed at least one write made by this process. A\n * process that only reads never hears a count again after it is seeded, so\n * it could not notice the conversation moving on without it.\n */\n participant: boolean;\n /** At some point this entry held everything the server held. */\n synced: boolean;\n /**\n * The server's count ran ahead of an entry that was in sync, so something\n * else is writing to this conversation. Latched: this process cannot see\n * those writes when they happen, only at its own next write.\n */\n shared: boolean;\n}\n\n/** What a write learned from the server about the turn and the conversation. */\nexport interface TurnConfirmation {\n /** The server's id for the turn. A turn already held under it is not appended twice. */\n messageId?: string | undefined;\n /** The conversation's message count as the server reported it on this write. */\n serverMessageCount?: number | null | undefined;\n /** Other writes this process still has outstanding for the conversation. */\n inFlight?: number | undefined;\n}\n\n/** What `/context-for-prompt` returned, as far as `reseed` needs it. */\nexport interface ServerPromptState {\n compactionId: string | null;\n recentMessages: ShortTermTurn[];\n compactedMessageCount: number;\n totalMessageCount: number;\n}\n\nexport function localMessageCount(entry: ShortTermEntry): number {\n return entry.compactedMessageCount + entry.recentTurns.length;\n}\n\n/**\n * Whether an entry can stand in for the server's short-term context.\n *\n * True when the server holds nothing this entry lacks. Holding MORE than the\n * server has confirmed is fine: those are this process's own turns, sent and\n * not yet counted.\n */\nexport function isComplete(entry: ShortTermEntry): boolean {\n return (\n entry.participant &&\n entry.synced &&\n !entry.shared &&\n entry.serverMessageCount !== null &&\n entry.serverMessageCount <= localMessageCount(entry)\n );\n}\n\n/**\n * Whether `turns` already holds `turn`.\n *\n * By message id where both sides have one. Otherwise by role and text: a\n * streamed turn has no id (the server assigns it in a background write), and\n * the turns in a `fetch` response carry none either.\n */\nfunction heldIn(turn: ShortTermTurn, turns: ShortTermTurn[]): boolean {\n return turns.some((held) =>\n turn.message_id && held.message_id\n ? held.message_id === turn.message_id\n : held.role === turn.role && held.content === turn.content,\n );\n}\n\n/**\n * The local turns that come after everything in `serverTurns`.\n *\n * Walks back from the newest local turn and stops at the first one the\n * server's list already holds. What is left is what this process wrote or sent\n * after the server's list was built.\n *\n * A turn the server rewrote before storing it (a privacy policy) does not\n * match its local copy by text, so it can come back from here as well as from\n * the server. That shows one turn twice; it never loses one.\n */\nexport function turnsAfter(\n localTurns: readonly ShortTermTurn[],\n serverTurns: ShortTermTurn[],\n): ShortTermTurn[] {\n const newer: ShortTermTurn[] = [];\n for (let i = localTurns.length - 1; i >= 0; i -= 1) {\n const turn = localTurns[i] as ShortTermTurn;\n if (heldIn(turn, serverTurns)) break;\n newer.unshift(turn);\n }\n return newer;\n}\n\nexport interface ShortTermStoreOptions {\n maxConversations?: number;\n maxAgeMs?: number;\n /** Injected in tests. Wall clock, because entries are compared to ISO stamps. */\n now?: () => number;\n}\n\n/** Parse an ISO-8601 stamp to epoch millis, or null. */\nexport function parseIso(value: unknown): number | null {\n if (value === null || value === undefined || value === '') return null;\n if (value instanceof Date) return value.getTime();\n if (typeof value !== 'string') return null;\n const parsed = Date.parse(value);\n return Number.isNaN(parsed) ? null : parsed;\n}\n\n/**\n * True when a turn is strictly after the cutoff.\n *\n * An unparseable timestamp KEEPS the turn. Dropping it would silently lose a\n * message, which is worse than briefly showing one the server has already\n * folded into its summary.\n */\nfunction isAfter(turnTimestamp: unknown, cutoffMs: number): boolean {\n const parsed = parseIso(turnTimestamp);\n if (parsed === null) return true;\n return parsed > cutoffMs;\n}\n\nexport class ShortTermStore {\n // Insertion order IS the LRU order: re-inserting on touch moves an entry to\n // the end, so the first key Map yields is the least recently used.\n readonly #entries = new Map<string, ShortTermEntry>();\n readonly #maxConversations: number;\n readonly #maxAgeMs: number;\n readonly #now: () => number;\n\n /**\n * Every turn appended here is also kept in the archive (a TurnStore), which\n * a compaction does not prune: the turns it covers stay searchable locally\n * (history-search.ts). Python: `ShortTermContextStore.attach_archive`.\n */\n #archive: TurnStore | null = null;\n\n constructor(options: ShortTermStoreOptions = {}) {\n this.#maxConversations = options.maxConversations ?? DEFAULT_MAX_CONVERSATIONS;\n this.#maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;\n this.#now = options.now ?? (() => Date.now());\n }\n\n attachArchive(archive: TurnStore | null): void {\n this.#archive = archive;\n }\n\n get archive(): TurnStore | null {\n return this.#archive;\n }\n\n /** The entry, or null. Age-evicts on read, as Python does. */\n get(conversationId: string): ShortTermEntry | null {\n if (!conversationId) return null;\n const entry = this.#entries.get(conversationId);\n if (entry === undefined) return null;\n if (this.#now() - entry.lastActivityAt > this.#maxAgeMs) {\n this.#entries.delete(conversationId);\n return null;\n }\n this.#touch(conversationId, entry);\n return entry;\n }\n\n has(conversationId: string): boolean {\n return this.get(conversationId) !== null;\n }\n\n get size(): number {\n return this.#entries.size;\n }\n\n /**\n * Append a raw turn.\n *\n * Called from `record_message` and the stream's `send_message`. When no entry\n * exists yet (the first turn, before any compaction has arrived) a fresh one\n * is created with an empty summary; the next `compaction_update` fills the\n * summary fields in and prunes the turns it covers.\n *\n * `confirmation` is what the write learned from the server. See\n * `TurnConfirmation` and `noteServerCount`.\n */\n appendTurn(\n conversationId: string,\n role: string,\n content: string,\n timestamp?: string,\n confirmation: TurnConfirmation = {},\n ): void {\n if (!conversationId) return;\n const stamp = timestamp ?? new Date(this.#now()).toISOString();\n if (this.#archive !== null) {\n try {\n this.#archive.add(conversationId, role, content, stamp, { messageId: confirmation.messageId ?? null });\n } catch {\n // The archive never blocks the prompt path.\n }\n }\n const entry = this.#entries.get(conversationId) ?? this.#blank(conversationId);\n const messageId = confirmation.messageId;\n const alreadyHeld =\n Boolean(messageId) && entry.recentTurns.some((t) => t.message_id === messageId);\n if (!alreadyHeld) {\n const turn: ShortTermTurn = { role, content, timestamp: stamp };\n if (messageId) turn.message_id = messageId;\n entry.recentTurns.push(turn);\n }\n const count = confirmation.serverMessageCount;\n if (count !== null && count !== undefined) {\n this.#noteCount(entry, Number(count), confirmation.inFlight ?? 0);\n }\n entry.lastActivityAt = parseIso(stamp) ?? this.#now();\n this.#touch(conversationId, entry);\n this.#evict();\n }\n\n /**\n * Record the server's message count for a conversation.\n *\n * The streamed write path's half of `appendTurn`'s confirmation: a streamed\n * turn is appended when it is sent, and the count arrives later, once the\n * server has written it. A count for a conversation this process holds\n * nothing for is ignored.\n */\n noteServerCount(conversationId: string, count: number): void {\n if (!conversationId) return;\n const entry = this.#entries.get(conversationId);\n if (entry !== undefined) this.#noteCount(entry, Number(count), 0);\n }\n\n /**\n * Compare the server's count with what the entry holds.\n *\n * The count is taken after one of this process's own writes, so it proves\n * the process takes part in the conversation.\n *\n * Server ahead of an entry that was in sync: something else wrote, and the\n * entry is marked shared for good. Server ahead of an entry that was never\n * in sync: this process joined late (a restart, an eviction), and `reseed`\n * will fix it. `inFlight` covers this process's own writes that the server\n * has counted and the entry has not appended yet, so two concurrent writes\n * do not look like a second writer.\n */\n #noteCount(entry: ShortTermEntry, count: number, inFlight: number): void {\n entry.participant = true;\n if (count > localMessageCount(entry) + inFlight) {\n if (entry.synced) entry.shared = true;\n } else if (entry.serverMessageCount === null) {\n // First word from the server, and nothing is missing.\n entry.synced = true;\n }\n entry.serverMessageCount = Math.max(entry.serverMessageCount ?? 0, count);\n }\n\n /**\n * Apply a `compaction_update` bundle.\n *\n * Replaces the summary fields from the bundle's `conversation_context` and\n * drops the buffered turns the new summary covers.\n *\n * Which turns those are is decided by count, not by clock. The bundle says\n * how many messages the summary covers and how many the conversation held\n * when the bundle was built, so the last `total - compacted` of its\n * `recent_turns` are exactly the uncovered messages. Turns this process\n * appended after the server took that count are kept behind them. Timestamps\n * cannot do this job: the REST write returns the time of the response and\n * not of the stored row, a caller may supply its own message time, and a\n * streamed turn is stamped when a background write runs.\n *\n * A bundle from a server that sends no counts falls back to the older\n * timestamp prune, and the entry is left out of sync so it is never used to\n * answer on its own.\n */\n applyCompaction(bundle: Record<string, unknown>): void {\n if (typeof bundle !== 'object' || bundle === null) return;\n const cc = (bundle['conversation_context'] ?? {}) as Record<string, unknown>;\n const conversationId = String(\n cc['conversation_id'] ??\n bundle['anticipation_conversation_id'] ??\n bundle['conversation_id'] ??\n '',\n );\n if (!conversationId) return;\n\n const endTs = parseIso(cc['end_timestamp'] ?? cc['compacted_at']);\n const compactedAt = parseIso(cc['compacted_at']);\n const compactedCount = Number(cc['compacted_message_count'] ?? 0) || 0;\n const totalCount = Number(cc['total_message_count'] ?? 0) || 0;\n const entry = this.#entries.get(conversationId) ?? this.#blank(conversationId);\n\n const heldAt = parseIso(entry.compactedAt);\n // An older compaction must not replace a newer one.\n if (compactedAt !== null && heldAt !== null && compactedAt < heldAt) return;\n\n // `x || existing` throughout, matching Python: a blank field from the\n // server must not erase what is already held.\n const block = contextBlockOf(cc);\n if (block) {\n // A v3 compaction replaces the whole record. Keeping a v2 paragraph\n // cached from an earlier push would print it next to the block, so the\n // paragraphs are cleared, not kept.\n entry.contextBlock = block;\n entry.summary = (cc['summary'] as string) || block;\n entry.factualParagraph = null;\n entry.conversationalParagraph = null;\n } else {\n entry.summary = (cc['summary'] as string) || entry.summary;\n entry.factualParagraph = (cc['factual_paragraph'] as string) || entry.factualParagraph;\n entry.conversationalParagraph =\n (cc['conversational_paragraph'] as string) || entry.conversationalParagraph;\n }\n entry.currentState = { ...((cc['current_state'] as Record<string, unknown>) ?? {}) };\n entry.keyExtractions = { ...((cc['key_extractions'] as Record<string, unknown>) ?? {}) };\n entry.compactionId = (cc['compaction_id'] as string) || entry.compactionId;\n entry.compactedAt = (cc['compacted_at'] as string) || entry.compactedAt;\n entry.endTimestamp = (cc['end_timestamp'] as string) || entry.endTimestamp;\n\n if (compactedCount > 0 && compactedCount <= totalCount) {\n const uncovered = totalCount - compactedCount;\n const serverTurns = Array.isArray(cc['recent_turns'])\n ? (cc['recent_turns'] as ShortTermTurn[])\n : [];\n const tail: ShortTermTurn[] =\n uncovered > 0 ? serverTurns.slice(-uncovered).map((t) => ({ ...t })) : [];\n // Turns appended here after the server counted. Only an entry that was\n // in sync knows how many of its own turns those are.\n const ahead = localMessageCount(entry) - totalCount;\n const newer =\n entry.synced && ahead > 0\n ? entry.recentTurns.slice(-ahead).filter((t) => !heldIn(t, tail))\n : [];\n entry.recentTurns = [...tail, ...newer];\n entry.compactedMessageCount = compactedCount;\n entry.serverMessageCount = Math.max(entry.serverMessageCount ?? 0, totalCount);\n // A short tail means the bundle did not carry every uncovered message,\n // so the entry cannot claim to hold them.\n entry.synced = tail.length === uncovered;\n } else {\n if (endTs !== null && entry.recentTurns.length > 0) {\n entry.recentTurns = entry.recentTurns.filter((t) => isAfter(t.timestamp, endTs));\n }\n entry.synced = false;\n }\n entry.lastActivityAt = this.#now();\n this.#touch(conversationId, entry);\n this.#evict();\n }\n\n /**\n * Bring an entry back in step from a `/context-for-prompt` response.\n *\n * Called after the server was asked because the entry was not complete. The\n * response lists every message the server's summary does not cover, which is\n * what `recentTurns` is meant to hold.\n *\n * Only an entry that already exists is reseeded. Creating one here would let\n * a process that never writes to the conversation answer from a snapshot it\n * has no way to refresh.\n *\n * If the server's summary is not the one cached here, the entry cannot\n * render it, so it stays out of sync until the next bundle brings the\n * summary itself.\n *\n * Turns held locally and absent from the response are kept when they come\n * after everything the response has: they were written or sent after the\n * server read its messages.\n */\n reseed(conversationId: string, state: ServerPromptState): void {\n if (!conversationId) return;\n const entry = this.#entries.get(conversationId);\n if (entry === undefined || entry.shared) return;\n entry.serverMessageCount = Math.max(\n entry.serverMessageCount ?? 0,\n Number(state.totalMessageCount) || 0,\n );\n if ((state.compactionId || null) !== entry.compactionId) {\n entry.synced = false;\n return;\n }\n const serverTurns = state.recentMessages.map((m) => ({ ...m }));\n entry.recentTurns = [...serverTurns, ...turnsAfter(entry.recentTurns, serverTurns)];\n entry.compactedMessageCount = Number(state.compactedMessageCount) || 0;\n entry.synced = true;\n }\n\n invalidate(conversationId: string): void {\n this.#entries.delete(conversationId);\n }\n\n clear(): void {\n this.#entries.clear();\n }\n\n #blank(conversationId: string): ShortTermEntry {\n return {\n conversationId,\n summary: null,\n contextBlock: null,\n factualParagraph: null,\n conversationalParagraph: null,\n currentState: {},\n keyExtractions: {},\n compactionId: null,\n compactedAt: null,\n endTimestamp: null,\n recentTurns: [],\n lastActivityAt: this.#now(),\n compactedMessageCount: 0,\n serverMessageCount: null,\n participant: false,\n synced: false,\n shared: false,\n };\n }\n\n /** Re-insert so this key becomes the most recently used. */\n #touch(conversationId: string, entry: ShortTermEntry): void {\n this.#entries.delete(conversationId);\n this.#entries.set(conversationId, entry);\n }\n\n #evict(): void {\n while (this.#entries.size > this.#maxConversations) {\n const oldest = this.#entries.keys().next();\n if (oldest.done === true) break;\n this.#entries.delete(oldest.value);\n }\n }\n}\n","/**\n * Local store of a conversation's raw turns, for search after compaction.\n *\n * Port of `maximem_synap/cache/turn_store.py`. The short-term store keeps\n * only the turns a compaction has not covered yet; this store keeps every\n * turn this process saw or fetched, so the details a compaction leaves out\n * can be found again by a local search (history-search.ts).\n *\n * Python keeps it in a SQLite file under ~/.synap. This one holds it in\n * memory, with the same limits and the same answers, because the SDK also\n * runs in browsers and edge runtimes, and the main entry may not load a node\n * builtin eagerly (bundle-hygiene.test.ts). A turn this process did not see\n * arrives through `addMany` from the turns route, with its position.\n */\n\nimport { HISTORY_SEARCH } from '../behavior/contract.js';\n\nexport interface StoreLimits {\n max_conversations: number;\n max_turns_per_conversation: number;\n max_bytes_per_conversation: number;\n idle_ttl_seconds: number;\n max_file_bytes: number;\n}\n\nexport interface StoredTurn {\n /** Insertion order in this store. */\n seq: number;\n role: string;\n content: string;\n timestamp: string;\n message_id: string | null;\n /** 1-based turn number on the server, when known. */\n position: number | null;\n}\n\nexport interface TurnInput {\n role?: string;\n content?: string;\n timestamp?: string | Date | null;\n message_id?: string | null;\n position?: number | string | null;\n}\n\nconst SWEEP_EVERY_WRITES = 50;\n\ninterface Conversation {\n lastActivity: number;\n turns: StoredTurn[];\n}\n\nfunction bytesOf(text: string): number {\n return new TextEncoder().encode(text).length;\n}\n\nexport class TurnStore {\n readonly limits: StoreLimits;\n readonly onDisk = false;\n #now: () => number;\n #seq = 0;\n #writes = 0;\n #conversations = new Map<string, Conversation>();\n #vectors = new Map<string, Map<number, Float32Array>>();\n\n constructor(options: { limits?: StoreLimits; now?: () => number } = {}) {\n this.limits = options.limits ?? { ...HISTORY_SEARCH.store_limits };\n this.#now = options.now ?? (() => Date.now() / 1000);\n }\n\n add(\n conversationId: string,\n role: string,\n content: string,\n timestamp?: string | null,\n options: { messageId?: string | null; position?: number | null } = {},\n ): void {\n this.addMany(conversationId, [\n { role, content, timestamp: timestamp ?? '', message_id: options.messageId ?? null, position: options.position ?? null },\n ]);\n }\n\n /**\n * Insert turns not held yet; returns how many were new. A turn held without\n * an id or position (a streamed write) gets them when the same role and\n * text come back from the server.\n */\n addMany(conversationId: string, turns: Iterable<TurnInput>): number {\n if (!conversationId) return 0;\n const conv = this.#conversations.get(conversationId) ?? { lastActivity: 0, turns: [] };\n let added = 0;\n for (const turn of turns) {\n const role = String(turn.role || 'user');\n const content = String(turn.content ?? '');\n if (!content) continue;\n const messageId = turn.message_id ? String(turn.message_id) : null;\n const raw = turn.position;\n const position =\n typeof raw === 'number' && Number.isInteger(raw) ? raw : /^\\d+$/.test(String(raw ?? '')) ? Number(raw) : null;\n const ts = turn.timestamp instanceof Date ? turn.timestamp.toISOString() : String(turn.timestamp ?? '');\n if (messageId !== null) {\n const held = conv.turns.find((t) => t.message_id === messageId);\n if (held !== undefined) {\n if (position !== null) held.position = position;\n continue;\n }\n }\n const same = conv.turns.find(\n (t) => t.role === role && t.content === content && (t.message_id === null || t.position === null),\n );\n if (same !== undefined) {\n same.message_id = same.message_id ?? messageId;\n same.position = position ?? same.position;\n continue;\n }\n conv.turns.push({ seq: ++this.#seq, role, content, timestamp: ts, message_id: messageId, position });\n added += 1;\n }\n conv.lastActivity = this.#now();\n this.#conversations.set(conversationId, conv);\n this.#trim(conv);\n this.#writes += 1;\n if (this.#writes % SWEEP_EVERY_WRITES === 0) this.sweep();\n return added;\n }\n\n #trim(conv: Conversation): void {\n const limits = this.limits;\n let size = conv.turns.reduce((n, t) => n + bytesOf(t.content), 0);\n while (conv.turns.length > limits.max_turns_per_conversation || size > limits.max_bytes_per_conversation) {\n const excess = Math.max(conv.turns.length - limits.max_turns_per_conversation, 1);\n const order = [...conv.turns].sort(\n (a, b) => (a.position ?? Number.MAX_SAFE_INTEGER) - (b.position ?? Number.MAX_SAFE_INTEGER) || a.seq - b.seq,\n );\n const gone = new Set(order.slice(0, excess).map((t) => t.seq));\n if (gone.size === 0) break;\n for (const t of conv.turns) if (gone.has(t.seq)) size -= bytesOf(t.content);\n conv.turns = conv.turns.filter((t) => !gone.has(t.seq));\n for (const vectors of this.#vectors.values()) for (const seq of gone) vectors.delete(seq);\n }\n }\n\n /** Idle conversations, then the least recently used beyond the count, then more until the bytes fit. */\n sweep(): void {\n const limits = this.limits;\n const now = this.#now();\n for (const [id, conv] of this.#conversations) {\n if (conv.lastActivity < now - limits.idle_ttl_seconds) this.drop(id);\n }\n const byRecent = [...this.#conversations.entries()].sort((a, b) => b[1].lastActivity - a[1].lastActivity);\n for (const [id] of byRecent.slice(limits.max_conversations)) this.drop(id);\n while (this.sizeBytes() > limits.max_file_bytes) {\n const oldest = this.conversations()[0];\n if (oldest === undefined) break;\n this.drop(oldest);\n }\n }\n\n drop(conversationId: string): void {\n const conv = this.#conversations.get(conversationId);\n if (conv === undefined) return;\n for (const vectors of this.#vectors.values()) for (const t of conv.turns) vectors.delete(t.seq);\n this.#conversations.delete(conversationId);\n }\n\n clear(): void {\n this.#conversations.clear();\n this.#vectors.clear();\n }\n\n close(): void {\n // Nothing to release in memory; kept for parity with the Python store.\n }\n\n /** In conversation order: by server position where known, else as written here. */\n turns(conversationId: string): StoredTurn[] {\n const conv = this.#conversations.get(conversationId);\n if (conv === undefined) return [];\n const bySeq = [...conv.turns].sort((a, b) => a.seq - b.seq);\n const key = new Map<number, number>();\n let top = 0;\n let step = 0;\n for (const t of bySeq) {\n if (t.position !== null) {\n key.set(t.seq, t.position);\n top = Math.max(top, t.position);\n step = 0;\n } else {\n step += 1;\n key.set(t.seq, top + step * 1e-6);\n }\n }\n return bySeq\n .sort((a, b) => (key.get(a.seq) ?? 0) - (key.get(b.seq) ?? 0) || a.seq - b.seq)\n .map((t) => ({ ...t }));\n }\n\n /** Turns the server also holds: archived tool results are local only. Python: `count`. */\n count(conversationId: string): number {\n return (this.#conversations.get(conversationId)?.turns ?? []).filter((t) => t.role !== 'tool').length;\n }\n\n sizeBytes(): number {\n let n = 0;\n for (const conv of this.#conversations.values()) for (const t of conv.turns) n += bytesOf(t.content);\n return n;\n }\n\n /** Conversation ids, least recently active first. */\n conversations(): string[] {\n return [...this.#conversations.entries()].sort((a, b) => a[1].lastActivity - b[1].lastActivity).map(([id]) => id);\n }\n\n vectors(seqs: readonly number[], model: string): Map<number, Float32Array> {\n const held = this.#vectors.get(model);\n const out = new Map<number, Float32Array>();\n if (held === undefined) return out;\n for (const seq of seqs) {\n const v = held.get(seq);\n if (v !== undefined) out.set(seq, v);\n }\n return out;\n }\n\n putVectors(model: string, items: Map<number, Float32Array>): void {\n const held = this.#vectors.get(model) ?? new Map<number, Float32Array>();\n for (const [seq, vec] of items) held.set(seq, vec);\n this.#vectors.set(model, held);\n }\n}\n","/**\n * Local search over a conversation's earlier turns.\n *\n * Port of `maximem_synap/cache/history_search.py`; see it for the design.\n * Cascade, cheapest first: literal values in the query, then BM25 (the\n * anticipation cache's tokenizer and parameters), then an embedding model\n * when one is supplied. Ranks are fused by reciprocal rank with a small recency prior, and\n * excerpts are shown in conversation order. The rendered block reaches the\n * LLM prompt and is byte-identical to Python's.\n */\n\nimport { HISTORY_SEARCH } from '../behavior/contract.js';\nimport { BM25, tokenize as latinTokenize } from './bm25.js';\nimport type { StoredTurn, TurnStore } from './turn-store.js';\n\n/** A local embedding model. `model` names it, so vectors from another model are never compared. */\nexport interface Encoder {\n model: string;\n encode(texts: string[]): number[][] | Promise<number[][]>;\n /** Optional: a model trained with a query prefix (e5) encodes queries here and turns with `encode`. */\n encodeQuery?(texts: string[]): number[][] | Promise<number[][]>;\n}\n\nexport interface Excerpt {\n position: number | null;\n role: string;\n text: string;\n score: number;\n timestamp: string;\n /** \"literal\", \"bm25\" or \"embedding\": the strongest signal that found it. */\n via: string;\n}\n\n// Any script's letters and digits; Latin words still go through the\n// anticipation tokenizer for its stop words and stemming. Python: `tokenize`.\nconst WORD_RE = /[\\p{L}\\p{N}\\p{M}]+/gu;\n\nconst FORMS: Readonly<Record<string, string>> = HISTORY_SEARCH.irregular_forms ?? {};\n\nfunction tokenize(text: string): string[] {\n const out: string[] = [];\n for (const word of (text || '').toLowerCase().match(WORD_RE) ?? []) {\n // \"sold\" and \"sell\" are one word to a search; the stemmer only strips regular endings.\n if (/^[\\x00-\\x7f]+$/.test(word)) out.push(...latinTokenize(FORMS[word] ?? word));\n else out.push(word);\n }\n return out;\n}\n\nconst LITERAL_SOURCE = String.raw`\"([^\"]{3,})\"|\\b([A-Z0-9][A-Z0-9-]{3,})\\b|(\\d[\\d,.:/-]*\\d|\\d)`;\nconst SENTENCE_RE = /(?<=[.!?])\\s+/;\n\nfunction literalsOf(query: string): string[] {\n const out: string[] = [];\n for (const m of (query || '').matchAll(new RegExp(LITERAL_SOURCE, 'g'))) {\n const quoted = m[1];\n const value = quoted || m[2] || m[3];\n if (value && (quoted || /\\d/.test(value))) out.push(value.toLowerCase());\n }\n return out;\n}\n\nfunction cosine(a: ArrayLike<number>, b: ArrayLike<number>): number {\n let dot = 0;\n let na = 0;\n let nb = 0;\n const n = Math.min(a.length, b.length);\n for (let i = 0; i < n; i++) {\n dot += a[i]! * b[i]!;\n na += a[i]! * a[i]!;\n nb += b[i]! * b[i]!;\n }\n return na && nb ? dot / (Math.sqrt(na) * Math.sqrt(nb)) : 0;\n}\n\nfunction words(text: string): string[] {\n return text.split(/\\s+/).filter(Boolean);\n}\n\nfunction excerptOf(content: string, terms: Set<string>, literals: string[], maxWords: number): string {\n let sentences = content.trim().split(SENTENCE_RE).filter((s) => s.trim());\n if (sentences.length === 0) sentences = [content.trim()];\n const score = (sentence: string): number => {\n const low = sentence.toLowerCase();\n const held = new Set(tokenize(sentence));\n let n = 0;\n for (const t of terms) if (held.has(t)) n += 1;\n for (const lit of literals) if (low.includes(lit)) n += 2;\n return n;\n };\n let best = 0;\n for (let i = 1; i < sentences.length; i++) if (score(sentences[i]!) > score(sentences[best]!)) best = i;\n let lo = best;\n let hi = best;\n let count = words(sentences[best]!).length;\n for (;;) {\n let grew = false;\n for (const j of [hi + 1, lo - 1]) {\n if (j >= 0 && j < sentences.length && count + words(sentences[j]!).length <= maxWords) {\n count += words(sentences[j]!).length;\n lo = Math.min(lo, j);\n hi = Math.max(hi, j);\n grew = true;\n }\n }\n if (!grew) break;\n }\n let text = sentences.slice(lo, hi + 1).join(' ');\n if (words(text).length > maxWords) text = words(text).slice(0, maxWords).join(' ') + ' ...';\n return (lo > 0 ? '... ' : '') + text + (hi < sentences.length - 1 && !text.endsWith('...') ? ' ...' : '');\n}\n\n/**\n * The best-matching earlier turns for `query`. `exclude`: message ids or\n * exact texts already in the prompt (the recent turns).\n */\nconst FENCE = /^\\s*(?:```|~~~)/;\nconst TABLE_RULE = /^\\s*\\|?\\s*:?-{2,}/;\nconst SENTENCE = /(?<=[.!?])\\s+/;\n\n/**\n * A long turn as passages of up to about `maxWords` words: table rows carry the\n * table's header row, fenced blocks and other line-shaped text go a few lines\n * at a time, prose a few sentences at a time. Python: `split_passages`.\n */\nexport function splitPassages(content: string, maxWords: number): string[] {\n const text = (content ?? '').trim();\n if (words(text).length <= maxWords) return text ? [text] : [];\n const out: string[] = [];\n const lines = text.split('\\n');\n let i = 0;\n while (i < lines.length) {\n const line = lines[i]!;\n if (FENCE.test(line)) {\n let j = i + 1;\n while (j < lines.length && !FENCE.test(lines[j]!)) j += 1;\n out.push(...windowsOf(lines.slice(i + 1, j), maxWords));\n i = j + 1;\n continue;\n }\n if (line.trim().startsWith('|')) {\n let j = i;\n while (j < lines.length && lines[j]!.trim().startsWith('|')) j += 1;\n const rows = lines.slice(i, j).filter((r) => !TABLE_RULE.test(r)).map((r) => r.trim());\n const [header, body] = rows.length > 1 ? [rows[0]!, rows.slice(1)] : ['', rows];\n for (const row of body) {\n const both = `${header} ${row}`.trim();\n out.push(words(both).length <= Math.trunc((maxWords * 3) / 2) ? both : row);\n }\n i = j;\n continue;\n }\n let j = i;\n while (j < lines.length && lines[j]!.trim() && !FENCE.test(lines[j]!) && !lines[j]!.trim().startsWith('|')) j += 1;\n if (j === i) {\n i += 1;\n continue;\n }\n const block = lines.slice(i, j);\n if (block.length > 1 && block.every((b) => words(b).length <= 25)) {\n out.push(...windowsOf(block, maxWords));\n } else {\n const sentences = block.map((b) => b.trim()).join(' ').split(SENTENCE).filter((x) => x.trim());\n out.push(...windowsOf(sentences, maxWords));\n }\n i = j;\n }\n const kept = out.filter((x) => x.trim());\n return kept.length ? kept : [text];\n}\n\nfunction windowsOf(pieces: readonly string[], maxWords: number): string[] {\n const out: string[] = [];\n let cur: string[] = [];\n let used = 0;\n for (const raw of pieces) {\n const piece = raw.trim();\n if (!piece) continue;\n const n = words(piece).length;\n if (cur.length && used + n > maxWords) {\n out.push(cur.join(' '));\n cur = [];\n used = 0;\n }\n if (n > maxWords) {\n const w = words(piece);\n for (let k = 0; k < w.length; k += maxWords) out.push(w.slice(k, k + maxWords).join(' '));\n continue;\n }\n cur.push(piece);\n used += n;\n }\n if (cur.length) out.push(cur.join(' '));\n return out;\n}\n\n/** What search scores: a whole turn, or one passage of a long turn. */\ninterface Unit {\n seq: number;\n content: string;\n turn: number;\n first: boolean;\n last: boolean;\n /** The passage's index, -1 for a whole turn (its vector is the turn's). */\n part: number;\n}\n\n/** The value patterns of the kinds of value `query` asks for. Python: `_asked_kinds`. */\nfunction askedKinds(query: string): RegExp[] {\n const out: RegExp[] = [];\n for (const [name, spec] of Object.entries(HISTORY_SEARCH.asked_kinds ?? {})) {\n if (name.startsWith('$') || typeof spec !== 'object') continue;\n if (new RegExp(spec.query, 'i').test(query ?? '')) out.push(new RegExp(spec.value));\n }\n return out;\n}\n\n/** A question about how things started gets no recency prior. Python: `_asks_how_it_started`. */\nfunction asksHowItStarted(query: string): boolean {\n const spec = HISTORY_SEARCH.early_query;\n if (!spec?.pattern) return false;\n return new RegExp(spec.pattern, (spec.flags ?? '').includes('i') ? 'i' : '').test(query ?? '');\n}\n\nexport async function searchTurns(\n turns: readonly StoredTurn[],\n query: string,\n options: { exclude?: Iterable<string>; encoder?: Encoder | null; store?: TurnStore | null } = {},\n): Promise<Excerpt[]> {\n const params = HISTORY_SEARCH;\n const k = params.max_excerpts;\n const skip = new Set(options.exclude ?? []);\n const pool = turns.filter((t) => t.content && !skip.has(t.content) && !skip.has(t.message_id ?? ''));\n const terms = new Set(tokenize(query || ''));\n const literals = literalsOf(query);\n if (pool.length === 0 || (terms.size === 0 && literals.length === 0 && !options.encoder)) return [];\n\n const maxWords = params.max_words_per_excerpt;\n const units: Unit[] = [];\n pool.forEach((t, ti) => {\n if (!params.passages) {\n units.push({ seq: t.seq, content: t.content, turn: ti, first: true, last: true, part: -1 });\n return;\n }\n const parts = splitPassages(t.content, maxWords);\n parts.forEach((part, pi) => {\n // A one-passage turn keeps the turn's vector; passages of a longer one are stored per passage.\n units.push({\n seq: t.seq, content: part, turn: ti, first: pi === 0, last: pi === parts.length - 1,\n part: parts.length > 1 ? pi : -1,\n });\n });\n });\n\n const docs = units.map((u) => tokenize(u.content));\n const bm = new BM25(docs);\n const termList = [...terms];\n const rows = units.map((turn, i) => {\n const held = new Set(docs[i]);\n let matched = 0;\n for (const t of terms) if (held.has(t)) matched += 1;\n const low = turn.content.toLowerCase();\n let literal = 0;\n for (const lit of literals) if (low.includes(lit)) literal += 1;\n return { i, bm25: matched ? bm.score(termList, i) : 0, matched, literal };\n });\n const need = Math.min(params.min_matched_terms, Math.max(1, terms.size));\n // One query word is enough when few turns use it, not when many do.\n const rareCap = Math.max(1, Math.trunc((params.rare_term_max_share ?? 0) * units.length));\n const rare = new Set<string>();\n for (const t of terms) {\n let n = 0;\n for (const doc of docs) if (doc.includes(t)) n += 1;\n if (n > 0 && n <= rareCap) rare.add(t);\n }\n let eligible;\n if (params.passages) {\n // A passage of a turn that matches enough query words as a whole.\n const turnTerms = new Map<number, Set<string>>();\n for (const r of rows) {\n const held = turnTerms.get(units[r.i]!.turn) ?? new Set<string>();\n for (const t of docs[r.i]!) if (terms.has(t)) held.add(t);\n turnTerms.set(units[r.i]!.turn, held);\n }\n eligible = rows.filter(\n (r) =>\n r.literal > 0 ||\n (r.matched > 0 &&\n (turnTerms.get(units[r.i]!.turn)!.size >= need || docs[r.i]!.some((t) => rare.has(t)))),\n );\n } else {\n eligible = rows.filter(\n (r) => r.literal > 0 || r.matched >= need || (r.matched > 0 && docs[r.i]!.some((t) => rare.has(t))),\n );\n }\n\n const rrf = params.rrf_k;\n const fused = new Map<number, number>();\n const via = new Map<number, string>();\n for (const r of eligible) fused.set(r.i, 0);\n const add = (i: number, rank: number, how: string): void => {\n fused.set(i, (fused.get(i) ?? 0) + 1 / (rrf + rank + 1));\n if (!via.has(i)) via.set(i, how);\n };\n eligible\n .filter((r) => r.literal > 0)\n .sort((a, b) => b.literal - a.literal || a.i - b.i)\n .forEach((r, rank) => add(r.i, rank, 'literal'));\n eligible\n .filter((r) => r.bm25 > 0)\n .sort((a, b) => b.bm25 - a.bm25 || a.i - b.i)\n .forEach((r, rank) => add(r.i, rank, 'bm25'));\n\n // A question asking for a kind of value also ranks the passages holding one.\n const kinds = askedKinds(query);\n if (kinds.length) {\n rows\n .filter((r) => r.matched > 0 && kinds.some((k) => k.test(units[r.i]!.content)))\n .sort((a, b) => b.bm25 - a.bm25 || a.i - b.i)\n .forEach((r, rank) => add(r.i, rank, 'kind'));\n }\n\n // Fused on every query when a model is supplied (Python: measured 19 Oct).\n if (options.encoder) {\n const ranked = await embeddingRanks(units, query, options.encoder, options.store ?? null, params.min_cosine ?? 0.4);\n ranked.forEach(([i], rank) => add(i, rank, 'embedding'));\n }\n if (fused.size === 0) return [];\n\n const n = Math.max(pool.length - 1, 1);\n const weight = asksHowItStarted(query) ? 0 : params.recency_weight;\n // A turn ranks by its best unit; its best units are what is shown of it.\n const top = new Map<number, [number, number]>();\n const ranked = new Map<number, [number, number][]>();\n for (const [u, v] of fused) {\n const ti = units[u]!.turn;\n const score = v + (weight * (ti / n)) / (rrf + 1);\n ranked.set(ti, [...(ranked.get(ti) ?? []), [score, u]]);\n const held = top.get(ti);\n if (!held || score > held[0]) top.set(ti, [score, u]);\n }\n const best = [...top.keys()].sort((a, b) => top.get(b)![0] - top.get(a)![0] || a - b).slice(0, k);\n const perTurn = params.passages_per_turn ?? 1;\n const shown = (ti: number): string => {\n const unit = units[top.get(ti)![1]]!;\n if (!params.passages || (unit.first && unit.last)) return excerptOf(pool[ti]!.content, terms, literals, maxWords);\n const picked = [...ranked.get(ti)!]\n .sort((a, b) => b[0] - a[0] || b[1] - a[1])\n .slice(0, perTurn)\n .map(([, u]) => u)\n .sort((a, b) => a - b);\n const parts: string[] = [];\n for (const u of picked) parts.push((units[u]!.first || parts.length ? '' : '... ') + units[u]!.content);\n return parts.join(' ... ') + (units[picked[picked.length - 1]!]!.last ? '' : ' ...');\n };\n const found = new Map<number, Excerpt>();\n for (const ti of best) {\n found.set(ti, {\n position: pool[ti]!.position,\n role: pool[ti]!.role,\n timestamp: pool[ti]!.timestamp,\n score: top.get(ti)![0],\n via: via.get(top.get(ti)![1]) ?? '',\n text: shown(ti),\n });\n }\n // The turn before a hit is often what it answers or confirms; it comes along, cut to its end.\n const before = params.previous_turns ?? 0;\n const tail = params.previous_turn_words ?? 40;\n for (const i of best) {\n for (let j = i - before; j < i; j++) {\n if (j < 0 || found.has(j)) continue;\n const hit = pool[i]!.position;\n const prev = pool[j]!.position;\n if (hit && prev && hit - prev > before) continue; // the turn just before is in the prompt\n const w = words(pool[j]!.content);\n found.set(j, {\n position: prev,\n role: pool[j]!.role,\n timestamp: pool[j]!.timestamp,\n score: 0,\n via: 'previous',\n text: w.length <= tail ? w.join(' ') : '... ' + w.slice(-tail).join(' '),\n });\n }\n }\n return [...found.keys()].sort((a, b) => a - b).map((i) => found.get(i)!);\n}\n\nasync function embeddingRanks(\n pool: readonly { seq: number; content: string; part?: number }[],\n query: string,\n encoder: Encoder,\n store: TurnStore | null,\n minCosine: number,\n): Promise<[number, number][]> {\n // Stored under the turn's seq and the model's name (a passage's: name + \"#p<index>\"). Python: `_embedding_ranks`.\n const keyOf = (u: { part?: number }): string => ((u.part ?? -1) < 0 ? encoder.model : `${encoder.model}#p${u.part}`);\n const vectors = new Map<number, ArrayLike<number>>();\n if (store !== null) {\n const byKey = new Map<string, number[]>();\n pool.forEach((u, i) => byKey.set(keyOf(u), [...(byKey.get(keyOf(u)) ?? []), i]));\n for (const [key, idx] of byKey) {\n const held = store.vectors(idx.map((i) => pool[i]!.seq), key);\n for (const i of idx) {\n const v = held.get(pool[i]!.seq);\n if (v) vectors.set(i, v);\n }\n }\n }\n const missing = pool.map((_, i) => i).filter((i) => !vectors.has(i));\n if (missing.length > 0) {\n const fresh = await encoder.encode(missing.map((i) => pool[i]!.content));\n const put = new Map<string, Map<number, Float32Array>>();\n missing.forEach((i, j) => {\n const v = Float32Array.from(fresh[j] ?? []);\n vectors.set(i, v);\n const key = keyOf(pool[i]!);\n put.set(key, (put.get(key) ?? new Map()).set(pool[i]!.seq, v));\n });\n if (store) for (const [key, items] of put) store.putVectors(key, items);\n }\n const q = (await (encoder.encodeQuery ?? encoder.encode).call(encoder, [query]))[0] ?? [];\n return pool\n .map((_, i): [number, number] => [i, cosine(q, vectors.get(i) ?? [])])\n .filter(([, c]) => c >= minCosine)\n .sort((a, b) => b[1] - a[1]);\n}\n\n/**\n * How an archived tool result reads in the turn store and in excerpts: compact\n * JSON, cut to the contract's size. Python: `tool_result_text`.\n */\nexport function toolResultText(result: unknown, toolName?: string | null): string {\n let text = typeof result === 'string' ? result : JSON.stringify(result) ?? String(result);\n const limit = HISTORY_SEARCH.tool_result_max_chars ?? 16_000;\n if (text.length > limit) text = text.slice(0, limit) + ' ...[cut]';\n return `[tool result: ${toolName || 'tool'}] ${text}`;\n}\n\n/** The block appended after the compaction and the recent turns. */\nexport function renderExcerpts(excerpts: readonly Excerpt[], latest = ''): string {\n if (excerpts.length === 0) return '';\n const lines = [HISTORY_SEARCH.heading];\n for (const e of excerpts) {\n let where = e.position ? `turn ${e.position}, ${e.role}` : `${e.role}, earlier`;\n if (e.timestamp) {\n const ago = before(e.timestamp, latest);\n where += `, ${e.timestamp}` + (ago ? ` (${ago})` : '');\n }\n lines.push(`- ${where}: \"${e.text}\"`);\n }\n return lines.join('\\n');\n}\n\nconst ZONED = /(?:Z|[+-]\\d{2}:\\d{2})$/;\n\n/** Seconds since the epoch, for an ISO-8601 timestamp with a zone only. Python: `_instant`. */\nfunction instant(stamp: string): number | null {\n const text = (stamp ?? '').trim();\n if (!ZONED.test(text)) return null;\n const ms = Date.parse(text);\n return Number.isNaN(ms) ? null : ms / 1000;\n}\n\n/** \"3 days before the latest message\", or \"\" when either is unreadable. Python: `_before`. */\nfunction before(stamp: string, latest: string): string {\n const at = instant(stamp);\n const now = instant(latest);\n if (at === null || now === null || now < at) return '';\n const seconds = Math.trunc(now - at);\n for (const [unit, size] of [['day', 86_400], ['hour', 3_600], ['minute', 60]] as const) {\n if (seconds >= size) {\n const n = Math.trunc(seconds / size);\n return `${n} ${unit}${n !== 1 ? 's' : ''} before the latest message`;\n }\n }\n return 'less than a minute before the latest message';\n}\n","/**\n * Read-your-writes overlay for fetch responses.\n *\n * Ports Python's `_overlay_local_recent_turns` and `_budget_recent_turns`.\n *\n * The server's `recent_turns` reflects what it has persisted and compacted. A\n * turn recorded moments ago may not be in there yet, so a fast follow-up would\n * fetch context missing the thing the user just said. The overlay replaces\n * `recent_turns` with the locally buffered tail while leaving every summary\n * field to the server.\n *\n * On by default, matching Python. `SYNAP_ST_VERBATIM_OVERLAY` is the\n * kill-switch.\n */\n\nimport type { Json, RawContext } from './types.js';\nimport { isComplete, turnsAfter } from '../cache/short-term-store.js';\nimport type { ShortTermStore, ShortTermTurn } from '../cache/short-term-store.js';\nimport { getEnv } from '../util/env.js';\n\n/**\n * The server trims `recent_turns` to this window before returning it. The\n * overlay re-applies the same budget so a long pre-compaction local tail\n * cannot silently re-inflate the prompt past what the server would have\n * allowed.\n */\nconst MAX_TURNS = 20;\nconst TOKEN_BUDGET = 2000;\n\n/**\n * Whether to splice the local tail in.\n *\n * Default ON: this is the correctness switch (a just-written turn must surface\n * on the next fetch), separate from the cost-side `st_authoritative` flag that\n * tells the server to skip assembling short-term context at all.\n */\nexport function verbatimOverlayEnabled(configured?: boolean): boolean {\n // Resolution order matches Python's `_is_st_verbatim_overlay`: the env var\n // is an explicit override / kill-switch and wins over the config field.\n const raw = (getEnv('SYNAP_ST_VERBATIM_OVERLAY') ?? '').trim().toLowerCase();\n if (['0', 'false', 'no', 'off'].includes(raw)) return false;\n if (['1', 'true', 'yes', 'on'].includes(raw)) return true;\n return configured ?? true;\n}\n\n/** ~4 characters per token, matching the server-side heuristic. */\nfunction estimateTokens(turn: ShortTermTurn): number {\n return Math.max(1, Math.floor(String(turn.content ?? '').length / 4));\n}\n\n/**\n * Cap the tail to the server's window, then drop OLDEST turns until it fits\n * the token budget. Oldest-first because the newest turn is the one the caller\n * is most likely to be following up on.\n */\nexport function budgetRecentTurns(turns: readonly ShortTermTurn[]): ShortTermTurn[] {\n const windowed = turns.slice(-MAX_TURNS);\n let total = windowed.reduce((n, t) => n + estimateTokens(t), 0);\n while (windowed.length > 0 && total > TOKEN_BUDGET) {\n const dropped = windowed.shift() as ShortTermTurn;\n total -= estimateTokens(dropped);\n }\n return windowed;\n}\n\n/**\n * Splice the local tail into a fetch response, in place.\n *\n * Deliberately never throws: this runs inside the fetch path, and a bookkeeping\n * problem here must not fail a retrieval that otherwise succeeded.\n */\nexport function overlayLocalRecentTurns(\n response: RawContext,\n store: ShortTermStore,\n /** `null` too: callers spell \"no conversation\" as `convId || null`. */\n conversationId: string | null | undefined,\n /** `st_verbatim_overlay` from the client options. The env var still wins. */\n configured?: boolean,\n): void {\n if (conversationId === undefined || conversationId === null || conversationId === '') return;\n try {\n const entry = store.get(conversationId);\n // Cold store: nothing locally, so leave the server's response untouched.\n if (entry === null) return;\n\n const existing = (response.conversation_context ?? null) as Json | null;\n\n // The server returned a conversation_context AND the overlay is switched\n // off: leave it exactly as sent. When the server OMITTED one we still\n // splice from local, so the skip-server-ST optimisation keeps working\n // regardless of this switch.\n if (existing !== null && !verbatimOverlayEnabled(configured)) return;\n\n // Which turns the tail is built from. The local list replaces the\n // server's only when it is known to be the whole conversation and to\n // follow the same summary the server sent. Otherwise the server's turns\n // stand, and the local list adds only what comes after them. A process\n // that has just restarted holds one turn, and replacing the server's list\n // with it dropped every turn before the restart.\n const serverCompaction = existing === null ? null : existing['compaction_id'];\n const localIsWhole =\n isComplete(entry) && (!serverCompaction || serverCompaction === entry.compactionId);\n let sourceTurns: ShortTermTurn[] = entry.recentTurns;\n if (existing !== null && !localIsWhole) {\n const raw = Array.isArray(existing['recent_turns']) ? existing['recent_turns'] : [];\n const serverTurns: ShortTermTurn[] = (raw as Array<Record<string, unknown>>).map((t) => ({\n role: String(t['role'] ?? 'user'),\n content: String(t['content'] ?? ''),\n timestamp: String(t['timestamp'] ?? ''),\n }));\n sourceTurns = [...serverTurns, ...turnsAfter(entry.recentTurns, serverTurns)];\n }\n // Role, text and time only, as Python sends: the server's id for a turn is\n // bookkeeping for the store, not part of the response.\n const localTurns = budgetRecentTurns(sourceTurns).map((t) => ({\n role: t.role,\n content: t.content,\n timestamp: t.timestamp,\n }));\n\n // `x || fallback`, not `??`, matching Python: the server sends '' and {}\n // for absent fields rather than null, and those must not win over a local\n // value that actually has content.\n const serverField = (name: string): unknown =>\n existing === null ? undefined : existing[name];\n const orLocal = <T>(name: string, local: T): unknown => {\n const value = serverField(name);\n if (value === undefined || value === null || value === '') return local;\n if (typeof value === 'object' && Object.keys(value as object).length === 0) return local;\n return value;\n };\n\n // Server wins on every summary field; local only ever supplies the tail.\n response.conversation_context = {\n summary: orLocal('summary', entry.summary),\n current_state: orLocal('current_state', { ...entry.currentState }),\n key_extractions: orLocal('key_extractions', { ...entry.keyExtractions }),\n recent_turns: localTurns,\n compaction_id: orLocal('compaction_id', entry.compactionId),\n compacted_at: orLocal('compacted_at', entry.compactedAt),\n conversation_id: conversationId,\n } as NonNullable<RawContext['conversation_context']>;\n } catch {\n // Non-fatal by design. See the note above.\n }\n}\n","/**\n * Render `get_context_for_prompt` locally from the short-term store.\n *\n * A direct port of Python's `formatter/context_for_prompt.py`. It exists for\n * the same reason: when the SDK is authoritative for short-term context, a\n * warm conversation can answer this call without a cloud round trip at all,\n * which is a latency saving and a metered-call saving both.\n *\n * The three styles and their exact heading text are copied from Python\n * deliberately. A prompt assembled by one SDK and one assembled by the other\n * must be the same string, or an application that switches languages silently\n * changes what its model sees.\n */\n\nimport { CONTEXT_BLOCK_PREFIX } from '../cache/short-term-store.js';\nimport type { ShortTermEntry, ShortTermTurn } from '../cache/short-term-store.js';\nimport type { Json } from './types.js';\n\nexport const SUPPORTED_STYLES = ['structured', 'narrative', 'bullet_points'] as const;\nexport type PromptStyle = (typeof SUPPORTED_STYLES)[number];\nexport const DEFAULT_STYLE: PromptStyle = 'structured';\n\nexport interface LocalRecentMessage {\n role: string;\n content: string;\n /** ISO-8601. */\n timestamp: string;\n message_id: string;\n}\n\nexport interface LocalPromptResponse {\n formatted_context: string | null;\n available: boolean;\n is_stale: boolean;\n compression_ratio: number | null;\n validation_score: number | null;\n compaction_age_seconds: number | null;\n quality_warning: boolean;\n recent_messages: LocalRecentMessage[];\n recent_message_count: number;\n compacted_message_count: number;\n total_message_count: number;\n /**\n * The framed compaction block alone (also inside formatted_context), for a\n * framework that keeps its own message list: a system message ahead of\n * recent_messages, in place of the turns it covers.\n */\n context_block: string | null;\n compaction_id: string | null;\n [key: string]: unknown;\n}\n\nfunction stringify(value: unknown): string {\n if (value === null || value === undefined) return '';\n const t = typeof value;\n if (t === 'string' || t === 'number' || t === 'boolean') return String(value);\n try {\n return JSON.stringify(value) ?? '';\n } catch {\n return String(value);\n }\n}\n\n/** Key extractions arrive as a list, a bare string, or a single object. */\nfunction asListOfObjects(value: unknown): Record<string, unknown>[] {\n if (!value) return [];\n if (Array.isArray(value)) {\n const out: Record<string, unknown>[] = [];\n for (const v of value) {\n if (v && typeof v === 'object' && !Array.isArray(v)) out.push(v as Record<string, unknown>);\n else if (typeof v === 'string') out.push({ content: v });\n }\n return out;\n }\n if (typeof value === 'object') return [value as Record<string, unknown>];\n return [];\n}\n\nfunction itemText(item: Record<string, unknown>): string {\n return (item['content'] as string) || (item['text'] as string) || stringify(item);\n}\n\nfunction buildRecentMessages(turns: readonly ShortTermTurn[]): LocalRecentMessage[] {\n return (turns ?? []).map((t, idx) => ({\n role: t.role || 'user',\n content: t.content ?? '',\n timestamp: t.timestamp,\n message_id: t.message_id || `local-${idx}`,\n }));\n}\n\n/**\n * The SDK does not know how many messages a compaction covered: the server\n * does not put that count in the bundle. Python reports 0 here for the same\n * reason, and both must agree.\n */\nfunction compactedMessageCount(_entry: ShortTermEntry): number {\n return 0;\n}\n\n/** The v3 block, which is printed as received, once, under its own header. */\nfunction blockOf(entry: ShortTermEntry): string | null {\n if (entry.contextBlock) return entry.contextBlock.trim();\n if (entry.summary && entry.summary.startsWith(CONTEXT_BLOCK_PREFIX)) return entry.summary.trim();\n return null;\n}\n\nfunction formatStructured(entry: ShortTermEntry, recent: LocalRecentMessage[]): string {\n const parts: string[] = [];\n const block = blockOf(entry);\n if (block) {\n parts.push(block);\n if (recent.length > 0) {\n parts.push(`## Recent Turns (${recent.length})`);\n for (const m of recent) parts.push(`**${m.role}**: ${m.content}`);\n }\n return parts.filter(Boolean).join('\\n\\n').trim();\n }\n if (entry.summary) { parts.push('## Summary'); parts.push(entry.summary.trim()); }\n if (entry.factualParagraph) { parts.push('## Facts'); parts.push(entry.factualParagraph.trim()); }\n if (entry.conversationalParagraph) {\n parts.push('## Conversation'); parts.push(entry.conversationalParagraph.trim());\n }\n\n const cs = entry.currentState ?? {};\n if (Object.keys(cs).length > 0) {\n parts.push('## Current State');\n for (const [k, v] of Object.entries(cs)) parts.push(`- ${k}: ${stringify(v)}`);\n }\n\n const ke = entry.keyExtractions ?? {};\n if (Object.keys(ke).length > 0) {\n parts.push('## Key Extractions');\n for (const [cat, items] of Object.entries(ke)) {\n if (!items) continue;\n parts.push(`### ${cat}`);\n for (const it of asListOfObjects(items)) parts.push(`- ${itemText(it)}`);\n }\n }\n\n if (recent.length > 0) {\n parts.push(`## Recent Turns (${recent.length})`);\n for (const m of recent) parts.push(`**${m.role}**: ${m.content}`);\n }\n\n return parts.filter(Boolean).join('\\n\\n').trim();\n}\n\nfunction formatNarrative(entry: ShortTermEntry, recent: LocalRecentMessage[]): string {\n const parts: string[] = [];\n const block = blockOf(entry);\n if (block) parts.push(block);\n else if (entry.summary) parts.push(entry.summary.trim());\n if (!block && entry.conversationalParagraph) parts.push(entry.conversationalParagraph.trim());\n else if (!block && entry.factualParagraph) parts.push(entry.factualParagraph.trim());\n\n if (recent.length > 0) {\n const tail = recent.map((m) => `${m.role}: ${m.content}`).join('\\n');\n parts.push(`Recent exchanges:\\n${tail}`);\n }\n return parts.filter(Boolean).join('\\n\\n').trim();\n}\n\nfunction formatBullets(entry: ShortTermEntry, recent: LocalRecentMessage[]): string {\n const parts: string[] = [];\n const block = blockOf(entry);\n if (block) {\n // The block is structured text already; bulleting it would mangle it.\n return [block, ...recent.map((m) => `- ${m.role}: ${m.content}`)].join('\\n').trim();\n }\n if (entry.summary) parts.push(`- Summary: ${entry.summary.trim()}`);\n\n for (const [k, v] of Object.entries(entry.currentState ?? {})) {\n parts.push(`- ${k}: ${stringify(v)}`);\n }\n for (const [cat, items] of Object.entries(entry.keyExtractions ?? {})) {\n for (const it of asListOfObjects(items)) parts.push(`- (${cat}) ${itemText(it)}`);\n }\n for (const m of recent) parts.push(`- ${m.role}: ${m.content}`);\n\n return parts.join('\\n').trim();\n}\n\nfunction formatBlock(entry: ShortTermEntry, recent: LocalRecentMessage[], style: string): string {\n if (style === 'narrative') return formatNarrative(entry, recent);\n if (style === 'bullet_points') return formatBullets(entry, recent);\n return formatStructured(entry, recent);\n}\n\n/** Build a context-for-prompt response from a cached entry. */\nexport function renderForPrompt(\n entry: ShortTermEntry,\n style: string = DEFAULT_STYLE,\n now: () => number = Date.now,\n): LocalPromptResponse {\n const chosen: string = (SUPPORTED_STYLES as readonly string[]).includes(style)\n ? style\n : DEFAULT_STYLE;\n\n const recent = buildRecentMessages(entry.recentTurns);\n const formatted = formatBlock(entry, recent, chosen);\n const available = entry.compactionId !== null || entry.recentTurns.length > 0;\n\n let compactionAge: number | null = null;\n if (entry.compactedAt !== null) {\n const parsed = Date.parse(entry.compactedAt);\n if (!Number.isNaN(parsed)) compactionAge = Math.floor((now() - parsed) / 1000);\n }\n\n const compacted = compactedMessageCount(entry);\n return {\n formatted_context: available ? formatted : null,\n available,\n is_stale: false,\n compression_ratio: null,\n validation_score: null,\n compaction_age_seconds: compactionAge,\n quality_warning: false,\n recent_messages: recent,\n recent_message_count: recent.length,\n compacted_message_count: compacted,\n total_message_count: recent.length + compacted,\n context_block: blockOf(entry),\n compaction_id: entry.compactionId,\n };\n}\n\n/** Build a compaction response from a cached entry, or null if none landed. */\nexport function renderCompacted(\n entry: ShortTermEntry,\n format = 'structured',\n correlationId = '',\n): Json | null {\n if (entry.compactionId === null) return null;\n\n const formatted = formatBlock(\n entry,\n buildRecentMessages(entry.recentTurns),\n format === 'narrative' ? 'narrative' : 'structured',\n );\n\n return {\n compacted_context: formatted,\n compaction_id: entry.compactionId,\n compacted_at: entry.compactedAt,\n conversation_id: entry.conversationId,\n current_state: entry.currentState,\n correlation_id: correlationId,\n original_token_count: null,\n compacted_token_count: null,\n compression_ratio: null,\n level_applied: null,\n validation_score: null,\n validation_passed: null,\n quality_warning: false,\n };\n}\n","/**\n * Periodic user-summary injection.\n *\n * Ports Python's `_should_inject_user_summary` / `_merge_user_summary_into_response`.\n *\n * Every Nth turn, a cached `user_summary` bundle is folded into the fetch\n * response. It is what keeps a long conversation grounded in who the user is\n * without paying for a broader retrieval on every turn. Without it, JS returns\n * less context than Python from identical usage, and the gap widens the longer\n * the conversation runs.\n */\n\nimport type { Json, RawContext, RawContextItem } from './types.js';\n\n/** Python's `_user_summary_interval`. */\nexport const USER_SUMMARY_INTERVAL = 5;\n/** Python's `max_summary_items`. */\nconst MAX_SUMMARY_ITEMS = 3;\n\nconst COLLECTIONS = ['facts', 'preferences', 'episodes', 'emotions', 'temporal_events'] as const;\n\n/** Per-conversation turn counters, so the cadence is per conversation. */\nexport class TurnCounter {\n readonly #counts = new Map<string, number>();\n\n /** Increment and return the new count. Python buckets a missing id under `_global`. */\n increment(conversationId: string | undefined): number {\n const key = conversationId !== undefined && conversationId !== '' ? conversationId : '_global';\n const next = (this.#counts.get(key) ?? 0) + 1;\n this.#counts.set(key, next);\n return next;\n }\n\n /**\n * True on every Nth turn, and never on the first.\n *\n * Python: `count > 0 and count % interval == 0`. Turn 5 injects, turn 1 does\n * not, which matters because the first turn is usually the one that just\n * triggered a fresh retrieval anyway.\n */\n shouldInject(conversationId: string | undefined): boolean {\n const key = conversationId !== undefined && conversationId !== '' ? conversationId : '_global';\n const count = this.#counts.get(key) ?? 0;\n return count > 0 && count % USER_SUMMARY_INTERVAL === 0;\n }\n\n clear(): void {\n this.#counts.clear();\n }\n}\n\nfunction itemId(item: Json): string {\n return String(item['item_id'] ?? item['id'] ?? '');\n}\n\nfunction pick(item: Json, ...names: string[]): unknown {\n for (const name of names) {\n const value = item[name];\n if (value !== undefined && value !== null) return value;\n }\n return undefined;\n}\n\n/**\n * Normalise a summary-bundle item to the response item shape.\n *\n * The bundle carries the proto's field names, which differ from the REST\n * response's. Python spells the whole mapping out rather than passing items\n * through, because a consumer reading `.content` on an episode would otherwise\n * get undefined.\n */\nfunction normalise(item: Json): RawContextItem {\n const content = pick(item, 'content') ?? '';\n const confidence = pick(item, 'confidence') ?? 0;\n const extractedAt = pick(item, 'extracted_at', 'created_at') ?? null;\n return {\n id: itemId(item),\n content: String(content),\n confidence: Number(confidence),\n source: String(pick(item, 'source') ?? ''),\n category: String(pick(item, 'context_type', 'category') ?? ''),\n emotion_type: String(pick(item, 'context_type', 'emotion_type') ?? ''),\n strength: Number(pick(item, 'strength', 'confidence') ?? 0),\n summary: String(pick(item, 'summary', 'content') ?? ''),\n significance: Number(pick(item, 'significance', 'confidence') ?? 0),\n intensity: Number(pick(item, 'intensity', 'confidence') ?? 0),\n context: String(pick(item, 'context', 'content') ?? ''),\n participants: (pick(item, 'participants') ?? []) as unknown[],\n extracted_at: extractedAt as string | null,\n occurred_at: (pick(item, 'occurred_at', 'extracted_at', 'created_at') ?? null) as string | null,\n detected_at: (pick(item, 'detected_at', 'extracted_at', 'created_at') ?? null) as string | null,\n metadata: (pick(item, 'metadata') ?? {}) as Json,\n event_date: (pick(item, 'event_date') ?? null) as string | null,\n said_at: (pick(item, 'said_at') ?? null) as string | null,\n valid_until: (pick(item, 'valid_until') ?? null) as string | null,\n temporal_category: (pick(item, 'temporal_category') ?? null) as string | null,\n temporal_confidence: Number(pick(item, 'temporal_confidence') ?? 0),\n };\n}\n\n/**\n * Fold a cached summary bundle into a response, in place.\n *\n * Appends at most `MAX_SUMMARY_ITEMS` per collection, skipping anything whose\n * id the response already carries: the summary supplements a retrieval, it does\n * not replace or duplicate it.\n */\nexport function mergeUserSummary(response: RawContext, bundle: Json): RawContext {\n const itemsByType = (bundle['items_by_type'] ?? {}) as Record<string, unknown>;\n for (const collection of COLLECTIONS) {\n const items = itemsByType[collection];\n if (!Array.isArray(items)) continue;\n\n const existing = (response[collection] ?? []) as RawContextItem[];\n const existingIds = new Set(existing.map((i) => String(i.id ?? i.item_id ?? '')));\n const fresh = (items as Json[])\n .filter((i) => typeof i === 'object' && i !== null && !existingIds.has(itemId(i)))\n .slice(0, MAX_SUMMARY_ITEMS)\n .map(normalise);\n if (fresh.length === 0) continue;\n\n response[collection] = [...existing, ...fresh];\n }\n return response;\n}\n","/**\n * Serving a fetch from the anticipation cache.\n *\n * Ports Python's `_build_anticipation_response` and the cache-first branch each\n * scope fetch runs before going to the network.\n *\n * This is the point of the whole anticipation feature. The gRPC stream pushes\n * bundles ahead of time; without a lookup on the read path the cache is\n * write-only, every fetch is a billed round trip, and the stream does nothing\n * but consume bandwidth. That was the state of this SDK before: `store()` was\n * wired, `lookup()` was never called.\n */\n\nimport type { Json, RawContext } from './types.js';\nimport type { AnticipationCache, LookupResult } from './anticipation-cache.js';\n\nexport type Scope = 'conversation' | 'user' | 'customer' | 'client';\n\nexport interface AnticipationLookupParams {\n // `| undefined` explicitly, because exactOptionalPropertyTypes distinguishes\n // an absent key from one set to undefined, and callers build these by\n // spreading optionals.\n searchQuery?: readonly string[] | null | undefined;\n /** The user. Absent for customer- and client-scope fetches. */\n entityId?: string | null | undefined;\n customerId?: string | null | undefined;\n clientId?: string | null | undefined;\n conversationId?: string | null | undefined;\n /**\n * The rung the request names, from its `scope_path`. Passed through every\n * scope unchanged: unlike the ids below it, a rung never widens, so there is\n * no scope for which dropping it would be the safe thing to do.\n */\n scopeRung?: string | null | undefined;\n}\n\n/**\n * Which identifiers a scope may widen its lookup across.\n *\n * A customer-scope request widens to client-shared bundles but explicitly NOT\n * to user-scoped ones: those would leak one visitor's data into another\n * visitor's customer fetch. Client scope narrows furthest for the same reason.\n * This mirrors the scoping notes on Python's four fetch methods.\n */\nexport function scopeLookupParams(\n scope: Scope,\n params: AnticipationLookupParams,\n): AnticipationLookupParams {\n const { searchQuery, entityId, customerId, clientId, conversationId, scopeRung } = params;\n switch (scope) {\n case 'conversation':\n return { searchQuery, entityId, customerId, clientId, conversationId, scopeRung };\n case 'user':\n // No conversationId: a user-scope fetch is not bound to one thread.\n return { searchQuery, entityId, customerId, clientId, scopeRung };\n case 'customer':\n return { searchQuery, entityId: null, customerId, clientId, scopeRung };\n case 'client':\n return { searchQuery, entityId: null, customerId: null, clientId, scopeRung };\n }\n}\n\n/** Turn a cache hit into the same raw shape the network path returns. */\nexport function buildAnticipationResponse(hit: LookupResult): RawContext {\n const response: RawContext = {};\n for (const [type, items] of Object.entries(hit.itemsByType)) {\n (response as Json)[type] = items;\n }\n // `source: \"anticipation\"` is how a caller (and the metering that reads it)\n // can tell a locally served fetch from a billed one.\n response.metadata = {\n source: 'anticipation',\n ttl_seconds: 0,\n cache_hit: true,\n retrieved_at: new Date().toISOString(),\n };\n return response;\n}\n\n/** Every item id in a hit, for the `context_used` event's `served_item_ids`. */\nexport function servedItemIds(hit: LookupResult): string[] {\n const ids: string[] = [];\n for (const items of Object.values(hit.itemsByType)) {\n for (const item of items) {\n const id = item.item_id ?? (item as Json)['id'];\n if (typeof id === 'string' && id !== '') ids.push(id);\n }\n }\n return ids;\n}\n\nexport interface AnticipationAttempt {\n /** The response to return, or null to fall through to the network. */\n response: RawContext | null;\n hit: LookupResult | null;\n}\n\n/**\n * Try to serve this fetch locally.\n *\n * Deliberately swallows its own errors: a fault in the cache must degrade to a\n * network fetch, never fail a retrieval that the server could have answered.\n */\nexport async function tryServeFromCache(\n cache: AnticipationCache,\n scope: Scope,\n params: AnticipationLookupParams,\n maxItems: number,\n): Promise<AnticipationAttempt> {\n try {\n const scoped = scopeLookupParams(scope, params);\n const hit = await cache.lookupAsync({\n searchQuery: scoped.searchQuery ?? null,\n entityId: scoped.entityId ?? null,\n customerId: scoped.customerId ?? null,\n clientId: scoped.clientId ?? null,\n conversationId: scoped.conversationId ?? null,\n scopeRung: scoped.scopeRung ?? null,\n maxItems,\n });\n if (hit === null) return { response: null, hit: null };\n return { response: buildAnticipationResponse(hit), hit };\n } catch {\n return { response: null, hit: null };\n }\n}\n\n/**\n * How many distinct, non-empty search queries a fetch carried, for the\n * `context_used` event's `query_count`. Matches the Python SDK and the count\n * the server itself uses when it answers a fetch: the same query sent twice is\n * one, and an empty string is none.\n */\nexport function distinctQueryCount(searchQuery: unknown): number {\n if (!Array.isArray(searchQuery)) return 0;\n return new Set(searchQuery.filter((q) => typeof q === 'string' && q.length > 0)).size;\n}\n","/**\n * `context_assembled`: what the SDK actually composed for this turn.\n *\n * Ports Python's `_emit_context_assembled_event`. Fires after every fetch\n * resolves, whether it was served from the cache, the local short-term store or\n * the network, and records what the caller actually received. That can differ\n * from what the server returned, because of SDK-side trimming, the overlay, or\n * a cache hit that skipped the server entirely.\n *\n * It drives the Requests-page audit. The server backfills a synthetic row if no\n * event arrives within its window, so a missing event is not silently lost, it\n * just produces a less accurate one.\n *\n * Privacy: ids, counts and assembly metadata only. Never prompt or item content.\n */\n\nimport type { Json, RawContext } from './types.js';\n\nconst COLLECTIONS = ['facts', 'preferences', 'episodes', 'emotions', 'temporal_events'] as const;\n\nexport interface AssembledEvent extends Json {\n correlation_id: string;\n conversation_id: string;\n user_id: string;\n customer_id: string;\n final_item_ids: string[];\n final_total_tokens: number;\n compaction_id: string;\n recent_turn_count: number;\n compaction_end_timestamp: string;\n assembly_source: string;\n assembly_duration_ms: number;\n cache_hit: boolean;\n sdk_version: string;\n}\n\n/** Every item id the caller ended up with, across all five collections. */\nexport function finalItemIds(response: RawContext): string[] {\n const ids: string[] = [];\n for (const collection of COLLECTIONS) {\n const items = response[collection];\n if (!Array.isArray(items)) continue;\n for (const item of items) {\n const id = (item as Json)['id'] ?? (item as Json)['item_id'];\n if (typeof id === 'string' && id !== '') ids.push(id);\n }\n }\n return ids;\n}\n\n/**\n * Normalise the assembly source and derive `cache_hit` from it.\n *\n * `ResponseMetadata` uses `cache` as shorthand for the local HTTP cache; the\n * proto wants the explicit name. Three sources imply a hit regardless of what\n * the metadata's own flag says, because in each the server was not consulted.\n */\nexport function resolveSource(\n response: RawContext,\n override?: string,\n): { source: string; cacheHit: boolean } {\n const metadata = (response.metadata ?? {}) as Json;\n let source = override ?? String(metadata['source'] ?? '') ?? '';\n if (source === '') source = 'cloud';\n if (source === 'cache') source = 'http_cache';\n const impliesHit = ['anticipation', 'anticipation_cache', 'http_cache', 'sdk_authoritative'];\n const cacheHit = impliesHit.includes(source) ? true : Boolean(metadata['cache_hit']);\n return { source, cacheHit };\n}\n\nexport function buildAssembledEvent(params: {\n correlationId: string;\n response: RawContext;\n conversationId?: string | undefined;\n userId?: string | undefined;\n customerId?: string | undefined;\n startedAt: number;\n sdkVersion: string;\n sourceOverride?: string | undefined;\n}): AssembledEvent {\n const { response } = params;\n const { source, cacheHit } = resolveSource(response, params.sourceOverride);\n const conversationContext = (response.conversation_context ?? {}) as Json;\n const recentTurns = conversationContext['recent_turns'];\n const metadata = (response.metadata ?? {}) as Json;\n\n return {\n correlation_id: params.correlationId,\n conversation_id: params.conversationId ?? '',\n user_id: params.userId ?? '',\n customer_id: params.customerId ?? '',\n final_item_ids: finalItemIds(response),\n final_total_tokens: Number(metadata['total_tokens'] ?? 0),\n compaction_id: String(conversationContext['compaction_id'] ?? ''),\n recent_turn_count: Array.isArray(recentTurns) ? recentTurns.length : 0,\n compaction_end_timestamp: String(\n conversationContext['end_timestamp'] ?? conversationContext['compacted_at'] ?? '',\n ),\n assembly_source: source,\n assembly_duration_ms: Math.max(0, Date.now() - params.startedAt),\n cache_hit: cacheHit,\n sdk_version: params.sdkVersion,\n };\n}\n","/**\n * Cross-scope merge + prompt formatting for `client.fetch()`.\n *\n * Ports Python's `UnifiedContextResponse.merge` and `format_for_prompt` from\n * `models/context.py`. The formatted string is the part customers actually\n * paste into a system prompt, so it is reproduced character for character:\n * section order, the `- ` bullet, the `[scope: x]` / `strength: 0.8` bracket\n * suffixes, the `## User Context` wrapper, and the blank-line joins. A\n * cosmetic drift here shows up as a diff in every prompt a customer builds.\n */\n\nimport { rankedLabel, sortByRankScore } from './ranked.js';\nimport type { Json, RawContext, RawContextItem } from './types.js';\n\n/** The five item collections, in Python's iteration order. */\nexport const ITEM_COLLECTIONS = [\n 'facts',\n 'preferences',\n 'episodes',\n 'emotions',\n 'temporal_events',\n] as const;\n\nexport type ItemCollection = (typeof ITEM_COLLECTIONS)[number];\n\nexport interface UnifiedContext {\n facts: RawContextItem[];\n preferences: RawContextItem[];\n episodes: RawContextItem[];\n emotions: RawContextItem[];\n temporal_events: RawContextItem[];\n /**\n * `response_shape: \"ranked\"` only: every item once, best first across\n * scopes and types, renumbered from 1. Empty for a by-type fetch.\n */\n items: RawContextItem[];\n /** `response_shape: \"ranked\"` only: the chunks (quotes from source documents). */\n excerpts: RawContextItem[];\n /** item id -> the scope that supplied it. */\n scope_map: Record<string, string>;\n conversation_context: Json | null;\n profile: Json | null;\n conversations: Json[] | null;\n formatted_context: string;\n scopes_queried: string[];\n total_items: number;\n metadata: Json | null;\n}\n\nfunction itemId(item: RawContextItem): string | undefined {\n const id = item.id ?? item.item_id;\n return typeof id === 'string' && id !== '' ? id : undefined;\n}\n\n/**\n * Merge per-scope responses, deduplicating by item id.\n *\n * First scope wins on a duplicate, so the caller's scope order is significant.\n * An item with no usable id is kept rather than dropped: Python indexes\n * `scope_map` by `item.id`, and a missing id there would collide every such\n * item onto one key, so keeping-and-not-mapping is the closest faithful\n * behaviour.\n */\nexport function mergeScopeResults(\n scopeResults: ReadonlyArray<readonly [string, RawContext]>,\n): UnifiedContext {\n const seen = new Set<string>();\n const out: UnifiedContext = {\n facts: [], preferences: [], episodes: [], emotions: [], temporal_events: [],\n items: [], excerpts: [],\n scope_map: {},\n conversation_context: null,\n profile: null,\n conversations: null,\n formatted_context: '',\n scopes_queried: [],\n total_items: 0,\n metadata: null,\n };\n\n const rankedSeen = new Set<string>();\n const rankedItems: RawContextItem[] = [];\n\n for (const [scopeName, response] of scopeResults) {\n out.scopes_queried.push(scopeName);\n if (out.metadata === null && response.metadata !== undefined) {\n out.metadata = response.metadata as Json;\n }\n // The C4 summary sections ride up from whichever scope produced them,\n // which is always the user-scope sub-fetch. See the fan-out rule in fetch().\n if (out.profile === null && response['profile'] !== undefined && response['profile'] !== null) {\n out.profile = response['profile'] as Json;\n }\n if (\n out.conversations === null &&\n response['conversations'] !== undefined &&\n response['conversations'] !== null\n ) {\n out.conversations = response['conversations'] as Json[];\n }\n\n for (const collection of ITEM_COLLECTIONS) {\n const items = response[collection];\n if (!Array.isArray(items)) continue;\n for (const item of items as RawContextItem[]) {\n const id = itemId(item);\n if (id !== undefined) {\n if (seen.has(id)) continue;\n seen.add(id);\n out.scope_map[id] = scopeName;\n }\n out[collection].push(item);\n }\n }\n\n // Ranked responses only, and in Python's order: excerpts share the typed\n // lists' dedup and scope map; the ranked items have a dedup of their own,\n // because they are the same items again, in order.\n const excerpts = response['excerpts'];\n if (Array.isArray(excerpts)) {\n for (const item of excerpts as RawContextItem[]) {\n const id = itemId(item);\n if (id !== undefined) {\n if (seen.has(id)) continue;\n seen.add(id);\n out.scope_map[id] = scopeName;\n }\n out.excerpts.push(item);\n }\n }\n const ranked = response['items'];\n if (Array.isArray(ranked)) {\n for (const item of ranked as RawContextItem[]) {\n const id = itemId(item);\n if (id !== undefined) {\n if (rankedSeen.has(id)) continue;\n rankedSeen.add(id);\n }\n rankedItems.push(item);\n }\n }\n }\n\n // One order across scopes, by each item's rank_score. The sort is stable, so\n // equal scores keep scope order, as Python's does, and unscored items go last.\n out.items = sortByRankScore(rankedItems).map((item, n) => ({ ...item, rank: n + 1 }));\n out.total_items =\n ITEM_COLLECTIONS.reduce((n, c) => n + out[c].length, 0) + out.excerpts.length;\n return out;\n}\n\n/** Python's `f\"{x:.1f}\"`. */\nfunction oneDecimal(value: number): string {\n return value.toFixed(1);\n}\n\n/** Python's `datetime.strftime(\"%Y-%m-%d\")` on an ISO string. */\nfunction isoDate(value: unknown): string | null {\n if (typeof value !== 'string' || value === '') return null;\n const parsed = new Date(value);\n if (Number.isNaN(parsed.getTime())) return null;\n return parsed.toISOString().slice(0, 10);\n}\n\nfunction numberOf(value: unknown): number | null {\n return typeof value === 'number' && Number.isFinite(value) ? value : null;\n}\n\nfunction scopeOf(ctx: UnifiedContext, item: RawContextItem): string {\n const id = itemId(item);\n return (id !== undefined ? ctx.scope_map[id] : undefined) ?? 'unknown';\n}\n\n/** `- text [a, b]`, with the bracket omitted when there are no extras. */\nfunction bulletWithExtras(text: string, extras: string[]): string {\n return extras.length > 0 ? `- ${text} [${extras.join(', ')}]` : `- ${text}`;\n}\n\nexport interface FormatOptions {\n includeScope?: boolean;\n includeConversationContext?: boolean;\n}\n\n/**\n * Render the merged context for prompt injection.\n *\n * Mirrors `UnifiedContextResponse.format_for_prompt`. Note the two different\n * bracket styles: facts and episodes only ever carry `[scope: x]` appended\n * directly, while preferences, emotions and temporal events build a joined\n * extras list. That asymmetry is in Python and is preserved.\n */\nexport function formatForPrompt(ctx: UnifiedContext, options: FormatOptions = {}): string {\n const includeScope = options.includeScope ?? false;\n const includeConversationContext = options.includeConversationContext ?? true;\n const sections: string[] = [];\n\n // A ranked response is one numbered list in rank order, as in Python: the\n // typed sections below would split the order back up by type.\n const ranked = ctx.items ?? [];\n if (ranked.length > 0) {\n const lines = ranked.map((it, n) => {\n let label = rankedLabel(String(it['category'] ?? ''));\n if (includeScope) label += `, scope: ${scopeOf(ctx, it)}`;\n return `${n + 1}. [${label}] ${String(it.content ?? '')}`;\n });\n sections.push(`### Memories\\n${lines.join('\\n')}`);\n }\n const typed = ranked.length === 0;\n\n if (typed && ctx.facts.length > 0) {\n const lines = ctx.facts.map((f) => {\n let line = `- ${String(f.content ?? '')}`;\n if (includeScope) line += ` [scope: ${scopeOf(ctx, f)}]`;\n return line;\n });\n sections.push(`### Facts\\n${lines.join('\\n')}`);\n }\n\n if (typed && ctx.preferences.length > 0) {\n const lines = ctx.preferences.map((p) => {\n const extras: string[] = [];\n if (includeScope) extras.push(`scope: ${scopeOf(ctx, p)}`);\n const strength = numberOf(p['strength']);\n // Python: `if p.strength and p.strength >= 0.7` -- 0 is falsy there, so a\n // zero strength is omitted rather than printed.\n if (strength !== null && strength !== 0 && strength >= 0.7) {\n extras.push(`strength: ${oneDecimal(strength)}`);\n }\n return bulletWithExtras(String(p.content ?? ''), extras);\n });\n sections.push(`### Preferences\\n${lines.join('\\n')}`);\n }\n\n if (typed && ctx.episodes.length > 0) {\n const lines = ctx.episodes.map((e) => {\n // Episodes render `summary`, not `content`.\n let line = `- ${String(e['summary'] ?? '')}`;\n if (includeScope) line += ` [scope: ${scopeOf(ctx, e)}]`;\n return line;\n });\n sections.push(`### Episodes\\n${lines.join('\\n')}`);\n }\n\n if (typed && ctx.emotions.length > 0) {\n const lines = ctx.emotions.map((em) => {\n const extras: string[] = [];\n if (includeScope) extras.push(`scope: ${scopeOf(ctx, em)}`);\n const intensity = numberOf(em['intensity']);\n if (intensity !== null && intensity !== 0 && intensity >= 0.5) {\n extras.push(`intensity: ${oneDecimal(intensity)}`);\n }\n // Emotions render \"type: context\", not a bare content field.\n const text = `${String(em['emotion_type'] ?? '')}: ${String(em['context'] ?? '')}`;\n return bulletWithExtras(text, extras);\n });\n sections.push(`### Emotions\\n${lines.join('\\n')}`);\n }\n\n if (typed && ctx.temporal_events.length > 0) {\n const lines = ctx.temporal_events.map((te) => {\n const extras: string[] = [];\n if (includeScope) extras.push(`scope: ${scopeOf(ctx, te)}`);\n const validUntil = isoDate(te['valid_until']);\n if (validUntil !== null) extras.push(`valid until: ${validUntil}`);\n return bulletWithExtras(String(te.content ?? ''), extras);\n });\n sections.push(`### Temporal Events\\n${lines.join('\\n')}`);\n }\n\n if (includeConversationContext && ctx.conversation_context !== null) {\n const formatted = ctx.conversation_context['formatted_context'];\n if (typeof formatted === 'string' && formatted !== '') {\n sections.push(`### Conversation History\\n${formatted}`);\n }\n }\n\n const blocks: string[] = [];\n if (sections.length > 0) blocks.push(`## User Context\\n${sections.join('\\n\\n')}`);\n\n const profileBlock = formatProfileBlock(ctx.profile);\n if (profileBlock !== null) blocks.push(profileBlock);\n\n const conversationsBlock = formatConversationsBlock(ctx.conversations);\n if (conversationsBlock !== null) blocks.push(conversationsBlock);\n\n return blocks.length > 0 ? blocks.join('\\n\\n') : '';\n}\n\n/** `## Caller Profile`, or null when there is nothing to show. */\nfunction formatProfileBlock(profile: Json | null): string | null {\n if (profile === null) return null;\n const attributes = profile['attributes'];\n const lines: string[] = [];\n if (typeof attributes === 'object' && attributes !== null) {\n for (const [name, attr] of Object.entries(attributes as Json)) {\n const value =\n typeof attr === 'object' && attr !== null\n ? (attr as Json)['value']\n : undefined;\n let valueStr: string;\n if (Array.isArray(value)) valueStr = value.map((v) => String(v)).join(', ');\n else if (value === null || value === undefined) valueStr = '';\n else valueStr = String(value);\n // Python does `f\"- {name}: {value}\".rstrip()`, so an empty value leaves\n // \"- name:\" with no trailing space.\n lines.push(`- ${name}: ${valueStr}`.replace(/\\s+$/, ''));\n }\n }\n const bodyParts: string[] = [];\n if (lines.length > 0) bodyParts.push(lines.join('\\n'));\n const overview = profile['overview'];\n if (typeof overview === 'string' && overview !== '') bodyParts.push(overview);\n if (bodyParts.length === 0) return null;\n return `## Caller Profile\\n${bodyParts.join('\\n\\n')}`;\n}\n\n/** `## Previous Conversations`, or null. */\nfunction formatConversationsBlock(conversations: Json[] | null): string | null {\n if (conversations === null || conversations.length === 0) return null;\n const callBlocks = conversations.map((conv) => {\n const when =\n isoDate(conv['started_at']) ?? isoDate(conv['last_message_at']) ?? isoDate(conv['ended_at']);\n let header = `### Call on ${when ?? 'unknown date'}`;\n const convType = conv['conversation_type'];\n if (typeof convType === 'string' && convType !== '') header += ` (${convType})`;\n const callLines = [header];\n const overview = overviewText(conv);\n if (overview !== null) callLines.push(`Overview: ${overview}`);\n const outcome = outcomeText(conv);\n if (outcome !== null) callLines.push(`Outcome: ${outcome}`);\n if (overview === null && outcome === null) {\n // No compaction yet, or it failed. Still tell the model the call\n // happened so it can acknowledge the prior contact.\n // Python's model defaults summary_status to \"pending\", so an absent\n // field renders as \"pending\", not as an empty string.\n callLines.push(`Status: ${String(conv['summary_status'] ?? 'pending')}`);\n }\n return callLines.join('\\n');\n });\n return `## Previous Conversations\\n${callBlocks.join('\\n\\n')}`;\n}\n\n/**\n * Mirrors `ConversationSummaryModel.overview_text()`.\n *\n * Reads the nested `summary` OBJECT (not a string) and takes the first of four\n * keys that holds non-blank text, in this exact order.\n */\nfunction overviewText(conv: Json): string | null {\n const summary = conv['summary'];\n if (typeof summary !== 'object' || summary === null || Array.isArray(summary)) return null;\n for (const key of ['narrative_summary', 'overview', 'narrative', 'summary'] as const) {\n const val = (summary as Json)[key];\n if (typeof val === 'string' && val.trim() !== '') return val.trim();\n }\n return null;\n}\n\n/**\n * Mirrors `ConversationSummaryModel.outcome_text()`.\n *\n * Joins up to three classification fields with \" / \", skipping blanks, and\n * returns null when nothing survives.\n */\nfunction outcomeText(conv: Json): string | null {\n const classification = conv['classification'];\n if (typeof classification !== 'object' || classification === null || Array.isArray(classification)) {\n return null;\n }\n const c = classification as Json;\n const parts = [c['primary_category'], c['subcategory'], c['objective']]\n .filter((p): p is string => typeof p === 'string' && p.trim() !== '');\n return parts.length > 0 ? parts.join(' / ') : null;\n}\n","/**\n * `client.as_tool(...)`: an LLM-ready tool definition for fetching context.\n *\n * Ports Python's `MaximemSynapSDK.as_tool` plus its three module helpers.\n *\n * The point of the helper is that the scope identifiers are CLOSED OVER rather\n * than exposed in the JSON schema. The LLM chooses the query and the limits; it\n * cannot choose whose memory to read. Handing the model a `user_id` parameter\n * is how the per-user privacy filter gets dropped by accident, so the schema\n * deliberately has no such field.\n */\n\nimport { InvalidInputError } from '../errors.js';\nimport type { Json } from '../context/types.js';\n\nexport type ToolScope = 'conversation' | 'user' | 'customer' | 'client' | 'unified';\nexport type ToolStyle = 'openai' | 'anthropic';\n\nconst VALID_SCOPES: readonly ToolScope[] = ['conversation', 'user', 'customer', 'client', 'unified'];\n\nexport interface AsToolOptions {\n scope?: string;\n user_id?: string;\n userId?: string;\n customer_id?: string;\n customerId?: string;\n conversation_id?: string;\n conversationId?: string;\n name?: string;\n description?: string;\n style?: string;\n}\n\nexport type ToolHandler = (callArgs?: Json) => Promise<Json>;\n\nexport interface OpenAiToolDefinition {\n type: 'function';\n function: { name: string; description: string; parameters: Json };\n handler: ToolHandler;\n}\n\nexport interface AnthropicToolDefinition {\n name: string;\n description: string;\n input_schema: Json;\n handler: ToolHandler;\n}\n\nexport type ToolDefinition = OpenAiToolDefinition | AnthropicToolDefinition;\n\nconst BASE_DESCRIPTION =\n 'Retrieve stored context (facts, preferences, recent episodes, ' +\n 'emotions, temporal events) from Synap memory.';\n\n/** Python's `_default_tool_description`. Wording is load-bearing: it primes the model to call the tool. */\nexport function defaultToolDescription(scope: ToolScope): string {\n switch (scope) {\n case 'conversation':\n return (\n BASE_DESCRIPTION +\n ' Scoped to a specific conversation. Call this when you ' +\n 'need facts already established in the current conversation, ' +\n 'or to reload context after a topic shift.'\n );\n case 'user':\n return (\n BASE_DESCRIPTION +\n \" Scoped to the user's long-term memory. Call this when \" +\n 'you need to recall who the user is, their preferences, or ' +\n 'their history across conversations.'\n );\n case 'customer':\n return (\n BASE_DESCRIPTION +\n ' Scoped to the customer/organization. Call this when ' +\n 'you need facts about the customer org rather than an individual.'\n );\n case 'client':\n return (\n BASE_DESCRIPTION +\n ' Scoped to the integrating client/product. Call this for ' +\n 'product-level knowledge (e.g. policies, documentation).'\n );\n default:\n return (\n BASE_DESCRIPTION +\n ' Cross-scope: merges conversation, user, customer, and ' +\n 'client memory in one call. Call this at the start of a turn when ' +\n 'you need general grounding before responding.'\n );\n }\n}\n\n/**\n * Python's `_tool_input_schema`.\n *\n * `conversation_id` appears ONLY for `scope=\"conversation\"` when it was not\n * closed over, in which case it is also the one required field.\n */\nexport function toolInputSchema(scope: ToolScope, options: { hasConversationId: boolean }): Json {\n const props: Json = {\n search_query: {\n type: 'array',\n items: { type: 'string' },\n description:\n 'Optional list of search queries describing what context ' +\n 'you need. Free-form natural language is fine. Omit to ' +\n 'retrieve the most relevant recent context.',\n },\n max_results: {\n type: 'integer',\n minimum: 1,\n maximum: 50,\n description: 'Maximum items to return (default 10).',\n },\n types: {\n type: 'array',\n items: {\n type: 'string',\n enum: ['facts', 'preferences', 'episodes', 'emotions', 'temporal_events'],\n },\n description: 'Filter to specific memory categories. Omit to retrieve all.',\n },\n mode: {\n type: 'string',\n enum: ['fast', 'accurate'],\n description:\n \"Retrieval mode. 'fast' = low-latency (~50ms); 'accurate' = \" +\n \"LLM-decomposed multi-query (~200-500ms). Default 'fast'.\",\n },\n precision_level: {\n type: 'string',\n enum: ['high', 'medium'],\n description:\n \"Result filtering precision. 'high' (default) applies an extra \" +\n \"relevance-refinement pass; 'medium' skips it for faster, less \" +\n 'precisely filtered results (recall unaffected).',\n },\n };\n\n let required: string[] = [];\n if (scope === 'conversation' && !options.hasConversationId) {\n props['conversation_id'] = {\n type: 'string',\n description: 'The conversation id to fetch context for.',\n };\n required = ['conversation_id'];\n }\n\n return { type: 'object', properties: props, required, additionalProperties: false };\n}\n\n/** What `as_tool` needs from the client, so this module stays testable in isolation. */\nexport interface ToolDispatchTarget {\n fetch(options: Json): Promise<{\n formatted_context: string;\n scopes_queried: string[];\n total_items: number;\n }>;\n conversation: { context: { fetch(options: Json): Promise<Json> } };\n user: { context: { fetch(options: Json): Promise<Json> } };\n customer: { context: { fetch(options: Json): Promise<Json> } };\n client: { context: { fetch(options: Json): Promise<Json> } };\n}\n\nconst RESULT_COLLECTIONS = ['facts', 'preferences', 'episodes', 'emotions', 'temporal_events'] as const;\n\n/**\n * Python's `_invoke_scope_fetch`.\n *\n * Returns a plain object so the host runtime can JSON-serialise it straight\n * into the LLM's tool-result message.\n */\nexport async function invokeScopeFetch(params: {\n target: ToolDispatchTarget;\n scope: ToolScope;\n userId: string | undefined;\n customerId: string | undefined;\n conversationId: string | undefined;\n callArgs: Json;\n}): Promise<Json> {\n const { target, scope, userId, customerId, conversationId, callArgs } = params;\n\n const searchQuery = callArgs['search_query'];\n // Python: `int(call_args.get(\"max_results\") or 10)` -- so 0 falls back to 10.\n const rawMax = callArgs['max_results'];\n const maxResults = typeof rawMax === 'number' && rawMax !== 0 ? Math.trunc(rawMax) : 10;\n const types = callArgs['types'];\n const mode = String(callArgs['mode'] ?? '') || 'fast';\n const precisionLevel = String(callArgs['precision_level'] ?? '') || 'high';\n const callConvId =\n (typeof callArgs['conversation_id'] === 'string' && callArgs['conversation_id'] !== ''\n ? (callArgs['conversation_id'] as string)\n : undefined) ?? conversationId;\n\n const shared: Json = {\n search_query: searchQuery,\n max_results: maxResults,\n types,\n mode,\n precision_level: precisionLevel,\n };\n\n if (scope === 'unified') {\n const response = await target.fetch({\n conversation_id: callConvId,\n user_id: userId,\n customer_id: customerId,\n ...shared,\n });\n return {\n formatted_context: response.formatted_context,\n scopes_queried: response.scopes_queried,\n total_items: response.total_items,\n };\n }\n\n let response: Json;\n if (scope === 'conversation') {\n if (callConvId === undefined || callConvId === '') {\n // Python returns this as DATA, not an exception: the LLM sees the error\n // string in its tool result and can retry with an id.\n return { error: 'conversation_id is required', items: [] };\n }\n response = await target.conversation.context.fetch({\n conversation_id: callConvId,\n ...shared,\n user_id: userId,\n });\n } else if (scope === 'user') {\n response = await target.user.context.fetch({\n user_id: userId,\n conversation_id: callConvId,\n ...shared,\n customer_id: customerId,\n });\n } else if (scope === 'customer') {\n response = await target.customer.context.fetch({\n customer_id: customerId,\n conversation_id: callConvId,\n ...shared,\n });\n } else {\n response = await target.client.context.fetch({\n conversation_id: callConvId,\n ...shared,\n });\n }\n\n const out: Json = {};\n for (const collection of RESULT_COLLECTIONS) {\n out[collection] = Array.isArray(response[collection]) ? response[collection] : [];\n }\n return out;\n}\n\n/**\n * Build the tool definition.\n *\n * `onWarning` receives Python's `logger.warning` for the\n * `scope=\"conversation\"` without `user_id` case, which is a soft warning\n * rather than an error.\n */\nexport function buildTool(\n target: ToolDispatchTarget,\n options: AsToolOptions = {},\n onWarning: (message: string) => void = () => {},\n): ToolDefinition {\n const scope = String(options.scope ?? 'user').toLowerCase() as ToolScope;\n if (!VALID_SCOPES.includes(scope)) {\n // Python formats the valid set with sorted() and repr() on the input.\n const sorted = [...VALID_SCOPES].sort();\n throw new InvalidInputError(\n `scope must be one of ['${sorted.join(\"', '\")}'], got '${String(options.scope)}'`,\n );\n }\n\n const userId = options.user_id ?? options.userId;\n const customerId = options.customer_id ?? options.customerId;\n const conversationId = options.conversation_id ?? options.conversationId;\n\n if (scope === 'user' && (userId === undefined || userId === '')) {\n throw new InvalidInputError(\"scope='user' requires user_id\");\n }\n if (scope === 'customer' && (customerId === undefined || customerId === '')) {\n throw new InvalidInputError(\"scope='customer' requires customer_id\");\n }\n if (scope === 'conversation' && (userId === undefined || userId === '')) {\n // Not a hard error: the anticipation cache refuses cross-user matches\n // anyway. But the caller is leaving privacy on the floor, so say so.\n onWarning(\n \"as_tool(scope='conversation') without user_id: the SDK \" +\n 'anticipation cache cannot apply per-user filtering. Pass ' +\n 'user_id to enable the Section 15 privacy guarantee.',\n );\n }\n\n const style = String(options.style ?? 'openai');\n if (style !== 'openai' && style !== 'anthropic') {\n throw new InvalidInputError(`style must be 'openai' or 'anthropic', got '${style}'`);\n }\n\n const toolName = options.name ?? `synap_fetch_${scope}_context`;\n const toolDesc = options.description ?? defaultToolDescription(scope);\n const schema = toolInputSchema(scope, { hasConversationId: conversationId !== undefined });\n\n const handler: ToolHandler = (callArgs: Json = {}) =>\n invokeScopeFetch({ target, scope, userId, customerId, conversationId, callArgs });\n\n if (style === 'openai') {\n return {\n type: 'function',\n function: { name: toolName, description: toolDesc, parameters: schema },\n handler,\n };\n }\n return { name: toolName, description: toolDesc, input_schema: schema, handler };\n}\n","/**\n * SynapClient: the public entry point.\n *\n * Exposes two surfaces, both live in production:\n *\n * - **Namespaced, snake_case** (`client.user.context.fetch(...)`) is the\n * canonical surface and mirrors the Python SDK method for method. It\n * returns raw snake_case.\n * - **Top-level camelCase** (`client.fetchUserContext(...)`) is deprecated but\n * fully supported. It returns the normalised camelCase shape, and it exists\n * because the 0.3.x wrapper had it.\n *\n * They return different shapes on purpose. See gotcha G-B.\n */\n\nimport type { AgentModel } from './agent-model.js';\nimport { InvalidInputError } from './errors.js';\nimport { getEnv } from './util/env.js';\nimport { HttpTransport, type Credentials, type HttpTransportOptions } from './transport/http.js';\nimport { fetchContext, responseShapeOf } from './context/fetch.js';\nimport { normalizeContextResponse } from './context/normalize.js';\nimport {\n AnticipationCache, type AnticipationCacheSnapshot,\n} from './context/anticipation-cache.js';\nimport { flattenContextItems, type FlatMemory } from './context/flatten.js';\nimport {\n createMemoriesInterface, type CreateMemoryOptions, type CreateMemoryResult,\n type MemoriesInterface,\n} from './memories/interface.js';\nimport {\n createConversationNamespace, CompactionSubscribers, type ConversationNamespace,\n} from './conversation/interface.js';\nimport { createUserNamespace, type UserNamespace } from './user/interface.js';\nimport { createCreditsNamespace, type CreditsNamespace } from './credits/interface.js';\nimport { createCacheNamespace, type CacheNamespace } from './cache/interface.js';\nimport { createInstanceNamespace, type InstanceNamespace } from './instance/interface.js';\nimport type { FetchOptions, Json, NormalisedContext, RawContext } from './context/types.js';\nimport { SDK_VERSION } from './version.js';\nimport * as registry from './registry.js';\nimport { ShortTermStore, isComplete, type ShortTermTurn } from './cache/short-term-store.js';\nimport { TurnStore, type TurnInput } from './cache/turn-store.js';\nimport { renderExcerpts, searchTurns, toolResultText, type Encoder } from './cache/history-search.js';\nimport { SEMANTIC_MATCH } from './behavior/contract.js';\nimport { overlayLocalRecentTurns } from './context/overlay.js';\nimport { renderForPrompt, renderCompacted } from './context/local-prompt.js';\nimport { TurnCounter, mergeUserSummary } from './context/user-summary.js';\nimport {\n tryServeFromCache, servedItemIds, distinctQueryCount, type Scope as AnticipationScope,\n} from './context/anticipated.js';\nimport { rungKey } from './context/anticipation-cache.js';\nimport type { FetchHooks } from './context/fetch.js';\nimport { buildAssembledEvent } from './context/assembled.js';\nimport { newCorrelationId } from './util/correlation.js';\nimport { validateConversationId, validateInstanceId } from './util/validators.js';\nimport {\n mergeScopeResults, formatForPrompt, type UnifiedContext,\n} from './context/unified.js';\nimport { buildTool, type AsToolOptions, type ToolDefinition } from './tool/as-tool.js';\nimport { checkCustomerId } from './scoping.js';\n\n/** Levels the SDK emits. Mirrors the subset of Python's logging it uses. */\nexport type SynapLogLevel = 'debug' | 'info' | 'warn' | 'error';\n\n/**\n * Receives every diagnostic the SDK would otherwise write to the console.\n *\n * Python routes these through the stdlib `logging` module, which is why it\n * takes `log_level` and `logger`. There is no global logger in JavaScript, so\n * the SDK takes the sink directly instead of a level plus a framework.\n */\nexport type SynapLogger = (level: SynapLogLevel, message: string) => void;\n\nexport interface SynapClientOptions {\n /**\n * The four identity fields are `| undefined` rather than plain optional so a\n * consumer with `exactOptionalPropertyTypes` can pass `process.env.X`\n * directly, which is what every example does. Undefined already means \"fall\n * back to the environment\" at runtime; this makes the type say so.\n */\n apiKey?: string | undefined;\n clientId?: string | undefined;\n instanceId?: string | undefined;\n baseUrl?: string | undefined;\n /** Python spells this `api_base_url`; both are accepted so one config\n * object works against either SDK. */\n api_base_url?: string | undefined;\n timeouts?: HttpTransportOptions['timeouts'];\n retryPolicy?: HttpTransportOptions['retryPolicy'];\n keepAlive?: HttpTransportOptions['keepAlive'];\n /**\n * Keep the connection warm with a periodic /health ping.\n *\n * Off by default. A clear win for a long-lived process, pure waste in\n * serverless where the container is frozen between requests anyway.\n */\n heartbeat?: boolean;\n fetchImpl?: typeof fetch;\n /**\n * Splice locally-recorded turns into the next conversation-scope fetch, so a\n * just-written turn is visible before the server compacts. Defaults to true,\n * matching Python's `st_verbatim_overlay`. `SYNAP_ST_VERBATIM_OVERLAY`\n * overrides this either way.\n */\n st_verbatim_overlay?: boolean | undefined;\n stVerbatimOverlay?: boolean | undefined;\n /**\n * The model the agent runs on, and optionally its context window. Sent with\n * every recorded turn so the server sizes compaction to the agent's window\n * (it compacts at ~11.7% of it: 15K tokens on a 128K model). A name is\n * enough; the server knows common models' windows. Unset = 128K. Python's\n * `SDKConfig.agent_model` / `agent_context_window`; per-call `model` /\n * `context_window` override them.\n */\n agent_model?: string | undefined;\n agentModel?: string | undefined;\n agent_context_window?: number | undefined;\n agentContextWindow?: number | undefined;\n\n /**\n * Let the SDK answer `get_context_for_prompt` and `get_compacted` from its\n * own short-term store when the conversation is warm, skipping the cloud\n * round trip. Off by default. `SYNAP_SDK_ST_AUTHORITATIVE` overrides this in\n * either direction.\n *\n * Leave it off for an application that runs more than one process against\n * the same conversation. A process learns the server's message count only\n * when it writes. If it reads before it writes (build the prompt, then\n * record the turn), it cannot know that another process recorded a turn in\n * between, and it answers without that turn. Turn it on where one process\n * owns a conversation for its whole life, such as a voice agent.\n *\n * This is the COST switch. `st_verbatim_overlay` is the correctness one and\n * is independent of it.\n */\n sdk_st_authoritative?: boolean | undefined;\n sdkStAuthoritative?: boolean | undefined;\n\n /**\n * Your own embedding model for history search after compaction\n * (cache/history-search.ts), any `{ model, encode }`. It replaces the\n * SDK's model for history search; the anticipation cache uses it only if it\n * is the model its thresholds were tuned on (`history_search_model`).\n */\n history_search_encoder?: Encoder | undefined;\n historySearchEncoder?: Encoder | undefined;\n\n /**\n * The local embedding model, as Python's `history_search_model`: history\n * search after compaction, and matching fetch queries to prefetched items by\n * meaning. \"auto\" (default) runs the SDK's multilingual model in Node when\n * the optional `onnxruntime-node` and `@huggingface/tokenizers` packages are\n * installed: downloaded once (about 135 MB) into ~/.synap/models when the\n * first stream starts or the first history search runs, then loaded in the\n * background. An \"onnx:<repo>@<revision>/<file>\" names another (history\n * search only). null keeps keyword matching. Never loaded on Edge or in a\n * browser.\n */\n history_search_model?: string | null | undefined;\n historySearchModel?: string | null | undefined;\n\n /**\n * Where the SDK's diagnostics go. Defaults to `console.warn` prefixed with\n * `[synap]`. Pass a no-op to silence them, or your own logger to route them.\n */\n logger?: SynapLogger | undefined;\n\n /**\n * Bypass the process registry and build a genuinely separate client.\n *\n * Underscored because it mirrors Python's `_force_new` and is not part of the\n * everyday surface. Needed in tests, and for the rare case of deliberately\n * running two clients on one identity with separate caches.\n */\n _force_new?: boolean;\n}\n\ninterface ScopedContext {\n context: { fetch: (options?: FetchOptions) => Promise<RawContext> };\n}\n\n/**\n * Options for `configure()`.\n *\n * The Python-only keys are declared so a config object can be shared between\n * the two SDKs without a type error. They are accepted and ignored: there is\n * no SQLite cache backend and no global logger to replace here.\n */\nexport interface ConfigureOptions {\n timeouts?: HttpTransportOptions['timeouts'];\n retryPolicy?: HttpTransportOptions['retryPolicy'] | null;\n retry_policy?: HttpTransportOptions['retryPolicy'] | null;\n /** Accepted and ignored. Python-only. */\n storage_path?: string;\n /** Accepted and ignored. Python-only. */\n cache_backend?: string | null;\n /** Accepted and ignored. Python-only. */\n session_timeout_minutes?: number;\n /** Accepted and ignored. Python-only. */\n log_level?: string;\n /** The agent's model; see `SynapClientOptions.agent_model`. */\n agent_model?: string;\n agentModel?: string;\n agent_context_window?: number;\n agentContextWindow?: number;\n /** Replaces the diagnostic sink, same as the constructor option. */\n logger?: SynapLogger | undefined;\n}\n\n/** Options for the cross-scope `fetch()`. Snake_case, matching Python's kwargs.\n *\n * The three identifiers accept `null` as well as `undefined`, because that is\n * what callers actually pass: `conversation_id: convId || null` is the normal\n * way to say \"there is no conversation here\", and `validateConversationId`\n * has always treated `null` as absent. The declared type said otherwise, so\n * the `SynapClient`-to-`SynapSdkLike` assignability test in\n * `synap-claude-agent` (TS) could not typecheck, and the scope guards below\n * did not agree with the validator either. See the guards for that half.\n */\nexport interface UnifiedFetchOptions {\n conversation_id?: string | null;\n user_id?: string | null;\n customer_id?: string | null;\n search_query?: string[];\n max_results?: number;\n types?: string[];\n mode?: string;\n precision_level?: string;\n /** The moment the question is being asked. See FetchOptions.query_date. */\n query_date?: string;\n /** A share of the response per search query. See FetchOptions.max_results_per_query. */\n max_results_per_query?: number;\n include_conversation_context?: boolean;\n scopes?: string[];\n include_scope_labels?: boolean;\n context_mode?: string;\n include_profile?: boolean;\n last_n_conversations?: number;\n /**\n * `\"by_type\"` (default) or `\"ranked\"`. Ranked asks every scope for its\n * ranked list and merges them into `items`, best first across scopes and\n * types, by each item's `rank_score`; `formatted_context` is then one\n * numbered list in that order. The typed lists are filled either way.\n */\n response_shape?: string;\n}\n\n/** `SYNAP_SDK_LOCAL_MODEL=off` keeps the local embedding model from loading. Python: the same. */\nfunction localModelOff(): boolean {\n return ['off', '0', 'false', 'no', 'none'].includes((getEnv('SYNAP_SDK_LOCAL_MODEL') ?? '').trim().toLowerCase());\n}\n\nexport class SynapClient {\n // Native-private (`#`), not TypeScript `private`. TS `private` is erased at\n // compile time, so the field stays enumerable at runtime and a JS consumer\n // can still reach it. These two were public and made `client.transport\n // .credentials.apiKey` and the raw cache's `now`/`fire`/`invalidateEntity`\n // reachable, which is exactly what `client.cache` exists to prevent.\n // Definite-assignment assertions (`!`) because the constructor can RETURN an\n // existing client from the registry before reaching any of these. TypeScript\n // cannot follow a constructor return, so it sees a path where the fields are\n // unassigned. On that path the object is discarded and the incumbent is\n // handed back instead, so nothing observes them.\n readonly #transport!: HttpTransport;\n readonly #anticipationCache!: AnticipationCache;\n\n readonly user!: UserNamespace;\n readonly customer!: ScopedContext;\n readonly client!: ScopedContext;\n readonly conversation!: ConversationNamespace;\n readonly memories!: MemoriesInterface;\n readonly credits!: CreditsNamespace;\n readonly cache!: CacheNamespace;\n readonly instance!: InstanceNamespace;\n\n #closed = false;\n #initialized = false;\n readonly #registryKeys!: string[];\n readonly #compactionSubscribers!: CompactionSubscribers;\n readonly #shortTerm!: ShortTermStore;\n /** Every raw turn, past compaction, for local history search. */\n readonly #turnStore!: TurnStore;\n /** A local embedding model for history search; null = keywords only. */\n #localEncoder: Encoder | null = null;\n /** `history_search_model`: \"auto\", an \"onnx:...\" name, or null for none. */\n #localModelName: string | null = 'auto';\n #localModelStarted = false;\n readonly #backfillAfter = new Map<string, number>();\n readonly #stVerbatimOverlay!: boolean | undefined;\n #agentModel: AgentModel = {};\n #logger!: SynapLogger | undefined;\n readonly #stAuthoritative!: boolean | undefined;\n readonly #turns!: TurnCounter;\n readonly #fetchHooks!: FetchHooks;\n\n constructor(options: SynapClientOptions = {}) {\n const credentials = resolveCredentials(options);\n const resolvedBaseUrl = resolveBaseUrl(options);\n // Fail locally on a malformed id rather than sending it upstream.\n validateInstanceId(credentials.instanceId);\n\n // ── Registry: one client per identity, per process ──────────────────────\n // Python has shared state here since 0.2. Without it, a framework\n // integration that builds a client per request or per agent gets a cold\n // anticipation cache every time and pays for a retrieval that Python would\n // have served locally.\n //\n // Returning from a constructor is unusual in JS but legal, and it is the\n // closest equivalent of Python's `self.__dict__ = existing.__dict__`. The\n // two callers then hold the SAME object rather than two objects sharing a\n // state bag, which is stricter than Python.\n const registryKey = registry.buildRegistryKey(credentials.instanceId, credentials.apiKey);\n if (options._force_new !== true) {\n const existing = registry.get(registryKey) as SynapClient | undefined;\n if (existing !== undefined) {\n // Handing back a client built on a DIFFERENT credential than the caller\n // just supplied. Only reachable on the explicit instanceId path, where\n // the id is the identity and the key is not part of it. The caller has\n // no other way to find out: the returned object looks exactly like the\n // one they asked for.\n const incumbent = existing.#transport.currentCredentials().apiKey;\n if (credentials.apiKey && incumbent && credentials.apiKey !== incumbent) {\n existing.#warn(\n `Instance ${registryKey} is already running in this process on a different ` +\n 'API key. The key passed here is ignored; this client will authenticate ' +\n 'as the existing one. Rotating a key this way has no effect. Construct ' +\n 'with apiKey and no instanceId to get a client on your own credential.',\n );\n }\n return existing;\n }\n }\n this.#registryKeys = options._force_new === true ? [] : [registryKey];\n\n this.#transport = new HttpTransport({\n credentials,\n // SYNAP_BASE_URL is honoured, as Python does (sdk.py) and as the 0.3.x\n // wrapper did. Ignoring it was a wrong-DESTINATION bug rather than a\n // missing convenience: anyone pointing at a self-hosted or staging\n // deployment through the environment would silently have started sending\n // their data to production on upgrade.\n ...(resolvedBaseUrl !== undefined ? { baseUrl: resolvedBaseUrl } : {}),\n ...(options.timeouts !== undefined ? { timeouts: options.timeouts } : {}),\n ...(options.retryPolicy !== undefined ? { retryPolicy: options.retryPolicy } : {}),\n ...(options.keepAlive !== undefined ? { keepAlive: options.keepAlive } : {}),\n ...(options.fetchImpl !== undefined ? { fetchImpl: options.fetchImpl } : {}),\n userAgent: `@maximem/synap-js-sdk/${SDK_VERSION}`,\n });\n if (options.heartbeat) this.#transport.startHeartbeat();\n\n this.#anticipationCache = new AnticipationCache();\n\n this.#fetchHooks = {\n beforeFetch: async (scope, options) => {\n const o = options as Record<string, unknown>;\n // A prefetched bundle was assembled with no period restriction, so it\n // cannot answer a request that names one. Same class as the scope_path\n // defect described below: a bundle built for one question served to a\n // different one, entirely inside this process and invisible to every\n // server log. Skipping the cache costs one round trip; serving a bundle\n // that ignores the date costs the answer.\n if (o['query_date'] ?? o['queryDate']) return null;\n // A prefetched bundle was not assembled under a per-query share\n // either, and its items do not say which query found them.\n if (o['max_results_per_query'] ?? o['maxResultsPerQuery']) return null;\n const attempt = await tryServeFromCache(\n this.#anticipationCache,\n scope as AnticipationScope,\n {\n searchQuery: (o['search_query'] ?? o['searchQuery']) as string[] | undefined,\n entityId: (o['user_id'] ?? o['userId']) as string | undefined,\n customerId: (o['customer_id'] ?? o['customerId']) as string | undefined,\n clientId: this.#transport.currentCredentials().clientId || null,\n conversationId: (o['conversation_id'] ?? o['conversationId']) as string | undefined,\n // The rung this read is addressed to, in the same words the server\n // stamped on the bundles it pushed. Without it the cache matched\n // on entity and conversation alone, so a bundle prefetched for one\n // rung answered a caller standing at another, entirely inside this\n // process and invisible to every server log.\n scopeRung: rungKey(\n (o['scope_path'] ?? o['scopePath']) as Record<string, string> | undefined,\n ),\n },\n Number(o['max_results'] ?? o['maxResults'] ?? 10),\n );\n if (attempt.response === null || attempt.hit === null) return null;\n return {\n response: attempt.response,\n servedItemIds: servedItemIds(attempt.hit),\n bundleId: attempt.hit.bundleIds[0] ?? '',\n borderline: attempt.hit.band === 'borderline',\n raceDeadlineMs: SEMANTIC_MATCH.race_deadline_ms,\n };\n },\n onAssembled: (scope, options, response, startedAt) => {\n this.#emitContextAssembled(scope, options as Record<string, unknown>, response, startedAt);\n },\n onServedFromCache: (_scope, options, served) => {\n // Fire-and-forget over the Listen stream. Drives the server's learning\n // loop: per-prefetch outcome scoring, per-pattern hit rates. Carries\n // ids and scope only, never prompt content.\n this.#emitContextUsed(options as Record<string, unknown>, _scope, served);\n },\n };\n\n this.user = createUserNamespace(this.#transport, this.#fetchHooks);\n // `async` arrows, not bare ones. fetchContext is already async so nothing\n // can throw synchronously today, but these are the only two namespace\n // methods that were not async, and the next person to add a validation\n // line above the call would reintroduce a throw that bypasses .catch().\n this.customer = { context: { fetch: async (o = {}) => fetchContext(this.#transport, 'customer', o, this.#fetchHooks) } };\n this.client = { context: { fetch: async (o = {}) => fetchContext(this.#transport, 'client', o, this.#fetchHooks) } };\n this.#compactionSubscribers = new CompactionSubscribers();\n this.#shortTerm = new ShortTermStore();\n // Every raw turn, kept past compaction for local search (turn-store.ts).\n this.#turnStore = new TurnStore();\n this.#shortTerm.attachArchive(this.#turnStore);\n this.#stVerbatimOverlay = options.st_verbatim_overlay ?? options.stVerbatimOverlay;\n this.#agentModel = {\n model: options.agent_model ?? options.agentModel,\n contextWindow: options.agent_context_window ?? options.agentContextWindow,\n };\n this.#logger = options.logger;\n this.#stAuthoritative = options.sdk_st_authoritative ?? options.sdkStAuthoritative;\n this.#localEncoder = options.history_search_encoder ?? options.historySearchEncoder ?? null;\n const modelOption = options.history_search_model !== undefined\n ? options.history_search_model\n : options.historySearchModel;\n this.#localModelName = modelOption === undefined ? 'auto' : modelOption;\n if (this.#localEncoder?.model === SEMANTIC_MATCH.model) this.#anticipationCache.setEncoder(this.#localEncoder);\n this.#turns = new TurnCounter();\n this.conversation = createConversationNamespace(\n this.#transport,\n this.#compactionSubscribers,\n // Mirrors Python's `_invalidate_anticipation_on_write`, including its\n // gate: `invalidateEntity` is a no-op unless\n // SYNAP_SDK_CACHE_INVALIDATE_ON_WRITE is set, so default behaviour is\n // unchanged. Over-invalidating after a failed write is safe; it only\n // costs the next lookup a cold fetch.\n ({ userId, customerId }) => {\n for (const id of new Set([userId, customerId])) {\n if (id) this.#anticipationCache.invalidateEntity(id);\n }\n },\n // Buffer the turn locally so the next fetch sees it, ahead of the server\n // having persisted and compacted it.\n ({ conversationId, role, content, timestamp, messageId, serverMessageCount, inFlight }) => {\n this.#shortTerm.appendTurn(conversationId, role, content, timestamp, {\n messageId,\n serverMessageCount,\n inFlight,\n });\n },\n (response, conversationId, userId, scopePath) => {\n overlayLocalRecentTurns(response, this.#shortTerm, conversationId, this.#stVerbatimOverlay);\n this.#injectUserSummary(response, conversationId, userId, rungKey(scopePath));\n },\n {\n prompt: (conversationId, style) => this.#localPrompt(conversationId, style),\n compacted: (conversationId, format) => this.#localCompacted(conversationId, format),\n reseed: (conversationId, response) => this.#reseedShortTerm(conversationId, response),\n historyExcerpts: (conversationId, response, query) => this.#withHistoryExcerpts(conversationId, response, query),\n },\n () => this.#agentModel,\n );\n // After a forget that deleted something, drop that user's cached bundles so\n // they are not served until the TTL runs out. Unconditional, unlike the\n // invalidate-on-write hook: a forgotten memory must not be served.\n this.memories = createMemoriesInterface(this.#transport, ({ userId, customerId }) => {\n for (const id of new Set([userId, customerId])) {\n if (id) this.#anticipationCache.dropEntity(id);\n }\n });\n this.credits = createCreditsNamespace(this.#transport);\n this.cache = createCacheNamespace(this.#anticipationCache);\n this.instance = createInstanceNamespace(\n () => this.#transport.currentCredentials(),\n this.#anticipationCache,\n ({ conversationId, role, content }) => {\n this.#shortTerm.appendTurn(conversationId, role, content);\n },\n () => this.#agentModel,\n ({ conversationId, result, toolName }) => this.archive_tool_result(conversationId, result, { tool_name: toolName }),\n );\n // A compaction folds turns into the server's summary, so the local copies\n // of those turns must go or they keep being spliced back in.\n // Prefetched bundles start arriving with the stream: load the local model\n // then, not at construction, so a process that never listens or searches\n // never downloads it.\n this.instance._internal.setOnListen(() => this.#startLocalModel());\n this.instance._internal.setCompactionApplier((bundle) => {\n this.#shortTerm.applyCompaction(bundle);\n });\n // The server's message count after each streamed write, which is how the\n // store learns whether it holds the whole conversation.\n this.instance._internal.setConversationStateHandler((conversationId, messageCount) => {\n this.#shortTerm.noteServerCount(conversationId, messageCount);\n });\n\n if (options._force_new !== true) {\n // Claim the slot. A separate get-then-set would race in a threaded\n // runtime; JS is single-threaded between these statements, so this is\n // one operation for clarity rather than for locking.\n const incumbent = registry.registerIfAbsent(registryKey, this) as SynapClient | undefined;\n if (incumbent !== undefined) return incumbent;\n }\n // Route compaction bundles from the one Listen stream into the typed\n // per-conversation subscriptions. Wired here rather than inside either\n // namespace so the two do not have to import each other.\n this.instance._internal.setCompactionDispatcher((conversationId, bundle) => {\n this.#compactionSubscribers.dispatch(conversationId, bundle as Json, (error) => {\n this.#warn(`compaction subscriber threw: ${String(error)}`);\n });\n });\n }\n\n // ── Deprecated camelCase surface, carried over from the 0.3.x wrapper ───────\n // These return the NORMALISED shape, unlike their namespaced equivalents.\n\n /** @deprecated Use `client.user.context.fetch()`. Note it returns raw snake_case. */\n async fetchUserContext(options: FetchOptions = {}): Promise<NormalisedContext> {\n return normalizeContextResponse((await fetchContext(this.#transport, 'user', options)) as Json);\n }\n\n /** @deprecated Use `client.customer.context.fetch()`. */\n async fetchCustomerContext(options: FetchOptions = {}): Promise<NormalisedContext> {\n return normalizeContextResponse((await fetchContext(this.#transport, 'customer', options)) as Json);\n }\n\n /** @deprecated Use `client.client.context.fetch()`. */\n async fetchClientContext(options: FetchOptions = {}): Promise<NormalisedContext> {\n return normalizeContextResponse((await fetchContext(this.#transport, 'client', options)) as Json);\n }\n\n /** @deprecated Use `client.conversation.context.get_context_for_prompt()`. */\n async getContextForPrompt(\n options: { conversationId?: string; conversation_id?: string; style?: string } = {},\n ): Promise<Json> {\n return this.conversation.context.get_context_for_prompt(options);\n }\n\n /**\n * @deprecated Use `client.user.context.fetch()` and read the typed collections.\n *\n * Flattens every context type into one list, exactly as the 0.3.x bridge did.\n */\n async searchMemory(options: {\n userId?: string; user_id?: string;\n customerId?: string; customer_id?: string;\n query: string;\n maxResults?: number; max_results?: number;\n mode?: string;\n conversationId?: string; conversation_id?: string;\n types?: string[];\n }): Promise<{ results: FlatMemory[]; count: number }> {\n const userId = options.user_id ?? options.userId;\n if (!userId) throw new InvalidInputError('user_id is required');\n if (options.query === undefined || options.query === null) {\n throw new InvalidInputError('query is required');\n }\n const context = await fetchContext(this.#transport, 'user', {\n user_id: userId,\n ...(pick(options, 'customer_id', 'customerId') !== undefined\n ? { customer_id: pick(options, 'customer_id', 'customerId') as string }\n : {}),\n ...(pick(options, 'conversation_id', 'conversationId') !== undefined\n ? { conversation_id: pick(options, 'conversation_id', 'conversationId') as string }\n : {}),\n search_query: [options.query],\n max_results: (pick(options, 'max_results', 'maxResults') as number) ?? 10,\n types: options.types ?? ['all'],\n ...(options.mode !== undefined ? { mode: options.mode } : {}),\n });\n const results = flattenContextItems(context);\n return { results, count: results.length };\n }\n\n /**\n * @deprecated Use `client.user.context.fetch()` with an empty `search_query`.\n *\n * Note the default of 100, not 10: it matches the 0.3.x bridge.\n */\n async getMemories(options: {\n userId?: string; user_id?: string;\n customerId?: string; customer_id?: string;\n mode?: string;\n conversationId?: string; conversation_id?: string;\n maxResults?: number; max_results?: number;\n types?: string[];\n }): Promise<{ results: FlatMemory[]; count: number }> {\n const userId = options.user_id ?? options.userId;\n if (!userId) throw new InvalidInputError('user_id is required');\n const context = await fetchContext(this.#transport, 'user', {\n user_id: userId,\n ...(pick(options, 'customer_id', 'customerId') !== undefined\n ? { customer_id: pick(options, 'customer_id', 'customerId') as string }\n : {}),\n ...(pick(options, 'conversation_id', 'conversationId') !== undefined\n ? { conversation_id: pick(options, 'conversation_id', 'conversationId') as string }\n : {}),\n search_query: [],\n max_results: (pick(options, 'max_results', 'maxResults') as number) ?? 100,\n types: options.types ?? ['all'],\n ...(options.mode !== undefined ? { mode: options.mode } : {}),\n });\n const results = flattenContextItems(context);\n return { results, count: results.length };\n }\n\n /** @deprecated Use `client.memories.create()`. */\n async addMemory(options: CreateMemoryOptions): Promise<CreateMemoryResult> {\n return this.memories.create(options);\n }\n\n /**\n * @deprecated Use `client.memories.delete(memoryId)`.\n *\n * The `{ userId }` form is gone: it relied on a process-local id list and so\n * deleted nothing in serverless while reporting success (gotcha G-F).\n */\n async deleteMemory(options: { memoryId?: string; memory_id?: string }): Promise<Json> {\n return this.memories.delete(options.memory_id ?? options.memoryId ?? '');\n }\n\n /**\n * Resolve `client_id` and `instance_id` from the API key.\n *\n * Mirrors Python's `initialize()`. Calling it is optional here (unlike\n * Python, which raises \"SDK not initialized\" from `_ensure_initialized`)\n * because every HTTP route authenticates on the API key alone. It matters\n * for the anticipation cache: a bundle shared at client scope is filtered on\n * `client_id`, and an empty one fails that filter for every bundle. That was\n * a silent cache-scope bug in Python before whoami bootstrapping existed.\n *\n * Best-effort and idempotent. A whoami failure leaves whatever the\n * constructor resolved and is not fatal.\n */\n async initialize(): Promise<void> {\n if (this.#initialized) return;\n this.#initialized = true;\n const credentials = this.#transport.currentCredentials();\n if (credentials.clientId !== '' && credentials.instanceId !== '') return;\n try {\n const whoami = (await this.#transport.request('whoami', {})) as Json;\n const resolvedClientId = String(whoami['client_id'] ?? '');\n const resolvedInstanceId = String(whoami['instance_id'] ?? '');\n // The instance's scoping mode. Absent against any server older than this\n // field, and absent stays undefined, which every check treats as \"do not\n // enforce\". The server rejects independently either way.\n const iso = whoami['user_context_isolation'];\n // Stored on the transport, not on the client: every interface factory\n // already receives the transport, and surface-parity.test.ts is right\n // that a public member Python does not have is a divergence.\n this.#transport.userContextIsolation = typeof iso === 'string' ? iso : undefined;\n this.#transport.updateCredentials({\n ...(resolvedClientId !== '' && credentials.clientId === ''\n ? { clientId: resolvedClientId }\n : {}),\n ...(resolvedInstanceId !== '' && credentials.instanceId === ''\n ? { instanceId: resolvedInstanceId }\n : {}),\n });\n\n // Now that the real instance id is known, point it at this client too.\n // It was keyed on the credential because the id did not exist yet, so a\n // later `new SynapClient({ instanceId })` for the same instance would\n // otherwise miss and build a second client: two anticipation caches and\n // two Listen streams for one instance. The credential slot is kept, so\n // constructing by API key keeps resolving here as well.\n if (resolvedInstanceId !== '' && this.#registryKeys.length > 0) {\n if (registry.aliasIfAbsent(resolvedInstanceId, this)) {\n this.#registryKeys.push(resolvedInstanceId);\n }\n }\n } catch {\n // Non-fatal, exactly as in Python: identity resolution is an\n // optimisation, and every route still authenticates on the API key.\n }\n }\n\n /**\n * @deprecated Use `initialize()`, which is the Python name. `init()` was a\n * JS-only spelling with no Python counterpart; it now forwards, so a 0.3.x\n * startup script keeps working.\n */\n async init(): Promise<void> {\n return this.initialize();\n }\n\n /** The resolved instance id. Empty until `initialize()` resolves it. */\n get instance_id(): string {\n return this.#transport.currentCredentials().instanceId;\n }\n\n /** The resolved client id. Empty until `initialize()` resolves it. */\n get client_id(): string {\n return this.#transport.currentCredentials().clientId;\n }\n\n /**\n * Update configuration before `initialize()`.\n *\n * Mirrors Python's `configure()`, including its refusal to reconfigure after\n * initialization: the transport and cache are already built by then and a\n * half-applied config is worse than a rejected one.\n *\n * `logger` is honoured here as well as on the constructor. The remaining\n * Python-only keys (`storage_path`, `cache_backend`,\n * `session_timeout_minutes`, `log_level`) are accepted and ignored rather\n * than rejected, because this SDK has no SQLite cache backend and no global\n * logging framework whose level there would be to set. Ignoring them keeps\n * a shared config object portable between the two SDKs.\n */\n configure(options: ConfigureOptions = {}): void {\n if (this.#initialized) {\n throw new InvalidInputError('Cannot reconfigure after initialization');\n }\n if (options.timeouts !== undefined) this.#transport.setTimeouts(options.timeouts);\n if (options.retryPolicy !== undefined || options.retry_policy !== undefined) {\n this.#transport.setRetryPolicy(options.retryPolicy ?? options.retry_policy ?? null);\n }\n if (options.logger !== undefined) this.#logger = options.logger;\n const model = options.agent_model ?? options.agentModel;\n const window = options.agent_context_window ?? options.agentContextWindow;\n if (model !== undefined || window !== undefined) {\n this.#agentModel = {\n model: model ?? this.#agentModel.model,\n contextWindow: window ?? this.#agentModel.contextWindow,\n };\n }\n }\n\n /**\n * Fetch and merge context across every scope with an identifier, in one call.\n *\n * Mirrors Python's `sdk.fetch()`. The three behaviours worth knowing:\n *\n * - Scope fetches run in parallel and a FAILED scope is dropped with a\n * warning rather than failing the call, so partial context still gets\n * returned. The one exception is `InvalidInputError`: a bad request must\n * surface instead of degrading into a silently-empty context.\n * - `user_id` is threaded into the conversation-scope sub-fetch so the\n * anticipation cache can apply its per-user filter.\n * - The summary-mode parameters go ONLY to the user-scope sub-fetch. The\n * other three routes return 422 for `context_mode=\"conversation-summary\"`,\n * and that rejection would be swallowed by the drop-failed-scope rule.\n */\n async fetch(options: UnifiedFetchOptions = {}): Promise<UnifiedContext> {\n const {\n conversation_id: conversationId,\n user_id: userId,\n customer_id: customerId,\n search_query: searchQuery,\n max_results: maxResults = 20,\n types,\n mode = 'fast',\n precision_level: precisionLevel = 'high',\n query_date: queryDate,\n max_results_per_query: maxResultsPerQuery,\n include_conversation_context: includeConversationContext = true,\n scopes,\n include_scope_labels: includeScopeLabels = false,\n context_mode: contextMode = 'in-conversation',\n include_profile: includeProfile = true,\n last_n_conversations: lastNConversations = 1,\n response_shape: responseShape = 'by_type',\n } = options;\n\n validateConversationId(conversationId);\n // Up front, as Python does: a bad value inside a scope fetch would be an\n // InvalidInputError too, but only once a scope is queried.\n responseShapeOf({ response_shape: responseShape });\n\n const wants = (scope: string): boolean => scopes === undefined || scopes.includes(scope);\n const shared = {\n search_query: searchQuery,\n max_results: maxResults,\n types,\n mode,\n precision_level: precisionLevel,\n // In `shared` so it reaches all four branches at once. The Python edit\n // threaded three call sites by hand and missed the client branch, which\n // sits one indent deeper; spreading removes that whole class of miss.\n query_date: queryDate,\n max_results_per_query: maxResultsPerQuery,\n response_shape: responseShape,\n };\n\n const labels: string[] = [];\n const tasks: Array<Promise<RawContext>> = [];\n\n // `null` means absent, exactly as `validateConversationId` already reads\n // it. The guards below used to test only `!== undefined && !== ''`, so a\n // caller writing `conversation_id: convId || null` -- which is what the\n // Claude Agent (TS) integration writes, and the obvious way to spell \"no\n // conversation\" -- sailed past them and fired a conversation-scope fetch\n // carrying `conversation_id: null`. The validator waved it through and the\n // scope was queried for a conversation that does not exist.\n const present = (v: string | null | undefined): v is string =>\n v !== undefined && v !== null && v !== '';\n\n if (present(conversationId) && wants('conversation')) {\n labels.push('conversation');\n tasks.push(this.conversation.context.fetch({\n conversation_id: conversationId, ...shared,\n user_id: userId, customer_id: customerId,\n } as FetchOptions));\n }\n if (present(userId) && wants('user')) {\n labels.push('user');\n tasks.push(this.user.context.fetch({\n user_id: userId, conversation_id: conversationId, ...shared,\n customer_id: customerId,\n context_mode: contextMode,\n include_profile: includeProfile,\n last_n_conversations: lastNConversations,\n } as FetchOptions));\n }\n if (present(customerId) && wants('customer')) {\n labels.push('customer');\n tasks.push(this.customer.context.fetch({\n customer_id: customerId, conversation_id: conversationId, ...shared,\n } as FetchOptions));\n }\n // Client scope needs no identifier (it is inferred from auth), but Python\n // only queries it when EXPLICITLY named in `scopes`. Preserved: including\n // it by default would add a billed fetch to every unified call.\n if (scopes !== undefined && scopes.includes('client')) {\n labels.push('client');\n tasks.push(this.client.context.fetch({\n conversation_id: conversationId, ...shared,\n } as FetchOptions));\n }\n\n if (tasks.length === 0) {\n const empty = mergeScopeResults([]);\n return empty;\n }\n\n const settled = await Promise.allSettled(tasks);\n const successful: Array<readonly [string, RawContext]> = [];\n for (const [i, result] of settled.entries()) {\n const label = labels[i] as string;\n if (result.status === 'fulfilled') {\n successful.push([label, result.value]);\n continue;\n }\n if (result.reason instanceof InvalidInputError) throw result.reason;\n this.#warn(`Scope '${label}' fetch failed in unified fetch (non-fatal): ${String(result.reason)}`);\n }\n\n const merged = mergeScopeResults(successful);\n\n // Read-your-writes: splice the locally buffered tail over whatever the\n // server returned for this conversation.\n for (const [, response] of successful) {\n overlayLocalRecentTurns(response, this.#shortTerm, conversationId, this.#stVerbatimOverlay);\n }\n\n if (includeConversationContext && present(conversationId)) {\n try {\n merged.conversation_context = (await this.conversation.context.get_context_for_prompt({\n conversation_id: conversationId,\n })) as Json;\n } catch {\n // Non-fatal in Python too: the merged items are still worth returning.\n }\n }\n\n merged.formatted_context = formatForPrompt(merged, {\n includeScope: includeScopeLabels,\n includeConversationContext,\n });\n return merged;\n }\n\n /**\n * An LLM-ready tool definition that fetches Synap context.\n *\n * Mirrors Python's `as_tool()`. The scope identifiers are closed over, so\n * the model cannot choose whose memory to read.\n */\n as_tool(options: AsToolOptions = {}): ToolDefinition {\n return buildTool(this as never, options, (message) => this.#warn(message));\n }\n\n /**\n * A read-only diagnostic view of the in-process anticipation cache.\n *\n * Mirrors Python's `anticipation_cache_snapshot()`. Returns the same\n * snake_case shape, since this output gets logged and compared across SDKs.\n */\n anticipation_cache_snapshot(): AnticipationCacheSnapshot {\n return this.#anticipationCache.snapshot();\n }\n\n /**\n * Report a locally served fetch back to the server.\n *\n * Only while listening: the event rides the Listen stream, and there is\n * nowhere to put it otherwise. Never throws, because a telemetry failure\n * must not fail a retrieval that already succeeded.\n */\n #emitContextUsed(\n options: Record<string, unknown>,\n scope: string,\n served: { servedItemIds: string[]; bundleId: string },\n ): void {\n try {\n if (!this.instance.is_listening) return;\n this.instance._internal.sendContextUsed({\n bundle_id: served.bundleId,\n conversation_id: String(options['conversation_id'] ?? options['conversationId'] ?? ''),\n user_id: String(options['user_id'] ?? options['userId'] ?? ''),\n customer_id: String(options['customer_id'] ?? options['customerId'] ?? ''),\n served_item_ids: served.servedItemIds,\n scope,\n source_bundle_ids: served.bundleId ? [served.bundleId] : [],\n timestamp_ms: Date.now(),\n // How many distinct search queries the fetch carried. A hit never\n // reaches the fetch route, so this event is how the server learns it\n // happened and what it is charged by. The count only: this event must\n // not carry the caller's words.\n query_count: distinctQueryCount(options['search_query'] ?? options['searchQuery']),\n });\n } catch {\n // Non-fatal by design.\n }\n }\n\n /**\n * Report what was actually composed for this turn.\n *\n * Fires whether the fetch was served from the cache or the network, because\n * the audit needs to distinguish them. Like the other stream events it is\n * fire-and-forget and only goes anywhere while listening.\n */\n #emitContextAssembled(\n scope: string,\n options: Record<string, unknown>,\n response: RawContext,\n startedAt: number,\n ): void {\n try {\n if (!this.instance.is_listening) return;\n this.instance._internal.sendContextAssembled({\n ...buildAssembledEvent({\n correlationId: newCorrelationId(),\n response,\n conversationId: (options['conversation_id'] ?? options['conversationId']) as string | undefined,\n userId: (options['user_id'] ?? options['userId']) as string | undefined,\n customerId: (options['customer_id'] ?? options['customerId']) as string | undefined,\n startedAt,\n sdkVersion: SDK_VERSION,\n }),\n scope,\n });\n } catch {\n // Non-fatal by design.\n }\n }\n\n /**\n * Fold a cached user summary in, every Nth turn.\n *\n * Only when the caller supplied a user_id. Without one the summary lookup\n * cannot be scoped, and an unscoped lookup is how user A's summary ends up in\n * user B's response.\n */\n #injectUserSummary(\n response: RawContext,\n conversationId: string | undefined,\n userId: string | undefined,\n scopeRung: string | null,\n ): void {\n this.#turns.increment(conversationId);\n if (userId === undefined || userId === '') return;\n if (!this.#turns.shouldInject(conversationId)) return;\n // The rung, for the same reason the user id is here: a summary prefetched\n // for one rung is not this caller's summary if they are standing at\n // another, and splicing it in is the same failure one level down.\n const summary = this.#anticipationCache.lookupUserSummary(userId, scopeRung);\n if (summary === null) return;\n mergeUserSummary(response, summary as Json);\n }\n\n /**\n * Whether the SDK is authoritative for short-term context.\n *\n * Resolution order matches Python's `_is_st_authoritative`:\n * `SYNAP_SDK_ST_AUTHORITATIVE` first, as an explicit override in either\n * direction, then the option. Off by default. Being enabled does not mean\n * the local store is used: that also takes an entry that holds the whole\n * conversation, see `isComplete`.\n */\n #isStAuthoritative(): boolean {\n const raw = (getEnv('SYNAP_SDK_ST_AUTHORITATIVE') ?? '').trim().toLowerCase();\n if (['0', 'false', 'no', 'off'].includes(raw)) return false;\n if (['1', 'true', 'yes', 'on'].includes(raw)) return true;\n return this.#stAuthoritative === true;\n }\n\n /**\n * Render `get_context_for_prompt` from the local store, or null to fall\n * through to the network.\n *\n * The store answers only when it holds the whole conversation: the server\n * has confirmed a count the entry accounts for. An entry that is merely\n * present is not enough. After a restart or an eviction it holds one turn,\n * and rendering that as the conversation drops everything said before it.\n */\n /**\n * `response` with the earlier turns that best match `query` (by default the\n * latest user message) appended, when a compaction exists. The search runs\n * here, over the turn archive. Python: `_with_history_excerpts`.\n */\n async #withHistoryExcerpts(conversationId: string, response: Json, query: string | undefined): Promise<Json> {\n const r = (response ?? {}) as Record<string, unknown>;\n const store = this.#turnStore;\n const recent = (Array.isArray(r['recent_messages']) ? r['recent_messages'] : []) as Array<Record<string, unknown>>;\n store.addMany(\n conversationId,\n recent.map((m) => ({\n role: String(m['role'] ?? 'user'),\n content: String(m['content'] ?? ''),\n timestamp: String(m['timestamp'] ?? ''),\n message_id: typeof m['message_id'] === 'string' ? m['message_id'] : null,\n })),\n );\n const entry = this.#shortTerm.get(conversationId);\n const total = Math.max(Number(r['total_message_count'] ?? 0) || 0, Number(entry?.serverMessageCount ?? 0) || 0);\n this.#maybeBackfillTurns(conversationId, total);\n if (!r['context_block']) return response;\n const turns = store.turns(conversationId);\n let q = query;\n if (!q) {\n const latest = [...recent].reverse().find((m) => m['role'] === 'user');\n q = latest ? String(latest['content'] ?? '') : ([...turns].reverse().find((t) => t.role === 'user')?.content ?? '');\n }\n if (!q) return response;\n const exclude = new Set<string>();\n for (const m of recent) {\n if (typeof m['message_id'] === 'string' && m['message_id']) exclude.add(m['message_id']);\n exclude.add(String(m['content'] ?? ''));\n }\n this.#startLocalModel();\n const hits = await searchTurns(turns, q, { exclude, encoder: this.#localEncoder, store });\n if (hits.length === 0) return response;\n const block = renderExcerpts(hits, turns.length ? turns[turns.length - 1]!.timestamp : '');\n return {\n ...r,\n history_excerpts: hits.map((h) => ({ position: h.position, role: h.role, text: h.text })),\n history_block: block,\n formatted_context: (String(r['formatted_context'] ?? '').trimEnd() + '\\n\\n' + block).trim(),\n } as Json;\n }\n\n /**\n * Keep what a tool returned in this process's turn archive, so history search\n * can find it after a compaction. Never sent anywhere: the server's\n * conversation holds what people said, and a compaction is made from that.\n * `instance.record_tool_result` calls this too. Python: `archive_tool_result`.\n */\n archive_tool_result(\n conversationId: string,\n result: unknown,\n options: { tool_name?: string | null; toolName?: string | null; timestamp?: string } = {},\n ): void {\n if (!conversationId || result === null || result === undefined) return;\n try {\n this.#turnStore.add(conversationId, 'tool', toolResultText(result, options.tool_name ?? options.toolName),\n options.timestamp ?? new Date().toISOString());\n } catch {\n // The archive is a convenience: never fail the agent's call.\n }\n }\n\n static readonly BACKFILL_COOLDOWN_MS = 60_000;\n static readonly BACKFILL_PAGE = 500;\n\n /** Fetch, in the background, turns the server holds and the archive does not. */\n /**\n * Load the local embedding model in the background, once. Searches and\n * lookups before it is ready match by words. Python: `_start_history_encoder`.\n */\n #startLocalModel(): void {\n if (this.#localModelStarted) return;\n this.#localModelStarted = true;\n if (localModelOff()) return;\n const name = this.#localModelName === 'auto' ? SEMANTIC_MATCH.model : this.#localModelName;\n if (!name) return;\n const callerEncoder = this.#localEncoder;\n void import('./cache/onnx-encoder.js')\n .then(({ canRunLocalModel, loadOnnxEncoder }) => (canRunLocalModel() ? loadOnnxEncoder(name) : null))\n .then((encoder) => {\n if (encoder === null) return;\n // A caller's own encoder keeps history search.\n if (callerEncoder === null) this.#localEncoder = encoder;\n // The anticipation thresholds hold for one model only.\n if (encoder.model === SEMANTIC_MATCH.model) this.#anticipationCache.setEncoder(encoder);\n })\n .catch(() => {\n /* words only, as without a model */\n });\n }\n\n #maybeBackfillTurns(conversationId: string, total: number): void {\n if (total <= this.#turnStore.count(conversationId)) return;\n const now = Date.now();\n if (now < (this.#backfillAfter.get(conversationId) ?? 0)) return;\n this.#backfillAfter.set(conversationId, now + SynapClient.BACKFILL_COOLDOWN_MS);\n void this.#backfillTurns(conversationId, total);\n }\n\n async #backfillTurns(conversationId: string, total: number): Promise<void> {\n try {\n let start = 1;\n while (start <= total) {\n const result = await this.#transport.request<Json>('conversation_turns', {\n pathParams: { conversation_id: conversationId },\n query: { start, limit: SynapClient.BACKFILL_PAGE },\n });\n const page = (((result ?? {}) as Record<string, unknown>)['turns'] ?? []) as TurnInput[];\n this.#turnStore.addMany(conversationId, page);\n if (page.length < SynapClient.BACKFILL_PAGE) break;\n start += SynapClient.BACKFILL_PAGE;\n }\n } catch (error) {\n this.#warn(`Turn backfill for ${conversationId} failed: ${String(error)}`);\n }\n }\n\n #localPrompt(conversationId: string, style: string): Json | null {\n if (!this.#isStAuthoritative()) return null;\n const entry = this.#shortTerm.get(conversationId);\n if (entry === null || !isComplete(entry)) return null;\n if (entry.compactionId === null && entry.recentTurns.length === 0) return null;\n return renderForPrompt(entry, style) as unknown as Json;\n }\n\n /** Same, for `get_compacted`. Requires an actual compaction, not just turns. */\n #localCompacted(conversationId: string, format: string): Json | null {\n if (!this.#isStAuthoritative()) return null;\n const entry = this.#shortTerm.get(conversationId);\n if (entry === null || entry.compactionId === null || !isComplete(entry)) return null;\n return renderCompacted(entry, format);\n }\n\n /**\n * Bring the local store back in step from the server's answer.\n *\n * Only for a conversation this process writes to. The server's answer lists\n * every message its summary does not cover, which is exactly what the entry\n * was missing.\n */\n #reseedShortTerm(conversationId: string, response: Json): void {\n if (!this.#isStAuthoritative()) return;\n const entry = this.#shortTerm.get(conversationId);\n if (entry === null || !entry.participant) return;\n const r = (response ?? {}) as Record<string, unknown>;\n const recent = Array.isArray(r['recent_messages']) ? r['recent_messages'] : [];\n this.#shortTerm.reseed(conversationId, {\n compactionId: typeof r['compaction_id'] === 'string' ? r['compaction_id'] : null,\n recentMessages: recent as ShortTermTurn[],\n compactedMessageCount: Number(r['compacted_message_count'] ?? 0) || 0,\n totalMessageCount: Number(r['total_message_count'] ?? 0) || 0,\n });\n }\n\n /** Every diagnostic the SDK emits passes through here. */\n #warn(message: string): void {\n if (this.#logger !== undefined) {\n this.#logger('warn', message);\n return;\n }\n // eslint-disable-next-line no-console\n console.warn(`[synap] ${message}`);\n }\n\n /** Release timers and connections. Safe to call more than once. */\n async shutdown(): Promise<void> {\n if (this.#closed) return;\n this.#closed = true;\n this.#shortTerm.clear();\n this.#turns.clear();\n // Release every registry slot pointing here, so a later construction\n // builds a fresh client rather than adopting this shut-down one. Only\n // slots still owned by this client are dropped.\n for (const key of this.#registryKeys) registry.unregisterIfOwner(key, this);\n\n // Stop the stream first: it holds a socket and a heartbeat timer, and\n // leaving either alive is what keeps a finished process from exiting.\n await this.instance.stop_listening();\n this.#compactionSubscribers.removeAll();\n this.#transport.close();\n this.#anticipationCache.clear();\n }\n}\n\nfunction pick(o: Record<string, unknown>, snake: string, camel: string): unknown {\n const a = o[snake];\n return a !== undefined ? a : o[camel];\n}\n\n/** Explicit option wins, then the environment, then the SDK default. */\nfunction resolveBaseUrl(options: SynapClientOptions): string | undefined {\n const explicit = options.baseUrl ?? options.api_base_url;\n if (explicit !== undefined) return explicit;\n const fromEnv = getEnv('SYNAP_BASE_URL')?.trim();\n return fromEnv !== undefined && fromEnv !== '' ? fromEnv : undefined;\n}\n\nfunction resolveCredentials(options: SynapClientOptions): Credentials {\n // Read at call time, not module load, so a late process.env mutation works.\n const apiKey = options.apiKey ?? getEnv('SYNAP_API_KEY');\n if (!apiKey) {\n throw new InvalidInputError(\n 'No Synap API key found. Set SYNAP_API_KEY in your environment, or pass ' +\n 'apiKey to the SynapClient constructor.',\n );\n }\n return {\n apiKey,\n clientId: options.clientId ?? getEnv('SYNAP_CLIENT_ID') ?? '',\n instanceId: options.instanceId ?? getEnv('SYNAP_INSTANCE_ID') ?? '',\n };\n}\n"]}
1
+ {"version":3,"sources":["/Users/anish/maximemai/maximem_synap-js-parity/synap/sdk/js/dist/index.cjs","../src/transport/endpoints.ts","../src/transport/retry.ts","../src/transport/keepalive.ts","../src/scoping.ts","../src/transport/http.ts","../src/context/ranked.ts","../src/context/fetch.ts","../src/context/normalize.ts","../src/context/flatten.ts","../src/memories/interface.ts","../src/util/validators.ts","../src/agent-model.ts","../src/conversation/interface.ts","../src/user/interface.ts","../src/credits/interface.ts","../src/cache/interface.ts","../src/instance/interface.ts","../src/version.ts","../src/registry.ts","../src/cache/short-term-store.ts","../src/cache/turn-store.ts","../src/cache/history-search.ts","../src/context/overlay.ts","../src/context/local-prompt.ts","../src/context/user-summary.ts","../src/context/anticipated.ts","../src/context/assembled.ts","../src/context/unified.ts","../src/tool/as-tool.ts","../src/client.ts"],"names":["ranked","COLLECTIONS"],"mappings":"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;AAEA,EAAA;AACA,kBAAA;AACA,EAAA;AACF;APgeK;AACA;AQtuBE;AACL,EAAA;AACA,EAAA;AACF;AAEA;AACE,EAAA;AACF;AAEM;AAGN;AACE,EAAA;AACA,EAAA;AACF;AAEA;AACE,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACF;AAEO;AACL,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACF;AAEO;AACL,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACF;AAEO;AACL,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACF;AAEO;AACL,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACF;AAEO;AACL,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACF;AAEO;AACL,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AAAmB,MAAA;AACW,MAAA;AACC,MAAA;AAE/B,IAAA;AACF,EAAA;AACF;AAEO;AAGL,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACF;AAEA;AACE,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACF;AAGO;AACL,EAA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AA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACE,MAAA;AACA,MAAA;AACF,IAAA;AACA,IAAA;AACF,EAAA;AAEA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACF;ArB+qEK;AACA;AsBj3EC;AAEA;AAEN;AACE,EAAA;AACA,EAAA;AAEE,IAAA;AAA+E,IAAA;AAEjF,EAAA;AACA,EAAA;AACF;AAEM;AACA;AAEN;AACE,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACA,EAAA;AACF;AAEA;AACE,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACA,EAAA;AACF;AAEA;AACE,EAAA;AACF;AAEA;AACE,EAAA;AACA,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AACE,MAAA;AACE,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AAAO,MAAA;AAEX,IAAA;AACA,IAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAMM;AACA;AACA;AAOC;AACL,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AACE,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACF,IAAA;AACA,IAAA;AACE,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACE,QAAA;AACA,QAAA;AAA0E,MAAA;AAE5E,MAAA;AACA,MAAA;AACF,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACE,MAAA;AACA,MAAA;AACF,IAAA;AACA,IAAA;AACA,IAAA;AACE,MAAA;AACF,IAAA;AACE,MAAA;AACA,MAAA;AACF,IAAA;AACA,IAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAEA;AACE,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACE,MAAA;AACA,MAAA;AACA,MAAA;AACF,IAAA;AACA,IAAA;AACE,MAAA;AACA,MAAA;AACA,MAAA;AACF,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAcA;AACE,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AACF,EAAA;AACA,EAAA;AACF;AAGA;AACE,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAEA;AAKE,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AAEA,EAAA;AACA,EAAA;AACA,EAAA;AACE,IAAA;AACE,MAAA;AACA,MAAA;AACF,IAAA;AACA,IAAA;AACA,IAAA;AAEE,MAAA;AAAW,QAAA;AACF,QAAA;AAAc,QAAA;AAAY,QAAA;AAAkB,QAAA;AAA+B,QAAA;AACpD,MAAA;AAEjC,IAAA;AACF,EAAA;AAED,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACD,EAAA;AACD,EAAA;AAEA,EAAA;AACA,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AAEE,IAAA;AACA,IAAA;AACE,MAAA;AACA,MAAA;AACA,MAAA;AACF,IAAA;AACA,IAAA;AAAgB,MAAA;AAKhB,IAAA;AACF,EAAA;AACE,IAAA;AAAgB,MAAA;AAEhB,IAAA;AACF,EAAA;AAEA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AACF,EAAA;AACA,EAAA;AAIA,EAAA;AAMA,EAAA;AACA,EAAA;AACE,IAAA;AAIF,EAAA;AAGA,EAAA;AACE,IAAA;AACAA,IAAAA;AACF,EAAA;AACA,EAAA;AAEA,EAAA;AACA,EAAA;AAEA,EAAA;AACA,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AAKA,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AACE,IAAA;AAAc,MAAA;AACQ,MAAA;AACJ,MAAA;AACK,MAAA;AACA,MAAA;AACY,MAAA;AAElC,IAAA;AACH,EAAA;AAEA,EAAA;AACA,EAAA;AACA,EAAA;AACE,IAAA;AACE,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AAAa,QAAA;AACD,QAAA;AACK,QAAA;AACK,QAAA;AACb,QAAA;AACF,QAAA;AACkE,MAAA;AAE3E,IAAA;AACF,EAAA;AACA,EAAA;AACF;AAEA;AAQE,EAAA;AACA,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACE,MAAA;AACA,MAAA;AACE,QAAA;AACA,QAAA;AAAuB,MAAA;AAE3B,IAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACE,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACD,IAAA;AACD,IAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AAIF;AAMO;AACL,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAGO;AACL,EAAA;AACA,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AACE,MAAA;AACA,MAAA;AACF,IAAA;AACA,IAAA;AACF,EAAA;AACA,EAAA;AACF;AAEM;AAGN;AACE,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAGA;AACE,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACE,IAAA;AACE,MAAA;AACA,MAAA;AACF,IAAA;AACF,EAAA;AACA,EAAA;AACF;AtB2xEK;AACA;AuBhuFC;AACA;AASC;AAGL,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAGA;AACE,EAAA;AACF;AAOO;AACL,EAAA;AACA,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AACF,EAAA;AACA,EAAA;AACF;AAQO;AAQL,EAAA;AACA,EAAA;AACE,IAAA;AAEA,IAAA;AAEA,IAAA;AAMA,IAAA;AAQA,IAAA;AACA,IAAA;AAEA,IAAA;AACA,IAAA;AACE,MAAA;AACA,MAAA;AAAyF,QAAA;AACvD,QAAA;AACE,QAAA;AACI,MAAA;AAExC,MAAA;AACF,IAAA;AAGA,IAAA;AAA8D,MAAA;AACpD,MAAA;AACG,MAAA;AAEb,IAAA;AAKA,IAAA;AAEA,IAAA;AACE,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACF,IAAA;AAGA,IAAA;AAAgC,MAAA;AACW,MAAA;AACwB,MAAA;AACM,MAAA;AACzD,MAAA;AAC4C,MAAA;AACH,MAAA;AAEzD,IAAA;AACF,EAAA;AAEA,EAAA;AACF;AvByqFK;AACA;AwBzyFQ;AAEA;AAgCb;AACE,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACE,IAAA;AACF,EAAA;AACE,IAAA;AACF,EAAA;AACF;AAGA;AACE,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AACE,MAAA;AAA0F,MAAA;AAE5F,IAAA;AACA,IAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAEA;AACE,EAAA;AACF;AAEA;AACE,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,EAAA;AACJ;AAOA;AACE,EAAA;AACF;AAGA;AACE,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAEA;AACE,EAAA;AACA,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AACE,MAAA;AACA,MAAA;AACF,IAAA;AACA,IAAA;AACF,EAAA;AACA,EAAA;AAAqB,IAAA;AAA0B,IAAA;AAAkC,EAAA;AACjF,EAAA;AAA8B,IAAA;AAAwB,IAAA;AAA2C,EAAA;AACjG,EAAA;AACE,IAAA;AAA+B,IAAA;AACjC,EAAA;AAEA,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AACF,EAAA;AAEA,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AACE,MAAA;AACA,MAAA;AACA,MAAA;AACF,IAAA;AACF,EAAA;AAEA,EAAA;AACE,IAAA;AACA,IAAA;AACF,EAAA;AAEA,EAAA;AACF;AAEA;AACE,EAAA;AACA,EAAA;AACA,EAAA;AAA2B,EAAA;AAE3B,EAAA;AAA4F,EAAA;AAG5F,EAAA;AACE,IAAA;AACA,IAAA;AAAiC;AACnC,EAAA;AACA,EAAA;AACF;AAEA;AACE,EAAA;AACA,EAAA;AACA,EAAA;AAEE,IAAA;AACF,EAAA;AACA,EAAA;AAEA,EAAA;AACE,IAAA;AACF,EAAA;AACA,EAAA;AACE,IAAA;AACF,EAAA;AACA,EAAA;AAEA,EAAA;AACF;AAEA;AACE,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAGO;AAKL,EAAA;AAIA,EAAA;AACA,EAAA;AACA,EAAA;AAEA,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AACF,EAAA;AAEA,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACF;AAGO;AAKL,EAAA;AAEA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AAEA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACF;AxBwuFK;AACA;AyB19FQ;AAEP;AAEA;AAGO;AACF,EAAA;AAAkC;AAG3C,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AASA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AAEA,EAAA;AACE,IAAA;AACF,EAAA;AACF;AAEA;AACE,EAAA;AACF;AAEA;AACE,EAAA;AACE,IAAA;AACA,IAAA;AACF,EAAA;AACA,EAAA;AACF;AAUA;AACE,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACF;AASO;AACL,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AAEA,IAAA;AACA,IAAA;AACA,IAAA;AAIA,IAAA;AAEA,IAAA;AACF,EAAA;AACA,EAAA;AACF;AzB67FK;AACA;A0B9gGE;AAIL,EAAA;AACA,EAAA;AACE,IAAA;AACE,MAAA;AACF,IAAA;AAEE,MAAA;AACF,IAAA;AACE,MAAA;AACF,IAAA;AACE,MAAA;AACJ,EAAA;AACF;AAGO;AACL,EAAA;AACA,EAAA;AACG,IAAA;AACH,EAAA;AAGA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACA,EAAA;AACF;AAGO;AACL,EAAA;AACA,EAAA;AACE,IAAA;AACE,MAAA;AACA,MAAA;AACF,IAAA;AACF,EAAA;AACA,EAAA;AACF;AAcA;AAME,EAAA;AACE,IAAA;AACA,IAAA;AAAoC,MAAA;AACC,MAAA;AACN,MAAA;AACI,MAAA;AACJ,MAAA;AACY,MAAA;AACV,MAAA;AAEhC,IAAA;AACD,IAAA;AACA,IAAA;AACF,EAAA;AACE,IAAA;AACF,EAAA;AACF;AAQO;AACL,EAAA;AACA,EAAA;AACF;A1B6+FK;AACA;A2BpmGCC;AAmBC;AACL,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACE,MAAA;AACA,MAAA;AACF,IAAA;AACF,EAAA;AACA,EAAA;AACF;AASO;AAIL,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAEO;AAUL,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AAEA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AAA0B,wCAAA;AAE1B,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACF;A3B8jGK;AACA;A4BvpGQ;AACX,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACF;AA4BA;AACE,EAAA;AACA,EAAA;AACF;AAWO;AAGL,EAAA;AACA,EAAA;AACE,IAAA;AAAW,IAAA;AAAiB,IAAA;AAAc,IAAA;AAAc,IAAA;AACxD,IAAA;AAAW,IAAA;AACX,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AAEA,EAAA;AACA,EAAA;AAEA,EAAA;AACE,IAAA;AACA,IAAA;AACE,MAAA;AACF,IAAA;AAGA,IAAA;AACE,MAAA;AACF,IAAA;AACA,IAAA;AAKE,MAAA;AACF,IAAA;AAEA,IAAA;AACE,MAAA;AACA,MAAA;AACA,MAAA;AACE,QAAA;AACA,QAAA;AACE,UAAA;AACA,UAAA;AACA,UAAA;AAAoB,QAAA;AAEtB,QAAA;AAAyB,MAAA;AAE7B,IAAA;AAKA,IAAA;AACA,IAAA;AACE,MAAA;AACE,QAAA;AACA,QAAA;AACE,UAAA;AACA,UAAA;AACA,UAAA;AAAoB,QAAA;AAEtB,QAAA;AAAsB,MAAA;AAE1B,IAAA;AACA,IAAA;AACA,IAAA;AACE,MAAA;AACE,QAAA;AACA,QAAA;AACE,UAAA;AACA,UAAA;AAAiB,QAAA;AAEnB,QAAA;AAAqB,MAAA;AAEzB,IAAA;AACF,EAAA;AAIA,EAAA;AACA,EAAA;AAEA,EAAA;AACF;AAGA;AACE,EAAA;AACF;AAGA;AACE,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAEA;AACE,EAAA;AACF;AAEA;AACE,EAAA;AACA,EAAA;AACF;AAGA;AACE,EAAA;AACF;AAeO;AACL,EAAA;AACA,EAAA;AACA,EAAA;AAIA,EAAA;AACA,EAAA;AACE,IAAA;AACE,MAAA;AACA,MAAA;AACA,MAAA;AACD,IAAA;AACD,IAAA;AAA+B;AACjC,EAAA;AACA,EAAA;AAEA,EAAA;AACE,IAAA;AACE,MAAA;AACA,MAAA;AACA,MAAA;AACD,IAAA;AACD,IAAA;AAA4B;AAC9B,EAAA;AAEA,EAAA;AACE,IAAA;AACE,MAAA;AACA,MAAA;AACA,MAAA;AAGA,MAAA;AACE,QAAA;AAA+C,MAAA;AAEjD,MAAA;AACD,IAAA;AACD,IAAA;AAAkC;AACpC,EAAA;AAEA,EAAA;AACE,IAAA;AAEE,MAAA;AACA,MAAA;AACA,MAAA;AACD,IAAA;AACD,IAAA;AAA+B;AACjC,EAAA;AAEA,EAAA;AACE,IAAA;AACE,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACE,QAAA;AAAiD,MAAA;AAGnD,MAAA;AACA,MAAA;AACD,IAAA;AACD,IAAA;AAA+B;AACjC,EAAA;AAEA,EAAA;AACE,IAAA;AACE,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACD,IAAA;AACD,IAAA;AAAsC;AACxC,EAAA;AAEA,EAAA;AACE,IAAA;AACA,IAAA;AACE,MAAA;AAA2C;AAC7C,IAAA;AACF,EAAA;AAEA,EAAA;AACA,EAAA;AAAyD;AAEzD,EAAA;AACA,EAAA;AAEA,EAAA;AACA,EAAA;AAEA,EAAA;AACF;AAGA;AACE,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACE,IAAA;AACE,MAAA;AAIA,MAAA;AACA,MAAA;AAA0E,MAAA;AACf,MAAA;AAI3D,MAAA;AACF,IAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AAA6B;AAC/B;AAGA;AACE,EAAA;AACA,EAAA;AACE,IAAA;AAEA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AAKE,MAAA;AACF,IAAA;AACA,IAAA;AACD,EAAA;AACD,EAAA;AAAqC;AACvC;AAQA;AACE,EAAA;AACA,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AACF,EAAA;AACA,EAAA;AACF;AAQA;AACE,EAAA;AACA,EAAA;AACE,IAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AAEA,EAAA;AACF;A5B4iGK;AACA;A6Bh5GC;AAgCA;AAKC;AACL,EAAA;AACE,IAAA;AACE,MAAA;AAMF,IAAA;AACE,MAAA;AAMF,IAAA;AACE,MAAA;AAKF,IAAA;AACE,MAAA;AAKF,IAAA;AACE,MAAA;AAMJ,EAAA;AACF;AAQO;AACL,EAAA;AACE,IAAA;AAAc,MAAA;AACN,MAAA;AACkB,MAAA;AAK1B,IAAA;AACA,IAAA;AAAa,MAAA;AACL,MAAA;AACG,MAAA;AACA,MAAA;AAEX,IAAA;AACA,IAAA;AAAO,MAAA;AACC,MAAA;AACC,QAAA;AACC,QAAA;AACkE,MAAA;AAC1E,MAAA;AAEF,IAAA;AACA,IAAA;AAAM,MAAA;AACE,MAAA;AACmB,MAAA;AAI3B,IAAA;AACA,IAAA;AAAiB,MAAA;AACT,MAAA;AACiB,MAAA;AAKzB,IAAA;AACF,EAAA;AAEA,EAAA;AACA,EAAA;AACE,IAAA;AAA2B,MAAA;AACnB,MAAA;AAER,IAAA;AACA,IAAA;AACF,EAAA;AAEA,EAAA;AACF;AAeM;AAQN;AAQE,EAAA;AAEA,EAAA;AAEA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AAKA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AAEA,EAAA;AACE,IAAA;AAAoC,MAAA;AACjB,MAAA;AACR,MAAA;AACI,MAAA;AAEd,IAAA;AACD,IAAA;AAAO,MAAA;AACuB,MAAA;AACH,MAAA;AAE3B,IAAA;AACF,EAAA;AAEA,EAAA;AACA,EAAA;AACE,IAAA;AAGE,MAAA;AACF,IAAA;AACA,IAAA;AAAmD,MAAA;AAChC,MAAA;AACd,MAAA;AAEJ,IAAA;AACH,EAAA;AACE,IAAA;AAA2C,MAAA;AAChC,MAAA;AACQ,MAAA;AACd,MAAA;AAEJ,IAAA;AACH,EAAA;AACE,IAAA;AAA+C,MAAA;AAChC,MAAA;AACI,MAAA;AAElB,IAAA;AACH,EAAA;AACE,IAAA;AAA6C,MAAA;AAC1B,MAAA;AAElB,IAAA;AACH,EAAA;AAEA,EAAA;AACA,EAAA;AACE,IAAA;AACF,EAAA;AACA,EAAA;AACF;AASO;AAIW;AAChB,EAAA;AACA,EAAA;AAEE,IAAA;AACA,IAAA;AAAU,MAAA;AAEV,IAAA;AACF,EAAA;AAEA,EAAA;AACA,EAAA;AACA,EAAA;AAEA,EAAA;AACE,IAAA;AACF,EAAA;AACA,EAAA;AACE,IAAA;AACF,EAAA;AACA,EAAA;AAGE,IAAA;AAAA,MAAA;AAIA,IAAA;AACF,EAAA;AAEA,EAAA;AACA,EAAA;AACE,IAAA;AACF,EAAA;AAEA,EAAA;AACA,EAAA;AACA,EAAA;AAEA,EAAA;AAGA,EAAA;AACE,IAAA;AAAO,MAAA;AACC,MAAA;AACgE,MAAA;AAExE,IAAA;AACF,EAAA;AACA,EAAA;AACF;A7BywGK;AACA;A8Bh1GL;AACE,EAAA;AACF;AAEa;AAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAWd,EAAA;AACA,EAAA;AAEA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AAET,EAAA;AACA,EAAA;AACA,EAAA;AACS,EAAA;AACA,EAAA;AACA,EAAA;AAAA;AAEA,EAAA;AAAA;AAET,EAAA;AAAgC;AAEhC,EAAA;AACA,EAAA;AACS,EAAA;AACA,EAAA;AACT,EAAA;AACA,EAAA;AACS,EAAA;AACA,EAAA;AACA,EAAA;AAET,EAAA;AACE,IAAA;AACA,IAAA;AAEA,IAAA;AAYA,IAAA;AACA,IAAA;AACE,MAAA;AACA,MAAA;AAME,QAAA;AACA,QAAA;AACE,UAAA;AAAS,YAAA;AACgB,UAAA;AAIzB,QAAA;AAEF,QAAA;AAAO,MAAA;AAEX,IAAA;AACA,IAAA;AAEA,IAAA;AAAoC,MAAA;AAClC;AAAA;AAAA;AAAA;AAAA;AAAA,MAAA;AAMoE,MAAA;AACG,MAAA;AACS,MAAA;AACN,MAAA;AACA,MAAA;AAE3E,IAAA;AACD,IAAA;AACA,IAAA;AAEA,IAAA;AAEA,IAAA;AAAmB,MAAA;AAEf,QAAA;AAOA,QAAA;AAGA,QAAA;AACA,QAAA;AAAsB,UAAA;AACf,UAAA;AACL,UAAA;AACA,YAAA;AACoD,YAAA;AACb,YAAA;AACU,YAAA;AACY,YAAA;AACA;AAAA;AAAA;AAAA;AAAA;AAAA,YAAA;AAMhD,+BAAA;AACwB,YAAA;AACnC,UAAA;AACF,UAAA;AACgD,QAAA;AAElD,QAAA;AACA,QAAA;AAAO,UAAA;AACa,UAAA;AACsB,UAAA;AACF,UAAA;AACL,UAAA;AACF,QAAA;AACjC,MAAA;AACF,MAAA;AAEE,QAAA;AAAyF,MAAA;AAC3F,MAAA;AAKE,QAAA;AAAwE,MAAA;AAE5E,IAAA;AAEA,IAAA;AAKA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AAEA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AAAmB,MAAA;AACqB,MAAA;AAExC,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AAGA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AAAoB,MAAA;AACb,MAAA;AACA;AAAA;AAAA;AAAA;AAAA;AAAA,MAAA;AAOH,QAAA;AACE,UAAA;AAAmD,QAAA;AACrD,MAAA;AACF;AAAA;AAAA,MAAA;AAIE,QAAA;AAAqE,UAAA;AACnE,UAAA;AACA,UAAA;AACA,QAAA;AACD,MAAA;AACH,MAAA;AAEE,QAAA;AACA,QAAA;AAA4E,MAAA;AAC9E,MAAA;AACA,QAAA;AAC4E,QAAA;AACQ,QAAA;AACE,QAAA;AAC2B,MAAA;AACjH,MAAA;AAEF,IAAA;AAIA,IAAA;AACE,MAAA;AACE,QAAA;AAA6C,MAAA;AAEhD,IAAA;AACD,IAAA;AACA,IAAA;AACA,IAAA;AAAgB,MAAA;AAC2B,MAAA;AACpC,MAAA;AAEH,QAAA;AAAwD,MAAA;AAC1D,MAAA;AACW,MAAA;AAEb,IAAA;AAMA,IAAA;AACA,IAAA;AACE,MAAA;AACD,IAAA;AAGD,IAAA;AACE,MAAA;AACD,IAAA;AAED,IAAA;AAIE,MAAA;AACA,MAAA;AACF,IAAA;AAIA,IAAA;AACE,MAAA;AACE,QAAA;AAA0D,MAAA;AAE7D,IAAA;AACH,EAAA;AAAA;AAAA;AAAA;AAMA,EAAA;AACE,IAAA;AACF,EAAA;AAAA;AAGA,EAAA;AACE,IAAA;AACF,EAAA;AAAA;AAGA,EAAA;AACE,IAAA;AACF,EAAA;AAAA;AAGA,EAAA;AAGE,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOA,EAAA;AASE,IAAA;AACA,IAAA;AACA,IAAA;AACE,MAAA;AACF,IAAA;AACA,IAAA;AAA4D,MAAA;AACjD,MAAA;AAGJ,MAAA;AAGA,MAAA;AACuB,MAAA;AAC2C,MAAA;AACzC,MAAA;AAE/B,IAAA;AACD,IAAA;AACA,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOA,EAAA;AAQE,IAAA;AACA,IAAA;AACA,IAAA;AAA4D,MAAA;AACjD,MAAA;AAGJ,MAAA;AAGA,MAAA;AACU,MAAA;AACwD,MAAA;AACzC,MAAA;AAE/B,IAAA;AACD,IAAA;AACA,IAAA;AACF,EAAA;AAAA;AAGA,EAAA;AACE,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQA,EAAA;AACE,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAmBA,EAAA;AACE,IAAA;AACA,IAAA;AACF,EAAA;AAAA;AAGA,EAAA;AACE,IAAA;AACA,IAAA;AACF,EAAA;AAEA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACE,MAAA;AACA,MAAA;AACA,MAAA;AAIA,MAAA;AAIA,MAAA;AACA,MAAA;AAAkC,QAAA;AAG3B,QAAA;AAGA,MAAA;AASP,MAAA;AACE,QAAA;AACE,UAAA;AAA0C,QAAA;AAC5C,MAAA;AAEJ,IAAA;AAGA,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOA,EAAA;AACE,IAAA;AACF,EAAA;AAAA;AAGA,EAAA;AACE,IAAA;AACF,EAAA;AAAA;AAGA,EAAA;AACE,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAgBA,EAAA;AACE,IAAA;AACE,MAAA;AACF,IAAA;AACA,IAAA;AACA,IAAA;AACE,MAAA;AACF,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACE,MAAA;AAAmB,QAAA;AACgB,QAAA;AACS,MAAA;AAE9C,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAiBA,EAAA;AACE,IAAA;AAAM,MAAA;AACa,MAAA;AACR,MAAA;AACI,MAAA;AACC,MAAA;AACY,MAAA;AAC1B,MAAA;AACO,MAAA;AAC2B,MAAA;AACtB,MAAA;AACW,MAAA;AACoC,MAAA;AAC3D,MAAA;AAC2C,MAAA;AACf,MAAA;AACM,MAAA;AACS,MAAA;AAE7C,IAAA;AAEA,IAAA;AAGA,IAAA;AAEA,IAAA;AACA,IAAA;AAAe,MAAA;AACC,MAAA;AACD,MAAA;AACb,MAAA;AACA,MAAA;AACiB;AAAA;AAAA;AAAA,MAAA;AAIL,MAAA;AACW,MAAA;AAEzB,IAAA;AAEA,IAAA;AACA,IAAA;AASA,IAAA;AAGA,IAAA;AACE,MAAA;AACA,MAAA;AAA2C,QAAA;AACxB,QAAA;AAAmB,QAAA;AAC3B,QAAA;AAAqB,MAAA;AAElC,IAAA;AACA,IAAA;AACE,MAAA;AACA,MAAA;AAAmC,QAAA;AACxB,QAAA;AAAyB,QAAA;AAAmB,QAAA;AACxC,QAAA;AACC,QAAA;AACG,QAAA;AACK,MAAA;AAE1B,IAAA;AACA,IAAA;AACE,MAAA;AACA,MAAA;AAAuC,QAAA;AACxB,QAAA;AAA6B,QAAA;AAAmB,MAAA;AAEjE,IAAA;AAIA,IAAA;AACE,MAAA;AACA,MAAA;AAAqC,QAAA;AAClB,QAAA;AAAmB,MAAA;AAExC,IAAA;AAEA,IAAA;AACE,MAAA;AACA,MAAA;AACF,IAAA;AAEA,IAAA;AACA,IAAA;AACA,IAAA;AACE,MAAA;AACA,MAAA;AACE,QAAA;AACA,QAAA;AAAA,MAAA;AAEF,MAAA;AACA,MAAA;AACF,IAAA;AAEA,IAAA;AAIA,IAAA;AACE,MAAA;AACF,IAAA;AAEA,IAAA;AACE,MAAA;AACE,QAAA;AAAsF,UAAA;AACnE,QAAA;AAClB,MAAA;AACK,MAAA;AAGV,IAAA;AAEA,IAAA;AAAmD,MAAA;AACnC,MAAA;AAEf,IAAA;AACD,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQA,EAAA;AACE,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQA,EAAA;AACE,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AASA,EAAA;AAKE,IAAA;AACE,MAAA;AACA,MAAA;AAAwC,QAAA;AACpB,QAAA;AACmE,QAAA;AACxB,QAAA;AACY,QAAA;AACjD,QAAA;AACxB,QAAA;AAC0D,QAAA;AACnC;AAAA;AAAA;AAAA;AAAA,QAAA;AAK0D,MAAA;AAErF,IAAA;AAEA,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AASA,EAAA;AAME,IAAA;AACE,MAAA;AACA,MAAA;AAA6C,QAAA;AACpB,UAAA;AACW,UAAA;AAChC,UAAA;AACuE,UAAA;AACxB,UAAA;AACY,UAAA;AAC3D,UAAA;AACY,QAAA;AACb,QAAA;AACD,MAAA;AAEJ,IAAA;AAEA,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AASA,EAAA;AAME,IAAA;AACA,IAAA;AACA,IAAA;AAIA,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAWA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAgBA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AAAM,MAAA;AACJ,MAAA;AACmB,QAAA;AACe,QAAA;AACE,QAAA;AACI,QAAA;AAC8B,MAAA;AAExE,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACE,MAAA;AACA,MAAA;AACF,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACE,MAAA;AACA,MAAA;AACF,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AAAO,MAAA;AACF,MAAA;AACqF,MAAA;AACzE,MAAA;AAEjB,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQA,EAAA;AAKE,IAAA;AACA,IAAA;AACE,MAAA;AAAgB,QAAA;AAAI,QAAA;AAAgB,QAAA;AAAoE,yBAAA;AAC1D,MAAA;AAChD,IAAA;AAEA,IAAA;AACF,EAAA;AAEA,EAAA;AACA,EAAA;AAAgC;AAAA;AAAA;AAAA;AAAA;AAOhC,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AAGI,MAAA;AAEA,MAAA;AAEA,MAAA;AACD,IAAA;AAGA,IAAA;AACL,EAAA;AAEA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AAEA,EAAA;AACE,IAAA;AACE,MAAA;AACA,MAAA;AACE,QAAA;AAAyE,UAAA;AACzB,UAAA;AACG,QAAA;AAEnD,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AAAqB,MAAA;AAEzB,IAAA;AACE,MAAA;AACF,IAAA;AACF,EAAA;AAEA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AAAA;AAGA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AASA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AAAuC,MAAA;AACuC,MAAA;AAC5D,MAAA;AACoD,MAAA;AAErE,IAAA;AACH,EAAA;AAAA;AAGA,EAAA;AACE,IAAA;AACE,MAAA;AACA,MAAA;AACF,IAAA;AAEA,IAAA;AACF,EAAA;AAAA;AAGA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AAIA,IAAA;AAIA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACF;AAEA;AACE,EAAA;AACA,EAAA;AACF;AAGA;AACE,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAEA;AAEE,EAAA;AACA,EAAA;AACE,IAAA;AAAU,MAAA;AAGV,IAAA;AACF,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACF;A9B0oGK;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/Users/anish/maximemai/maximem_synap-js-parity/synap/sdk/js/dist/index.cjs","sourcesContent":[null,"/**\n * AUTOGENERATED FROM synap/sdk/CONTRACT/endpoints.yaml -- DO NOT EDIT.\n * Regenerate with synap/sdk/scripts/sync-contract.sh.\n *\n * The /api/v1 vs /v1 prefix split is intentional and load-bearing. See the\n * contract file: normalising it 404s every affected call in production.\n */\n\nexport interface EndpointSpec {\n readonly method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';\n readonly path: string;\n /**\n * Whether a retry after an ambiguous failure is safe.\n *\n * False here means a retry can double-ingest and therefore double-bill.\n * See defect D / gotcha G-L in the migration plan.\n */\n readonly idempotent: boolean;\n}\n\nexport const ENDPOINTS = {\n whoami: { method: \"GET\", path: \"/api/v1/auth/whoami\", idempotent: true },\n memories_create: { method: \"POST\", path: \"/api/v1/memories/create\", idempotent: false },\n memories_batch: { method: \"POST\", path: \"/api/v1/memories/batch\", idempotent: false },\n memories_upload: { method: \"POST\", path: \"/api/v1/memories/upload\", idempotent: false },\n memories_status: { method: \"GET\", path: \"/api/v1/memories/status/{ingestion_id}\", idempotent: true },\n memories_get: { method: \"GET\", path: \"/api/v1/memories/{memory_id}\", idempotent: true },\n memories_update: { method: \"PATCH\", path: \"/api/v1/memories/{memory_id}\", idempotent: false },\n memories_delete: { method: \"DELETE\", path: \"/api/v1/memories/{memory_id}\", idempotent: true },\n memories_forget: { method: \"POST\", path: \"/api/v1/memories/forget\", idempotent: false },\n context_fetch_user: { method: \"POST\", path: \"/v1/context/user/fetch\", idempotent: true },\n context_fetch_customer: { method: \"POST\", path: \"/v1/context/customer/fetch\", idempotent: true },\n context_fetch_client: { method: \"POST\", path: \"/v1/context/client/fetch\", idempotent: true },\n context_fetch_conversation: { method: \"POST\", path: \"/v1/context/conversation/fetch\", idempotent: true },\n context_for_prompt: { method: \"GET\", path: \"/v1/conversations/{conversation_id}/context-for-prompt\", idempotent: true },\n conversation_turns: { method: \"GET\", path: \"/v1/conversations/{conversation_id}/messages\", idempotent: true },\n conversations_ingest: { method: \"POST\", path: \"/v1/conversations/ingest\", idempotent: false },\n conversations_messages: { method: \"POST\", path: \"/v1/conversations/messages\", idempotent: false },\n conversations_messages_batch: { method: \"POST\", path: \"/v1/conversations/messages/batch\", idempotent: false },\n conversations_compact: { method: \"POST\", path: \"/v1/conversations/compact\", idempotent: false },\n conversations_compacted: { method: \"GET\", path: \"/v1/conversations/{conversation_id}/compacted\", idempotent: true },\n conversations_compaction_status: { method: \"GET\", path: \"/v1/conversations/{conversation_id}/compaction/status\", idempotent: true },\n user_profile: { method: \"GET\", path: \"/v1/users/{user_id}/profile\", idempotent: true },\n credits_balance: { method: \"GET\", path: \"/v1/credits/balance\", idempotent: true },\n credits_ledger: { method: \"GET\", path: \"/v1/credits/ledger\", idempotent: true },\n credits_estimate: { method: \"POST\", path: \"/v1/credits/estimate\", idempotent: true },\n credits_redeem: { method: \"POST\", path: \"/v1/credits/redeem\", idempotent: false },\n telemetry_batch: { method: \"POST\", path: \"/v1/telemetry/batch\", idempotent: false },\n} as const satisfies Record<string, EndpointSpec>;\n\nexport type EndpointName = keyof typeof ENDPOINTS;\n\n/** Substitute `{placeholder}` segments. Values are URL-encoded.\n *\n * Encoding matters: an unencoded id containing `/` or `?` silently\n * retargets the request at a different route.\n */\nexport function resolvePath(\n name: EndpointName,\n params: Readonly<Record<string, string | number>> = {},\n): string {\n return ENDPOINTS[name].path.replace(/\\{(\\w+)\\}/g, (_m, key: string) => {\n const value = params[key];\n if (value === undefined || value === null || value === '') {\n throw new Error(\n `Endpoint ${name} requires path parameter '${key}'.`,\n );\n }\n return encodeURIComponent(String(value));\n });\n}\n","/**\n * Retry policy, mirroring `maximem_synap/resilience/retry.py` plus the\n * idempotency gate added there for defect D.\n *\n * ## Why idempotency is part of the retry decision\n *\n * The obvious retry rule is \"retry transient errors\". That rule double-bills.\n *\n * `POST /api/v1/memories/create` is metered ingestion. If the request reached\n * the server, was processed, and the *response* was lost, a retry ingests the\n * same content twice and bills for it twice. The client cannot tell that case\n * apart from \"the request never arrived\" by looking at the error type alone.\n *\n * So retries are gated on two independent questions:\n *\n * 1. Is the error transient? (worth retrying at all)\n * 2. Is the outcome *knowable*? (safe to retry a non-idempotent call)\n *\n * A connection error means the request provably never reached the application,\n * so the outcome is known: nothing happened. A read timeout means the request\n * may well have been processed. That is the ambiguous case, and for a\n * non-idempotent method it is where we stop.\n *\n * The residual window -- server accepted the request, began processing, then\n * died before responding -- cannot be closed from the client at all. Closing it\n * requires a server-honoured idempotency key on the ingest route. Until that\n * exists, this policy narrows the window rather than eliminating it.\n */\n\nimport { RateLimitError, SynapError, isSynapError } from '../errors.js';\n\n/** HTTP methods that are idempotent by specification. */\nconst IDEMPOTENT_METHODS: ReadonlySet<string> = new Set([\n 'GET',\n 'HEAD',\n 'OPTIONS',\n 'PUT',\n 'DELETE',\n]);\n\nexport interface RetryPolicy {\n maxAttempts: number;\n /** Base delay in seconds. */\n backoffBase: number;\n /** Delay cap in seconds. */\n backoffMax: number;\n /** Randomise the delay to avoid a thundering herd. */\n backoffJitter: boolean;\n}\n\nexport const DEFAULT_RETRY_POLICY: RetryPolicy = {\n maxAttempts: 3,\n backoffBase: 1.0,\n backoffMax: 10.0,\n backoffJitter: true,\n};\n\nexport interface RetryDecisionInput {\n error: unknown;\n /** 1-based. */\n attempt: number;\n policy: RetryPolicy;\n method: string;\n /**\n * True when the request may have been processed by the server despite the\n * error. Read timeouts and mid-flight disconnects are ambiguous; connection\n * errors and definitive HTTP status responses are not.\n */\n outcomeUnknown: boolean;\n /** Overrides idempotency for a route, e.g. a read-only POST. */\n idempotent?: boolean;\n}\n\nexport interface RetryDecision {\n retry: boolean;\n /** Seconds to wait before the next attempt. */\n delaySeconds: number;\n reason: string;\n}\n\nexport function shouldRetry(input: RetryDecisionInput): RetryDecision {\n const { error, attempt, policy, method, outcomeUnknown } = input;\n\n if (attempt >= policy.maxAttempts) {\n return { retry: false, delaySeconds: 0, reason: 'max_attempts_exhausted' };\n }\n\n if (!isSynapError(error) || !(error as SynapError).transient) {\n return { retry: false, delaySeconds: 0, reason: 'not_transient' };\n }\n\n const idempotent = input.idempotent ?? IDEMPOTENT_METHODS.has(method.toUpperCase());\n if (outcomeUnknown && !idempotent) {\n // The request may have been processed. For ingestion that means a retry\n // would store and bill twice. Surfacing the error is the cheaper mistake.\n return {\n retry: false,\n delaySeconds: 0,\n reason: 'non_idempotent_outcome_unknown',\n };\n }\n\n return {\n retry: true,\n delaySeconds: backoffDelay(error, attempt, policy),\n reason: 'transient_retryable',\n };\n}\n\n/** Exponential backoff, honouring Retry-After when the server sent one. */\nexport function backoffDelay(error: unknown, attempt: number, policy: RetryPolicy): number {\n if (error instanceof RateLimitError && error.retryAfterSeconds != null) {\n // The server told us when to come back. Guessing shorter just gets us\n // rejected again and burns quota.\n return Math.min(error.retryAfterSeconds, policy.backoffMax);\n }\n const exponential = policy.backoffBase * 2 ** (attempt - 1);\n const capped = Math.min(exponential, policy.backoffMax);\n if (!policy.backoffJitter) return capped;\n // Full jitter. Equal-jitter still synchronises a fleet after a shared outage.\n return Math.random() * capped;\n}\n\nexport const _IDEMPOTENT_METHODS = IDEMPOTENT_METHODS;\n","/**\n * Connection reuse for the HTTP transport.\n *\n * ## Why this file exists at all\n *\n * Node's global `fetch` keeps an idle socket for roughly 4 seconds. Our real\n * call pattern is one fetch per conversation turn, often minutes apart, so\n * essentially every call would pay a full connection setup. Through the CDN to\n * the origin that was measured at 600-950ms.\n *\n * The Python SDK already solved this: `keepalive_expiry=300s` plus a 240s\n * `/health` heartbeat that keeps the socket warm for as long as the client\n * lives. Porting the SDK without porting this would ship a \"native and faster\"\n * release that is measurably *slower* than the Python-subprocess wrapper it\n * replaces, on every call, from day one. That is gotcha G-A.\n *\n * ## Why it is optional and lazily loaded\n *\n * `undici` is Node-only. Edge runtimes and Workers have their own connection\n * pooling and no `dispatcher` option, so there the correct behavior is to use\n * global `fetch` unchanged. Importing undici eagerly would break those builds\n * at bundle time, so it is behind a lazy `import()` and every failure path\n * degrades to plain `fetch` rather than throwing.\n */\n\n/** Mirrors HTTPTransport.KEEPALIVE_EXPIRY_SECONDS in the Python SDK. */\nexport const KEEPALIVE_EXPIRY_MS = 300_000;\n/** Mirrors HTTPTransport.HEARTBEAT_INTERVAL_SECONDS. */\nexport const HEARTBEAT_INTERVAL_MS = 240_000;\nexport const MAX_CONNECTIONS = 20;\n\nexport interface KeepAliveOptions {\n keepAliveExpiryMs?: number;\n maxConnections?: number;\n}\n\n/** An undici Dispatcher, typed loosely so undici is not a hard type dependency. */\nexport type Dispatcher = object;\n\nlet cached: Dispatcher | null | undefined;\n\n/** True when we are on a runtime that can host an undici dispatcher. */\nexport function supportsDispatcher(): boolean {\n return (\n typeof process !== 'undefined' &&\n process.versions?.node !== undefined &&\n // Edge builds of Next expose a partial `process` shim, but no real Node.\n typeof (globalThis as { EdgeRuntime?: unknown }).EdgeRuntime === 'undefined'\n );\n}\n\n/**\n * Build (once) a dispatcher with Python-equivalent keep-alive.\n *\n * Returns null on any runtime where that is not possible, in which case the\n * caller should simply omit the dispatcher.\n */\nexport async function getKeepAliveDispatcher(\n options: KeepAliveOptions = {},\n): Promise<Dispatcher | null> {\n if (cached !== undefined) return cached;\n if (!supportsDispatcher()) {\n cached = null;\n return cached;\n }\n try {\n // Bundlers must not try to follow this statically on Edge builds.\n const undici = (await import(/* @vite-ignore */ 'undici')) as {\n Agent: new (opts: Record<string, unknown>) => Dispatcher;\n };\n cached = new undici.Agent({\n keepAliveTimeout: options.keepAliveExpiryMs ?? KEEPALIVE_EXPIRY_MS,\n // undici will not honour a keepAliveTimeout above this ceiling.\n keepAliveMaxTimeout: options.keepAliveExpiryMs ?? KEEPALIVE_EXPIRY_MS,\n connections: options.maxConnections ?? MAX_CONNECTIONS,\n });\n } catch {\n // undici is an optionalDependency. Missing or unloadable is not fatal:\n // global fetch still works, just with a short-lived pool.\n cached = null;\n }\n return cached;\n}\n\n/** Test seam. */\nexport function _resetDispatcherCache(): void {\n cached = undefined;\n}\n\n/**\n * Keep an idle connection warm.\n *\n * The timer is `unref`'d. Without that the Node event loop stays alive for as\n * long as the interval exists and any CLI built on this SDK hangs forever\n * instead of exiting. Python has no equivalent symptom, so no parity test would\n * ever catch it: that is gotcha G-J.\n */\nexport class Heartbeat {\n private timer: ReturnType<typeof setInterval> | null = null;\n\n constructor(\n private readonly ping: () => void | Promise<void>,\n private readonly intervalMs: number = HEARTBEAT_INTERVAL_MS,\n ) {}\n\n start(): void {\n if (this.timer !== null) return;\n // 0 disables, matching the Python transport's convention.\n if (this.intervalMs <= 0) return;\n if (typeof setInterval !== 'function') return;\n\n this.timer = setInterval(() => {\n // A failed heartbeat is not an error the caller should ever see. The\n // next real request will surface a genuine connectivity problem.\n void Promise.resolve(this.ping()).catch(() => {});\n }, this.intervalMs);\n\n (this.timer as { unref?: () => void }).unref?.();\n }\n\n stop(): void {\n if (this.timer !== null) {\n clearInterval(this.timer);\n this.timer = null;\n }\n }\n\n get running(): boolean {\n return this.timer !== null;\n }\n}\n","/**\n * The B2C identifier contract, client side.\n *\n * B2C send user_id, and nothing else. customer_id is not accepted.\n * B2B customer_id is required.\n *\n * The server is authoritative and rejects independently. This exists so the\n * failure happens at the call site, with a message that says what to do,\n * instead of as a round trip that returns an empty result.\n *\n * That last part is the whole reason. Sending a customer_id on a B2C instance\n * used to produce no exception anywhere: the write was filed under the\n * customer, the read asked for the user, and both returned success. One client\n * ran 4,634 consecutive empty fetches across seven days without a single error\n * to look at. An SDK that stays quiet about a misuse it can see is not being\n * permissive, it is hiding the bug.\n */\nimport { InvalidInputError } from './errors.js';\n\nexport const B2C_ISOLATION = 'equals_customer';\n\n/**\n * Throw if this call breaks the contract.\n *\n * `isolation` is what `GET /api/v1/auth/whoami` reported. **undefined means the\n * server did not tell us**, which is true of any deployment older than that\n * field, and it must mean \"do nothing\". Guessing B2C there would make this SDK\n * refuse a B2B client's mandatory field against every server not yet upgraded,\n * a far worse failure than the one it prevents.\n */\nexport function checkCustomerId(\n isolation: string | undefined,\n customerId: unknown,\n where: string,\n instanceId?: string,\n): void {\n if (customerId === undefined || customerId === null || customerId === '') return;\n if (isolation !== B2C_ISOLATION) return;\n throw new InvalidInputError(\n `${where}: customer_id is not accepted on this instance` +\n `${instanceId ? ` (${instanceId})` : ''}, which is B2C ` +\n `(user_context_isolation='${B2C_ISOLATION}'). Send user_id only; it is the ` +\n `whole identity, and the server files and reads your data under it. ` +\n `Received customer_id=${JSON.stringify(customerId)}.`,\n );\n}\n","/**\n * Native HTTP transport.\n *\n * Port of `maximem_synap/transport/http_client.py`. Uses global `fetch` so the\n * same code runs on Node, Edge, Workers and Bun, with an optional undici\n * dispatcher on Node to restore Python's connection reuse (see keepalive.ts).\n */\n\nimport {\n AgentUnavailableError,\n AuthenticationError,\n ContextNotFoundError,\n ConflictError,\n TranscriptConflictError,\n InsufficientCreditsError,\n InvalidInputError,\n NetworkTimeoutError,\n RateLimitError,\n ServiceUnavailableError,\n SynapError,\n} from '../errors.js';\nimport { newCorrelationId } from '../util/correlation.js';\nimport { ENDPOINTS, resolvePath, type EndpointName } from './endpoints.js';\nimport {\n DEFAULT_RETRY_POLICY,\n shouldRetry,\n type RetryPolicy,\n} from './retry.js';\nimport {\n getKeepAliveDispatcher,\n Heartbeat,\n HEARTBEAT_INTERVAL_MS,\n type KeepAliveOptions,\n} from './keepalive.js';\nimport { checkCustomerId } from '../scoping.js';\n\nexport const DEFAULT_BASE_URL = 'https://synap-cloud-prod.maximem.ai';\n\nexport interface TimeoutConfig {\n /** Seconds. Mirrors TimeoutConfig in the Python SDK. */\n connect: number;\n read: number;\n write: number;\n /**\n * gRPC stream idle timeout. Not used by the HTTP transport; the stream\n * client reads it. Present so a config object round-trips between the two\n * SDKs, and because Python's TimeoutConfig has it.\n */\n streamIdle: number;\n}\n\nexport const DEFAULT_TIMEOUTS: TimeoutConfig = {\n connect: 5.0, read: 30.0, write: 10.0, streamIdle: 60.0,\n};\n\nexport interface Credentials {\n apiKey: string;\n clientId: string;\n instanceId: string;\n}\n\nexport interface HttpTransportOptions {\n credentials: Credentials;\n baseUrl?: string;\n timeouts?: Partial<TimeoutConfig>;\n /** `null` disables retries entirely, as Python's `retry_policy=None` does. */\n retryPolicy?: Partial<RetryPolicy> | null;\n keepAlive?: KeepAliveOptions & { heartbeatIntervalMs?: number };\n userAgent?: string;\n /** Injected in tests. Defaults to global fetch. */\n fetchImpl?: typeof fetch;\n}\n\nexport interface RequestOptions {\n pathParams?: Record<string, string | number>;\n query?: Record<string, string | number | boolean | undefined>;\n body?: unknown;\n /**\n * Send `body` as-is instead of JSON-encoding it, and drop the JSON\n * Content-Type so the runtime can set `multipart/form-data` with its own\n * boundary. Setting Content-Type by hand for FormData produces a body the\n * server cannot parse, because the boundary will not match.\n */\n rawBody?: boolean;\n correlationId?: string;\n /** Overrides the endpoint's declared idempotency. */\n idempotent?: boolean;\n signal?: AbortSignal;\n}\n\ninterface AttemptOutcome {\n error: unknown;\n /** True when the server may have processed the request despite the failure. */\n outcomeUnknown: boolean;\n}\n\nexport class HttpTransport {\n readonly baseUrl: string;\n // Mutable so `configure()` can adjust them before the first request, and so\n // `initialize()` can write back the identity resolved from whoami.\n timeouts: TimeoutConfig;\n retryPolicy: RetryPolicy;\n\n #credentials: Credentials;\n private readonly userAgent: string;\n private readonly fetchImpl: typeof fetch;\n private readonly keepAliveOptions: KeepAliveOptions;\n private readonly heartbeat: Heartbeat;\n private closed = false;\n\n constructor(options: HttpTransportOptions) {\n this.#credentials = options.credentials;\n // Trailing slash would produce `//v1/...`, which some proxies 404.\n this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, '');\n this.timeouts = { ...DEFAULT_TIMEOUTS, ...options.timeouts };\n this.retryPolicy = options.retryPolicy === null\n ? { ...DEFAULT_RETRY_POLICY, maxAttempts: 1 }\n : { ...DEFAULT_RETRY_POLICY, ...options.retryPolicy };\n this.userAgent = options.userAgent ?? '@maximem/synap-js-sdk';\n this.fetchImpl = options.fetchImpl ?? globalThis.fetch.bind(globalThis);\n this.keepAliveOptions = options.keepAlive ?? {};\n this.heartbeat = new Heartbeat(\n () => this.pingHealth(),\n options.keepAlive?.heartbeatIntervalMs ?? HEARTBEAT_INTERVAL_MS,\n );\n }\n\n /** Start keeping the connection warm. Opt-in: it is pointless in serverless. */\n startHeartbeat(): void {\n if (!this.closed) this.heartbeat.start();\n }\n\n /**\n * Release timers. Safe to call more than once.\n *\n * This is the supported shutdown path, deliberately explicit rather than\n * hooked to `process.on('exit')`: those handlers leak a listener per client,\n * never fire in Lambda, and `process` does not exist in Workers (gotcha G-I).\n */\n /**\n * A COPY of the current credentials.\n *\n * A copy, not the live object: handing out the internal reference is how\n * `client.transport.credentials.apiKey` became readable and mutable from\n * outside in the first place.\n */\n currentCredentials(): Credentials {\n return { ...this.#credentials };\n }\n\n /** Write back identity resolved from `GET /api/v1/auth/whoami`. */\n updateCredentials(patch: Partial<Credentials>): void {\n this.#credentials = { ...this.#credentials, ...patch };\n }\n\n /**\n * The instance's scoping mode, from whoami. Lives here rather than on the\n * client because every interface factory already receives the transport,\n * and it is per-client, so a module-level global would be wrong the moment a\n * process holds two clients.\n *\n * undefined means the server did not report it, which is true of any\n * deployment older than the field, and must mean \"enforce nothing\".\n */\n userContextIsolation: string | undefined = undefined;\n\n /**\n * Set by the client to its identity resolution (`initialize()`'s whoami).\n * Python refuses every call before `initialize()`; here the first request\n * runs it instead, so a caller who skips it gets the same identity-dependent\n * behaviour (client-scope cache hits, the B2C check) rather than an error.\n */\n identityResolver: (() => Promise<void>) | undefined = undefined;\n\n /** Resolve identity once, before the first request. Never throws. */\n async ready(): Promise<void> {\n if (this.identityResolver !== undefined) await this.identityResolver();\n }\n\n /** Refuse a customer_id on a B2C instance, at the call site. */\n checkCustomerId(customerId: unknown, where: string): void {\n checkCustomerId(\n this.userContextIsolation, customerId, where, this.#credentials.instanceId,\n );\n }\n\n setTimeouts(timeouts: Partial<TimeoutConfig>): void {\n this.timeouts = { ...this.timeouts, ...timeouts };\n }\n\n /**\n * `null` DISABLES retries, matching Python.\n *\n * Python's `SDKConfig(retry_policy=None)` leaves `self.retry_policy` unset and\n * `max_attempts` falls to 1 (http_client.py:180). We restored the default\n * policy instead, so a caller following the \"Disabling Retries\" documentation\n * got three attempts and no way to turn them off.\n */\n setRetryPolicy(policy: Partial<RetryPolicy> | null): void {\n this.retryPolicy = policy === null\n ? { ...DEFAULT_RETRY_POLICY, maxAttempts: 1 }\n : { ...this.retryPolicy, ...policy };\n }\n\n close(): void {\n this.closed = true;\n this.heartbeat.stop();\n }\n\n async request<T = unknown>(name: EndpointName, options: RequestOptions = {}): Promise<T> {\n const spec = ENDPOINTS[name];\n const correlationId = options.correlationId ?? newCorrelationId();\n const idempotent = options.idempotent ?? spec.idempotent;\n // whoami IS the identity resolution, so it must not wait on itself.\n if (name !== 'whoami') await this.ready();\n\n let attempt = 0;\n let last: AttemptOutcome | null = null;\n\n while (attempt < this.retryPolicy.maxAttempts) {\n attempt += 1;\n try {\n return await this.attempt<T>(name, options, correlationId);\n } catch (error) {\n last = { error, outcomeUnknown: isOutcomeUnknown(error) };\n const decision = shouldRetry({\n error,\n attempt,\n policy: this.retryPolicy,\n method: spec.method,\n outcomeUnknown: last.outcomeUnknown,\n idempotent,\n });\n if (!decision.retry) throw error;\n await sleep(decision.delaySeconds * 1000, options.signal);\n }\n }\n throw last?.error ?? new SynapError('Request failed with no recorded error');\n }\n\n private async attempt<T>(\n name: EndpointName,\n options: RequestOptions,\n correlationId: string,\n ): Promise<T> {\n const spec = ENDPOINTS[name];\n const url = this.buildUrl(resolvePath(name, options.pathParams), options.query);\n\n const dispatcher = await getKeepAliveDispatcher(this.keepAliveOptions);\n const signal = withTimeout(this.timeouts.read * 1000, options.signal);\n\n const init: RequestInit = {\n method: spec.method,\n headers: this.buildHeaders(correlationId, { json: options.rawBody !== true }),\n signal: signal.signal,\n };\n if (options.body !== undefined && spec.method !== 'GET') {\n // `BodyInit` is a DOM-lib type and this package does not include that lib,\n // so the field's own type is used instead of naming it.\n init.body = options.rawBody === true\n ? (options.body as NonNullable<RequestInit['body']>)\n : JSON.stringify(options.body);\n }\n // `dispatcher` is a Node/undici extension. Its ambient type is only\n // present when undici's types are installed, so it is set structurally\n // rather than declared -- otherwise this file fails to compile for anyone\n // who has not installed the optional dependency.\n if (dispatcher) (init as Record<string, unknown>)['dispatcher'] = dispatcher;\n\n let response: Response;\n try {\n response = await this.fetchImpl(url, init);\n } catch (error) {\n throw toTransportError(error, correlationId);\n } finally {\n signal.dispose();\n }\n\n if (!response.ok) {\n throw await toHttpError(response, correlationId);\n }\n\n if (response.status === 204) return undefined as T;\n const text = await response.text();\n if (!text) return undefined as T;\n try {\n return JSON.parse(text) as T;\n } catch {\n throw new SynapError(`Malformed JSON in response from ${url}`, { correlationId });\n }\n }\n\n private buildUrl(path: string, query?: RequestOptions['query']): string {\n const url = new URL(this.baseUrl + path);\n for (const [k, v] of Object.entries(query ?? {})) {\n if (v !== undefined) url.searchParams.set(k, String(v));\n }\n return url.toString();\n }\n\n private buildHeaders(\n correlationId: string,\n options: { json?: boolean } = {},\n ): Record<string, string> {\n return {\n Authorization: `Bearer ${this.#credentials.apiKey}`,\n 'X-Client-ID': this.#credentials.clientId,\n 'X-Instance-ID': this.#credentials.instanceId,\n 'X-Correlation-ID': correlationId,\n ...(options.json === false ? {} : { 'Content-Type': 'application/json' }),\n 'User-Agent': this.userAgent,\n };\n }\n\n /** Unauthenticated and unmetered, exactly like the Python heartbeat. */\n private async pingHealth(): Promise<void> {\n const signal = withTimeout(10_000);\n try {\n const dispatcher = await getKeepAliveDispatcher(this.keepAliveOptions);\n const init: RequestInit = { signal: signal.signal };\n if (dispatcher) (init as Record<string, unknown>)['dispatcher'] = dispatcher;\n await this.fetchImpl(`${this.baseUrl}/health`, init);\n } finally {\n signal.dispose();\n }\n }\n}\n\n// ─── Error classification ─────────────────────────────────────────────────────\n\n/**\n * Whether the server may have processed the request despite this error.\n *\n * This is the input to the double-billing gate in retry.ts, so the bias is\n * deliberate: anything we cannot positively rule out counts as ambiguous.\n */\nexport function isOutcomeUnknown(error: unknown): boolean {\n // A definitive HTTP status means the server answered. Nothing ambiguous.\n if (error instanceof RateLimitError) return false;\n if (error instanceof ServiceUnavailableError) return false;\n if (error instanceof AuthenticationError) return false;\n if (error instanceof InvalidInputError) return false;\n if (error instanceof ConflictError) return false;\n if (error instanceof InsufficientCreditsError) return false;\n if (error instanceof ContextNotFoundError) return false;\n\n // Timeouts are the ambiguous case: the request may have been handled and\n // only the response lost.\n if (error instanceof NetworkTimeoutError) return true;\n\n // Connection-level failures mean the request never reached the application.\n if (isConnectionRefusal(error)) return false;\n\n return true;\n}\n\nfunction isConnectionRefusal(error: unknown): boolean {\n const code = (error as { cause?: { code?: string }; code?: string })?.cause?.code\n ?? (error as { code?: string })?.code;\n return (\n code === 'ECONNREFUSED' ||\n code === 'ENOTFOUND' ||\n code === 'EAI_AGAIN' ||\n code === 'ECONNRESET' ||\n code === 'EHOSTUNREACH'\n );\n}\n\nfunction toTransportError(error: unknown, correlationId: string): SynapError {\n if (error instanceof SynapError) return error;\n const name = (error as { name?: string })?.name;\n if (name === 'AbortError' || name === 'TimeoutError') {\n return new NetworkTimeoutError('Request timed out', { correlationId, cause: error });\n }\n return new ServiceUnavailableError(\n `Network request failed: ${(error as Error)?.message ?? String(error)}`,\n { correlationId, cause: error },\n );\n}\n\n/**\n * Values the credit gate sends as the body's `error` field. Only these are read\n * as a reason, so an unrelated 429 carrying some other `error` string is not\n * mistaken for a credit stop. Mirrors `_CREDIT_ERRORS` in `http_client.py`.\n */\nconst CREDIT_ERRORS = new Set(['insufficient_credits', 'trial_limit_reached']);\n\n/**\n * The credit gate's structured body, however the server nested it.\n *\n * The gate raises an HTTPException, so FastAPI wraps its payload in `detail`;\n * the burn-service path returns the same fields at the top level. Reading only\n * the top level (what this transport did) meant the gate's balance, reason and\n * URLs never reached the caller.\n */\nfunction creditPayload(\n parsed: Record<string, unknown>,\n detailObject: Record<string, unknown> | null,\n): Record<string, unknown> {\n return detailObject ?? parsed;\n}\n\n/** Why the request was refused, or null if this was not a credit stop. */\nfunction creditReason(payload: Record<string, unknown>): string | null {\n const reason = stringOrNull(payload['reason']);\n if (reason !== null) return reason;\n const error = stringOrNull(payload['error']);\n return error !== null && CREDIT_ERRORS.has(error) ? error : null;\n}\n\nasync function toHttpError(response: Response, correlationId: string): Promise<SynapError> {\n const body = await response.text().catch(() => '');\n let parsed: Record<string, unknown> = {};\n try {\n parsed = JSON.parse(body) as Record<string, unknown>;\n } catch {\n /* body is not JSON; the raw text is still used in the message */\n }\n // `detail` is a string on most routes but a structured object on the ones\n // that need a machine-readable code: {\"detail\": {\"code\": ..., \"message\": ...}}.\n // Casting it straight to string rendered those as \"[object Object]\".\n const rawDetail = parsed['detail'] ?? parsed['message'] ?? body ?? '';\n const detailObject =\n typeof rawDetail === 'object' && rawDetail !== null\n ? (rawDetail as Record<string, unknown>)\n : null;\n const detail =\n detailObject !== null\n ? String(detailObject['message'] ?? JSON.stringify(detailObject))\n : String(rawDetail);\n const message = `HTTP ${response.status}: ${detail || response.statusText}`;\n const opts = { correlationId };\n\n switch (response.status) {\n case 400:\n case 422:\n return new InvalidInputError(message, opts);\n case 401:\n case 403:\n return new AuthenticationError(message, opts);\n case 404:\n return new ContextNotFoundError(message, opts);\n case 402: {\n // The server sends `minimum_required_credits` (see\n // synap/cloud/application/credits/enforcement.py). We read\n // `required_credits` for a while, a key that never exists, so\n // `requiredCredits` was always null. `recovery_url` and `redeem_url`\n // were dropped entirely, leaving no way to surface a top-up link.\n const credit = creditPayload(parsed, detailObject);\n return new InsufficientCreditsError(message, {\n ...opts,\n balanceCredits: numberOrNull(credit['balance_credits']),\n requiredCredits:\n numberOrNull(credit['minimum_required_credits']) ??\n numberOrNull(credit['required_credits']),\n recoveryUrl: stringOrNull(credit['recovery_url']),\n redeemUrl: stringOrNull(credit['redeem_url']),\n reason: creditReason(credit),\n manageUrl: stringOrNull(credit['manage_url']),\n });\n }\n case 409:\n // Conflict is PERMANENT and never retried. Python discriminates on the\n // structured body so a transcript-immutability conflict becomes\n // TranscriptConflictError and anything else (e.g. compact()'s \"already\n // in progress\") stays the generic ConflictError. Without this, a\n // customer's `catch (e) { if (e instanceof TranscriptConflictError) }`\n // never fires in JS while the identical Python code works.\n if (detailObject !== null && detailObject['code'] === 'transcript_conflict') {\n return new TranscriptConflictError(message, opts);\n }\n return new ConflictError(message, opts);\n case 429: {\n // Either an ordinary rate limit or a credit stop that time or a plan\n // change clears: the spent Trial cap (`trial_limit_reached`) and a paid\n // plan whose subscription lapsed (`subscription_inactive`). The credit\n // fields are absent on the plain rate limit.\n const header = response.headers.get('retry-after');\n const retryAfter = header !== null && header.trim() !== '' ? Number(header) : null;\n const credit = creditPayload(parsed, detailObject);\n return new RateLimitError(message, {\n ...opts,\n retryAfterSeconds: retryAfter !== null && Number.isFinite(retryAfter) ? retryAfter : null,\n reason: creditReason(credit),\n upgradeUrl: stringOrNull(credit['upgrade_url']),\n manageUrl: stringOrNull(credit['manage_url']),\n });\n }\n case 503:\n return new ServiceUnavailableError(message, opts);\n case 504:\n return new AgentUnavailableError(message, opts);\n default:\n if (response.status >= 500) return new ServiceUnavailableError(message, opts);\n return new SynapError(message, opts);\n }\n}\n\nfunction numberOrNull(v: unknown): number | null {\n return typeof v === 'number' && Number.isFinite(v) ? v : null;\n}\n\nfunction stringOrNull(v: unknown): string | null {\n return typeof v === 'string' && v !== '' ? v : null;\n}\n\n// ─── Utilities ────────────────────────────────────────────────────────────────\n\ninterface DisposableSignal {\n signal: AbortSignal;\n dispose(): void;\n}\n\n/**\n * Compose a timeout with a caller-supplied signal.\n *\n * Hand-rolled rather than `AbortSignal.timeout` + `AbortSignal.any` because\n * `any` is Node 20.3+ and this must also run on Workers and older Bun. The\n * timer is cleared in `dispose` so a fast response does not hold the event\n * loop open for the full timeout.\n */\nfunction withTimeout(ms: number, external?: AbortSignal): DisposableSignal {\n const controller = new AbortController();\n const onExternalAbort = () => controller.abort(external?.reason);\n const timer = setTimeout(() => {\n controller.abort(new NetworkTimeoutError(`Timed out after ${ms}ms`));\n }, ms);\n (timer as { unref?: () => void }).unref?.();\n\n if (external) {\n if (external.aborted) onExternalAbort();\n else external.addEventListener('abort', onExternalAbort, { once: true });\n }\n\n return {\n signal: controller.signal,\n dispose() {\n clearTimeout(timer);\n external?.removeEventListener('abort', onExternalAbort);\n },\n };\n}\n\n/**\n * Retry backoff.\n *\n * Deliberately NOT `unref`'d, unlike the heartbeat timer in keepalive.ts. That\n * distinction is the whole point: a heartbeat is background work and must not\n * hold the process open, but a backoff is FOREGROUND work that a caller is\n * awaiting. With it unref'd, a process whose only pending work was this sleep\n * exited during the backoff and the awaited request never settled -- Node\n * reports it as \"Detected unsettled top-level await\". Invisible in a server\n * with other traffic; fatal in a CLI or an idle serverless handler.\n */\nfunction sleep(ms: number, signal?: AbortSignal): Promise<void> {\n if (ms <= 0) return Promise.resolve();\n return new Promise((resolve, reject) => {\n const timer = setTimeout(() => {\n signal?.removeEventListener('abort', onAbort);\n resolve();\n }, ms);\n function onAbort() {\n clearTimeout(timer);\n reject(signal?.reason ?? new Error('Aborted'));\n }\n signal?.addEventListener('abort', onAbort, { once: true });\n });\n}\n","/**\n * `response_shape: \"ranked\"`: one list in the order the server ranked it.\n *\n * Ports the ranked half of Python's `models/context.py` (`rank_items`,\n * `_wire_rank_score`, `_wire_category` and the ranked branch of\n * `ContextResponse.from_cloud_response`). The cases are pinned in\n * `CONTRACT/conformance/ranked_shape.json`, captured from Python.\n *\n * Opt-in. A by-type fetch, the default, is returned exactly as the server sent\n * it, so nothing here touches a caller who does not ask for the ranked shape.\n */\n\nimport type { Json, RawContext, RawContextItem } from './types.js';\n\n/** The two shapes a context fetch can ask for. */\nexport const RESPONSE_SHAPES = ['by_type', 'ranked'] as const;\nexport type ResponseShape = (typeof RESPONSE_SHAPES)[number];\n\n/** The memory types, in the order the by-type response lists them. */\nexport const MEMORY_TYPES = [\n 'facts',\n 'preferences',\n 'episodes',\n 'emotions',\n 'temporal_events',\n] as const;\n\n/**\n * The type of a chunk in a ranked response: a quote from a source document,\n * not an extracted memory. A by-type response files it under `facts`.\n */\nexport const EXCERPT_CATEGORY = 'excerpts';\n\n/**\n * An item's rank score: `metadata.rank_score` on the HTTP response, a\n * top-level `rank_score` on a prefetched (gRPC) item. `null` when neither is a\n * number, which is how a server that predates ranking reads.\n */\nexport function wireRankScore(item: RawContextItem): number | null {\n const md = item.metadata;\n let raw: unknown = md !== null && typeof md === 'object' ? (md as Json)['rank_score'] : undefined;\n if (raw === undefined || raw === null) raw = item['rank_score'];\n return typeof raw === 'number' && Number.isFinite(raw) ? raw : null;\n}\n\n/**\n * An item's type. A chunk is an excerpt whatever list it came in: a server or\n * a prefetched bundle that predates ranking files it under facts.\n */\nexport function wireCategory(item: RawContextItem, fallback = ''): string {\n if (item.source === 'chunks') return EXCERPT_CATEGORY;\n const category = item['category'];\n if (typeof category === 'string' && category !== '') return category;\n const contextType = item['context_type'];\n if (typeof contextType === 'string' && contextType !== '') return contextType;\n return fallback;\n}\n\n/** Highest score first; items without one after, in the order they came. */\nfunction byRankScore(a: RawContextItem, b: RawContextItem): number {\n const sa = wireRankScore(a);\n const sb = wireRankScore(b);\n if (sa === null && sb === null) return 0;\n if (sa === null) return 1;\n if (sb === null) return -1;\n return sb - sa;\n}\n\n/**\n * Merge per-type item lists into one list in rank order, numbered from 1.\n *\n * For a server that predates `response_shape` (it returned the five lists) and\n * for a prefetched cache hit. Each returned item is a copy carrying `rank`,\n * `category` and `id` (a prefetched item names it `item_id`), so the input is\n * not changed. `Array.prototype.sort` is stable, like Python's.\n */\nexport function rankItems(byType: RawContext): RawContextItem[] {\n const flat: RawContextItem[] = [];\n // The five lists only: a raw context also carries other arrays\n // (`conversations` in summary mode), and none of them are memories.\n for (const type of MEMORY_TYPES) {\n const list = (byType as Json)[type];\n if (!Array.isArray(list)) continue;\n for (const it of list as RawContextItem[]) {\n if (it === null || typeof it !== 'object') continue;\n flat.push({\n ...it,\n category: wireCategory(it, type),\n id: (it.id || it.item_id || '') as string,\n });\n }\n }\n flat.sort(byRankScore);\n return flat.map((it, n) => ({ ...it, rank: n + 1 }));\n}\n\n/**\n * The raw context in the shape the caller asked for.\n *\n * \"ranked\": `items` in rank order (the server's own list, or one rebuilt by\n * rank score when the server or the cache predates the parameter), the five\n * typed lists as views of those items, and `excerpts` for the chunks. A\n * response that already carries `items` is read the same way even when the\n * caller did not ask, as Python does. Anything else is returned untouched.\n */\nexport function applyResponseShape(raw: RawContext, shape: string = 'by_type'): RawContext {\n const wire = (raw as Json)['items'];\n if (shape !== 'ranked' && !Array.isArray(wire)) return raw;\n const source: RawContextItem[] = Array.isArray(wire)\n ? (wire as RawContextItem[])\n : rankItems(raw);\n const items = source.map((it) => ({\n ...it,\n id: (it.id || it.item_id || '') as string,\n category: wireCategory(it),\n }));\n const out: RawContext = { ...raw, items };\n for (const type of MEMORY_TYPES) {\n out[type] = items.filter((it) => it['category'] === type);\n }\n (out as Json)['excerpts'] = items.filter((it) => it['category'] === EXCERPT_CATEGORY);\n return out;\n}\n\n/** How each type is named on a line of the ranked prompt list. */\nconst RANKED_LABELS: Record<string, string> = {\n facts: 'fact',\n preferences: 'preference',\n episodes: 'episode',\n emotions: 'emotion',\n temporal_events: 'temporal event',\n [EXCERPT_CATEGORY]: 'excerpt',\n};\n\n/** The label printed in front of a ranked item: `fact`, `temporal event`, ... */\nexport function rankedLabel(category: string): string {\n return RANKED_LABELS[category] ?? (category !== '' ? category : 'item');\n}\n\n/** Shared by the ranked merge: the same ordering Python's `merge` sorts with. */\nexport function sortByRankScore(items: RawContextItem[]): RawContextItem[] {\n return [...items].sort(byRankScore);\n}\n","/**\n * Scope-aware context fetching.\n *\n * Scopes are client > customer > user. `conversation_id` is NOT a tier: it\n * groups turns within a scope. Passing it narrows an existing scope rather\n * than selecting a different one.\n */\n\nimport type { EndpointName } from '../transport/endpoints.js';\nimport type { HttpTransport } from '../transport/http.js';\nimport { InvalidInputError } from '../errors.js';\nimport { RESPONSE_SHAPES, applyResponseShape } from './ranked.js';\nimport type { FetchOptions, Json, RawContext } from './types.js';\n\nexport type Scope = 'user' | 'customer' | 'client' | 'conversation';\n\nconst ENDPOINT_BY_SCOPE: Record<Scope, EndpointName> = {\n user: 'context_fetch_user',\n customer: 'context_fetch_customer',\n client: 'context_fetch_client',\n conversation: 'context_fetch_conversation',\n};\n\n/** Reads either spelling of an option. Callers depend on both. */\nfunction opt<T>(options: FetchOptions, snake: keyof FetchOptions, camel: keyof FetchOptions): T | undefined {\n const a = options[snake];\n if (a !== undefined) return a as T;\n const b = options[camel];\n return b === undefined ? undefined : (b as T);\n}\n\nexport interface BuiltRequest {\n endpoint: EndpointName;\n body: Json;\n}\n\nexport function buildFetchRequest(scope: Scope, options: FetchOptions): BuiltRequest {\n const userId = opt<string>(options, 'user_id', 'userId');\n const customerId = opt<string>(options, 'customer_id', 'customerId');\n const conversationId = opt<string>(options, 'conversation_id', 'conversationId');\n const searchQuery = opt<string[]>(options, 'search_query', 'searchQuery');\n const maxResults = opt<number>(options, 'max_results', 'maxResults') ?? 10;\n const queryDate = opt<string>(options, 'query_date', 'queryDate');\n const perQuery = opt<number>(options, 'max_results_per_query', 'maxResultsPerQuery');\n const responseShape = responseShapeOf(options);\n const { mode, precisionLevel } = retrievalOptionsOf(options);\n\n if (searchQuery !== undefined && !Array.isArray(searchQuery)) {\n throw new InvalidInputError('search_query must be an array when provided');\n }\n if (options.types !== undefined && !Array.isArray(options.types)) {\n throw new InvalidInputError('types must be an array when provided');\n }\n\n // Field-for-field identical to what the Python controllers post, because the\n // server is tuned against that payload and Python is the reference\n // implementation. In particular:\n // - conversation_id is ALWAYS present, null when unset, not omitted\n // - mode always defaults to \"fast\"\n // - precision_level is sent ONLY when it differs from \"high\"\n // - query_date is sent ONLY when the caller supplied one\n // - max_results_per_query is sent ONLY when the caller supplied one\n // - response_shape is sent ONLY when \"ranked\"\n // - include_conversation_context is sent ONLY when false\n // Sending an extra key, or omitting one Python always sends, is exactly the\n // kind of difference that shows up as \"works in Python, empty in JS\".\n const body: Json = {\n conversation_id: conversationId ?? null,\n search_query: searchQuery ?? [],\n max_results: maxResults,\n types: options.types ?? ['all'],\n mode,\n };\n if (precisionLevel !== 'high') body['precision_level'] = precisionLevel;\n // Omitted at its default, for the same reason precision_level is.\n if (queryDate) body['query_date'] = queryDate;\n // Omitted when unset, and `0` counts as unset, as in Python.\n if (perQuery) body['max_results_per_query'] = perQuery;\n if (responseShape !== 'by_type') body['response_shape'] = responseShape;\n if (options.include_conversation_context === false) {\n body['include_conversation_context'] = false;\n }\n // On all four scopes, as Python sends it. Until 0.5.10 only the user scope\n // did, so a scope_path passed to the client, customer or conversation fetch\n // was dropped and the read was served at the default levels instead.\n // Omitted when absent or empty, like Python's `if scope_path:`, so a caller\n // who does not use custom levels sends exactly the body they sent before.\n const scopePath = opt<Record<string, string>>(options, 'scope_path', 'scopePath');\n if (scopePath !== undefined && scopePath !== null && Object.keys(scopePath).length > 0) {\n body['scope'] = scopePath;\n }\n\n switch (scope) {\n case 'user': {\n if (!userId) throw new InvalidInputError('user_id is required');\n body['user_id'] = userId;\n\n const contextMode = opt<string>(options, 'context_mode', 'contextMode') ?? 'in-conversation';\n if (!VALID_CONTEXT_MODES.includes(contextMode)) {\n throw new InvalidInputError(\n `Invalid context_mode '${contextMode}'. Must be one of: ${VALID_CONTEXT_MODES.join(', ')}`,\n );\n }\n const lastN = opt<number>(options, 'last_n_conversations', 'lastNConversations') ?? 1;\n if (!Number.isInteger(lastN)) {\n throw new InvalidInputError('last_n_conversations must be an integer');\n }\n if (lastN < 0 || lastN > 20) {\n throw new InvalidInputError('last_n_conversations must be between 0 and 20');\n }\n\n // Conditional, exactly as Python does it: the summary-mode keys are sent\n // ONLY in summary mode, so an in-conversation fetch is byte-identical to\n // one that never knew about them.\n if (contextMode === 'conversation-summary') {\n body['context_mode'] = contextMode;\n body['include_profile'] = opt<boolean>(options, 'include_profile', 'includeProfile') ?? true;\n body['last_n_conversations'] = lastN;\n }\n // Python sends this only when not None. It is passed through the same\n // way rather than defaulted to '', so the server sees the same request\n // from either SDK.\n if (customerId !== undefined) body['customer_id'] = customerId;\n break;\n }\n break;\n case 'customer':\n if (!customerId) throw new InvalidInputError('customer_id is required');\n body['customer_id'] = customerId;\n break;\n case 'conversation':\n if (!conversationId) throw new InvalidInputError('conversation_id is required');\n if (userId !== undefined) body['user_id'] = userId;\n if (customerId !== undefined) body['customer_id'] = customerId;\n break;\n case 'client':\n break;\n }\n\n return { endpoint: ENDPOINT_BY_SCOPE[scope], body };\n}\n\n/**\n * Hooks that let the client serve a fetch locally and report the outcome.\n *\n * Passed in rather than imported so this module stays free of the cache and\n * the stream: it is the one place every scope's fetch goes through, and it\n * should not grow a dependency on either.\n */\nexport interface AnticipatedServe {\n response: RawContext;\n servedItemIds: string[];\n bundleId: string;\n /**\n * A semantic match below the sure bar. The server is asked too, and its\n * answer is used if it arrives within `raceDeadlineMs`. Python:\n * `_race_borderline`.\n */\n borderline?: boolean;\n raceDeadlineMs?: number;\n}\n\nexport interface FetchHooks {\n /** Returns a response to short-circuit the network, or null to continue. */\n beforeFetch?: (\n scope: Scope,\n options: FetchOptions,\n ) => AnticipatedServe | null | Promise<AnticipatedServe | null>;\n /** Called after a cache hit is served, for the learning-loop event. */\n onServedFromCache?: (\n scope: Scope,\n options: FetchOptions,\n served: { servedItemIds: string[]; bundleId: string },\n ) => void;\n /** Called after EVERY fetch resolves, cache or network, for the audit event. */\n onAssembled?: (\n scope: Scope,\n options: FetchOptions,\n response: RawContext,\n startedAt: number,\n ) => void;\n}\n\nconst VALID_CONTEXT_MODES = ['in-conversation', 'conversation-summary'];\n\n/** The shape asked for, validated as Python validates it, before any request. */\nexport function responseShapeOf(options: FetchOptions): string {\n const shape = opt<string>(options, 'response_shape', 'responseShape') ?? 'by_type';\n if (!(RESPONSE_SHAPES as readonly string[]).includes(shape)) {\n throw new InvalidInputError(\n `Invalid response_shape '${shape}'. Must be one of: ${RESPONSE_SHAPES.join(', ')}`,\n );\n }\n return shape;\n}\n\nconst VALID_MODES = ['fast', 'accurate'];\nconst VALID_PRECISION = ['high', 'medium'];\n\n/**\n * `mode` and `precision_level`, validated as Python validates them, before any\n * request. Without this a typo reached the server, which reads anything that\n * is not \"fast\" as \"accurate\": a slower, costlier read and no error.\n */\nexport function retrievalOptionsOf(options: FetchOptions): { mode: string; precisionLevel: string } {\n const mode = options.mode ?? 'fast';\n if (!VALID_MODES.includes(mode)) {\n throw new InvalidInputError(\n `Invalid mode '${mode}'. Must be one of: ${VALID_MODES.join(', ')}`,\n );\n }\n const precisionLevel = opt<string>(options, 'precision_level', 'precisionLevel') ?? 'high';\n if (!VALID_PRECISION.includes(precisionLevel)) {\n throw new InvalidInputError(\n `Invalid precision_level '${precisionLevel}'. Must be one of: ${VALID_PRECISION.join(', ')}`,\n );\n }\n return { mode, precisionLevel };\n}\n\n/** Fetches and returns the RAW snake_case context, as the namespaced surface does. */\nexport async function fetchContext(\n transport: HttpTransport,\n scope: Scope,\n options: FetchOptions = {},\n hooks: FetchHooks = {},\n): Promise<RawContext> {\n // Anticipation cache FIRST, as Python does on all four scope fetches. Without\n // this the stream's bundles are never read and every fetch is a billed round\n // trip.\n const startedAt = Date.now();\n\n // Identity first, as Python requires initialize() before any fetch: the\n // cache lookup below filters on client_id and the B2C check on the\n // instance's isolation, and both come from whoami. Resolved once, on the\n // first request of a client that never called initialize().\n await transport.ready?.();\n\n // Before the anticipation cache, not after: a cached hit would otherwise let\n // an illegal shape through silently, which is the exact failure mode this\n // contract exists to remove.\n transport.checkCustomerId(\n opt<string>(options, 'customer_id', 'customerId'),\n `${scope}.context.fetch`,\n );\n\n // Validated before the cache, like customer_id above, so a bad value fails\n // the same way whether or not a prefetched bundle would have answered.\n const responseShape = responseShapeOf(options);\n retrievalOptionsOf(options);\n\n const network = async (): Promise<RawContext> => {\n const { endpoint, body } = buildFetchRequest(scope, options);\n const result = await transport.request<Json>(endpoint, { body });\n // The wrapper returned `result.context || {}`; preserve that exactly.\n return applyResponseShape((result?.['context'] as RawContext) ?? {}, responseShape);\n };\n\n // Awaited: matching by meaning embeds the query with the local model first.\n const anticipated = (await hooks.beforeFetch?.(scope, options)) ?? null;\n if (anticipated !== null && anticipated.borderline) {\n // A borderline match races the server: its answer if in time, the cached\n // one if not. A late server call finishes unobserved.\n const pending = network();\n pending.catch(() => undefined);\n let timer: ReturnType<typeof setTimeout> | undefined;\n const late = new Promise<null>((resolve) => {\n timer = setTimeout(() => resolve(null), anticipated.raceDeadlineMs ?? 250);\n });\n const won = await Promise.race([pending.then((r) => r, () => null), late]);\n if (timer !== undefined) clearTimeout(timer);\n if (won !== null) {\n hooks.onAssembled?.(scope, options, won, startedAt);\n return won;\n }\n }\n if (anticipated !== null) {\n hooks.onServedFromCache?.(scope, options, anticipated);\n hooks.onAssembled?.(scope, options, anticipated.response, startedAt);\n // A hit is items picked from several prefetched bundles, so a ranked hit\n // is ordered by each item's own rank_score.\n return applyResponseShape(anticipated.response, responseShape);\n }\n\n const response = await network();\n hooks.onAssembled?.(scope, options, response, startedAt);\n return response;\n}\n","/**\n * camelCase normalisers for the legacy top-level methods.\n *\n * A faithful port of the normalisers in the removed `src/synap-client.js`,\n * including their quirks:\n *\n * - `score` and `source` on a memory item are passed through WITHOUT a\n * default, so `undefined` survives. Adding `?? 0` here would change the\n * shape consumers already branch on.\n * - `strength` falls back to `confidence`, `summary` falls back to `content`,\n * `intensity` falls back to `confidence`. Those fallbacks exist because the\n * server does not always send the specific field.\n * - `pickDefined` treats `null` as a real value and only skips `undefined`,\n * so an explicit server `null` is preserved rather than replaced.\n *\n * See gotcha G-B: this shape is one of two live shapes and must not converge\n * with the raw one before 2.0.\n */\n\nimport type {\n ConversationContext, ContextMetadata, Emotion, Episode, Fact, Json,\n NormalisedContext, Preference, RawContext, RawContextItem,\n RawConversationContext, RetrievalHealth, TemporalEvent, TemporalFields,\n} from './types.js';\n\n/** Returns the first argument that is not `undefined`. `null` counts as a value. */\nexport function pickDefined<T>(...values: (T | undefined)[]): T | undefined {\n for (const v of values) if (v !== undefined) return v;\n return undefined;\n}\n\nfunction pick<T>(...values: (T | undefined)[]): T {\n return pickDefined(...values) as T;\n}\n\nconst obj = (v: unknown): Json => (v as Json) ?? {};\n\n/** Always an array, so a caller can group by it without a null check. */\nfunction matchedQueries(item: RawContextItem = {}): string[] {\n const raw = item['matchedQueries'] ?? item.matched_queries;\n return Array.isArray(raw) ? (raw as string[]) : [];\n}\n\nfunction normalizeTemporalFields(item: RawContextItem = {}): TemporalFields {\n return {\n eventDate: pick(item['eventDate'] as string, item.event_date, null),\n eventEnd: pick(item['eventEnd'] as string, item.event_end as string, null),\n eventPrecision: pick(item['eventPrecision'] as string, item.event_precision as string, null),\n saidAt: pick(item['saidAt'] as string, item.said_at, null),\n validUntil: pick(item['validUntil'] as string, item.valid_until, null),\n temporalCategory: pick(item['temporalCategory'] as string, item.temporal_category, null),\n temporalConfidence: pick(item['temporalConfidence'] as number, item.temporal_confidence, 0),\n };\n}\n\nexport function normalizeFact(item: RawContextItem = {}): Fact {\n return {\n id: item.id || '',\n content: item.content || '',\n confidence: pick(item.confidence, 0),\n source: item.source || '',\n extractedAt: pick(item['extractedAt'] as string, item.extracted_at, null),\n metadata: obj(item.metadata),\n ...normalizeTemporalFields(item),\n matchedQueries: matchedQueries(item),\n };\n}\n\nexport function normalizePreference(item: RawContextItem = {}): Preference {\n return {\n id: item.id || '',\n category: (item['category'] as string) || '',\n content: item.content || '',\n strength: pick(item['strength'] as number, item.confidence, 0),\n source: item.source || '',\n extractedAt: pick(item['extractedAt'] as string, item.extracted_at, null),\n metadata: obj(item.metadata),\n ...normalizeTemporalFields(item),\n matchedQueries: matchedQueries(item),\n };\n}\n\nexport function normalizeEpisode(item: RawContextItem = {}): Episode {\n return {\n id: item.id || '',\n summary: pick(item['summary'] as string, item.content, ''),\n occurredAt: pick(item['occurredAt'] as string, item['occurred_at'] as string, null),\n significance: pick(item['significance'] as number, item.confidence, 0),\n participants: (item['participants'] as unknown[]) || [],\n metadata: obj(item.metadata),\n ...normalizeTemporalFields(item),\n matchedQueries: matchedQueries(item),\n };\n}\n\nexport function normalizeEmotion(item: RawContextItem = {}): Emotion {\n return {\n id: item.id || '',\n emotionType: pick(item['emotionType'] as string, item['emotion_type'] as string, ''),\n intensity: pick(item['intensity'] as number, item.confidence, 0),\n detectedAt: pick(item['detectedAt'] as string, item['detected_at'] as string, null),\n context: (item['context'] as string) || '',\n metadata: obj(item.metadata),\n ...normalizeTemporalFields(item),\n matchedQueries: matchedQueries(item),\n };\n}\n\nexport function normalizeTemporalEvent(item: RawContextItem = {}): TemporalEvent {\n return {\n id: item.id || '',\n content: item.content || '',\n eventDate: pick(item['eventDate'] as string, item.event_date, null),\n eventEnd: pick(item['eventEnd'] as string, item.event_end as string, null),\n eventPrecision: pick(item['eventPrecision'] as string, item.event_precision as string, null),\n saidAt: pick(item['saidAt'] as string, item.said_at, null),\n validUntil: pick(item['validUntil'] as string, item.valid_until, null),\n temporalCategory: pick(item['temporalCategory'] as string, item.temporal_category, ''),\n temporalConfidence: pick(item['temporalConfidence'] as number, item.temporal_confidence, 0),\n confidence: pick(item.confidence, 0),\n source: item.source || '',\n extractedAt: pick(item['extractedAt'] as string, item.extracted_at, null),\n metadata: obj(item.metadata),\n matchedQueries: matchedQueries(item),\n };\n}\n\nexport function normalizeContextMetadata(metadata: Json = {}): ContextMetadata {\n return {\n correlationId: pick(metadata['correlationId'] as string, metadata['correlation_id'] as string, ''),\n ttlSeconds: pick(metadata['ttlSeconds'] as number, metadata['ttl_seconds'] as number, 0),\n source: (metadata['source'] as string) || 'unknown',\n retrievedAt: pick(metadata['retrievedAt'] as string, metadata['retrieved_at'] as string, null),\n compactionApplied: pick(\n metadata['compactionApplied'] as boolean,\n metadata['compaction_applied'] as boolean,\n null,\n ),\n };\n}\n\nexport function normalizeConversationContext(\n value: RawConversationContext | null | undefined,\n): ConversationContext | null {\n if (!value) return null;\n return {\n summary: value.summary || null,\n currentState: pick(value['currentState'] as Json, value.current_state, {}),\n keyExtractions: pick(value['keyExtractions'] as Json, value.key_extractions, {}),\n recentTurns: pick(value['recentTurns'] as unknown[], value.recent_turns, []),\n compactionId: pick(value['compactionId'] as string, value.compaction_id, null),\n compactedAt: pick(value['compactedAt'] as string, value.compacted_at, null),\n conversationId: pick(value['conversationId'] as string, value.conversation_id, null),\n };\n}\n\nfunction normalizeRetrievalHealth(raw: unknown): RetrievalHealth | null {\n if (!raw || typeof raw !== 'object') return null;\n const r = raw as Json;\n const warnings = r['warnings'];\n return {\n degraded: r['degraded'] === undefined ? true : Boolean(r['degraded']),\n level: (r['level'] as string) ?? null,\n code: (r['code'] as string) ?? null,\n warnings: Array.isArray(warnings) ? warnings.map((w) => String(w)) : [],\n };\n}\n\n/** Accepts either a bare context or a `{ context: ... }` envelope, as the wrapper did. */\nexport function normalizeContextResponse(result: Json = {}): NormalisedContext {\n const context = (result['context'] as RawContext) ?? (result as RawContext);\n return {\n facts: (context.facts || []).map((i) => normalizeFact(i)),\n preferences: (context.preferences || []).map((i) => normalizePreference(i)),\n episodes: (context.episodes || []).map((i) => normalizeEpisode(i)),\n emotions: (context.emotions || []).map((i) => normalizeEmotion(i)),\n temporalEvents: (\n pick(context['temporalEvents'] as RawContextItem[], context.temporal_events, []) || []\n ).map((i) => normalizeTemporalEvent(i)),\n conversationContext: normalizeConversationContext(\n pick(\n context['conversationContext'] as RawConversationContext,\n context.conversation_context,\n null,\n ),\n ),\n metadata: normalizeContextMetadata(obj(context.metadata)),\n // `retrieval` is a SIBLING of `context` in the body, so it is read off the\n // envelope first and only then off a bare context. Reading it from\n // `context` alone is how the Python SDK dropped it.\n retrieval: normalizeRetrievalHealth(\n pick((result as Json)['retrieval'] as Json, (context as Json)['retrieval'] as Json, null),\n ),\n rawResponse: pick(context['rawResponse'] as Json, context['raw_response'] as Json, {}),\n // Only for a ranked fetch, so a by-type result has exactly the keys it had.\n ...(Array.isArray(context.items)\n ? { items: context.items, excerpts: Array.isArray(context.excerpts) ? context.excerpts : [] }\n : {}),\n };\n}\n","/**\n * Flatten typed context collections into one memory list.\n *\n * A faithful port of `flatten_context_items` from the 0.3.x bridge, which is\n * what `searchMemory` and `getMemories` returned. The per-type asymmetries are\n * deliberate and are reproduced exactly:\n *\n * - `memory` comes from a different field per type: fact/preference/temporal\n * event use `content`, episode uses `summary`, emotion uses `context`.\n * - `score` likewise: fact -> confidence, preference -> strength,\n * episode -> significance, emotion -> intensity, temporal event ->\n * temporal_confidence.\n * - **episode and emotion carry no `source` key at all.** The bridge simply\n * did not set one. Adding it would be an improvement and a shape change, so\n * it stays absent.\n * - Order is facts, preferences, episodes, emotions, temporal events.\n */\n\nimport type { Json, RawContext, RawContextItem } from './types.js';\n\nexport interface FlatMemory {\n id: string;\n memory: string;\n score: number;\n /** Absent for episodes and emotions, matching the 0.3.x bridge. */\n source?: string;\n metadata: Json;\n context_type: 'fact' | 'preference' | 'episode' | 'emotion' | 'temporal_event';\n event_date: string | null;\n /** See `TemporalFields.saidAt` in ./types. */\n said_at: string | null;\n valid_until: string | null;\n temporal_category: string | null;\n temporal_confidence: number;\n}\n\nconst s = (v: unknown): string => (typeof v === 'string' ? v : v == null ? '' : String(v));\nconst n = (v: unknown, d = 0): number => (typeof v === 'number' ? v : d);\nconst o = (v: unknown): Json => (v as Json) ?? {};\n/** The bridge stringified datetimes and mapped falsy to null. */\nconst dt = (v: unknown): string | null => (v ? String(v) : null);\n\nfunction temporal(item: RawContextItem) {\n return {\n event_date: dt(item['event_date']),\n said_at: dt(item['said_at']),\n valid_until: dt(item['valid_until']),\n temporal_category: (item['temporal_category'] as string | null) ?? null,\n temporal_confidence: n(item['temporal_confidence'], 0),\n };\n}\n\nexport function flattenContextItems(context: RawContext): FlatMemory[] {\n const items: FlatMemory[] = [];\n\n for (const f of context.facts ?? []) {\n items.push({\n id: s(f['id']), memory: s(f['content']), score: n(f['confidence']),\n source: s(f['source']), metadata: o(f['metadata']),\n context_type: 'fact', ...temporal(f),\n });\n }\n for (const p of context.preferences ?? []) {\n items.push({\n id: s(p['id']), memory: s(p['content']), score: n(p['strength']),\n source: s(p['source']), metadata: o(p['metadata']),\n context_type: 'preference', ...temporal(p),\n });\n }\n for (const e of context.episodes ?? []) {\n // No `source`: the bridge did not set one for episodes.\n items.push({\n id: s(e['id']), memory: s(e['summary']), score: n(e['significance']),\n metadata: o(e['metadata']), context_type: 'episode', ...temporal(e),\n });\n }\n for (const m of context.emotions ?? []) {\n // No `source` here either, and `memory` comes from `context`, not `content`.\n items.push({\n id: s(m['id']), memory: s(m['context']), score: n(m['intensity']),\n metadata: o(m['metadata']), context_type: 'emotion', ...temporal(m),\n });\n }\n for (const t of context.temporal_events ?? []) {\n items.push({\n id: s(t['id']), memory: s(t['content']), score: n(t['temporal_confidence']),\n source: s(t['source']), metadata: o(t['metadata']),\n context_type: 'temporal_event', ...temporal(t),\n });\n }\n\n return items;\n}\n","/**\n * Memories namespace. Mirrors `maximem_synap.memories.interface.MemoriesInterface`.\n *\n * ## The request body is built the way Python builds it, on purpose\n *\n * Python constructs a pydantic `CreateMemoryRequest` and posts\n * `model_dump(mode=\"json\")`, which emits **every** field, nulls included, with\n * defaults applied. It is tempting to omit unset keys instead and send a\n * tidier body. Do not: the server sees a different request, and the difference\n * that matters most is `document_type`.\n *\n * `document_type` defaults to `\"ai-chat-conversation\"`. Leaving it off is not\n * neutral, because the extraction path is type-dependent and the\n * `\"document\"` type extracts dramatically less from the same text. A body that\n * merely looks cleaner is how you get \"the JS SDK stores fewer memories than\n * the Python one from identical input\", which is slow and miserable to\n * diagnose.\n */\n\nimport { InvalidInputError } from '../errors.js';\nimport type { HttpTransport } from '../transport/http.js';\nimport type { Json } from '../context/types.js';\n\n/**\n * What `new Blob([...])` accepts here.\n *\n * Derived from the Blob constructor rather than written out, because `BlobPart`\n * lives in the DOM lib, which this package deliberately does not include, and\n * Node's own Blob accepts a slightly narrower set than the DOM's. Deriving it\n * means this keeps compiling if either definition shifts.\n */\ntype BlobSource = NonNullable<ConstructorParameters<typeof Blob>[0]>[number];\n\n/** Mirrors the Python DocumentType enum. */\nexport const DOCUMENT_TYPES = [\n 'ai-chat-conversation', 'document', 'email', 'pdf', 'image', 'audio', 'meeting-transcript',\n] as const;\nexport type DocumentType = (typeof DOCUMENT_TYPES)[number];\n\n/** Mirrors the Python IngestMode enum. */\nexport const INGEST_MODES = ['fast', 'long-range'] as const;\nexport type IngestMode = (typeof INGEST_MODES)[number];\n\nexport const DEFAULT_DOCUMENT_TYPE: DocumentType = 'ai-chat-conversation';\nexport const DEFAULT_INGEST_MODE: IngestMode = 'long-range';\n\nexport interface CreateMemoryOptions {\n document: string;\n user_id?: string | null;\n userId?: string | null;\n customer_id?: string | null;\n customerId?: string | null;\n document_type?: DocumentType | string;\n documentType?: DocumentType | string;\n document_id?: string | null;\n documentId?: string | null;\n /** ISO-8601 string or Date. Serialised the way Python serialises a datetime. */\n document_created_at?: string | Date | null;\n documentCreatedAt?: string | Date | null;\n mode?: IngestMode | string;\n metadata?: Json;\n /**\n * The level this memory belongs to, named in full, one entry per level down\n * to it: `{ customer: \"acme\", team: \"payments\", user: \"dana\" }`.\n *\n * Only needed by an account whose scope ladder has a level between its\n * customer and its user levels. `user_id` and `customer_id` cannot say which\n * one a write belongs to, so on such a ladder the server refuses the write\n * rather than guessing: a wrong guess at or above the customer level crosses\n * a tenant boundary.\n *\n * Omit it and nothing changes. Sending one to an account without nested\n * scoping is an error rather than ignored, because a caller who asks to be\n * narrowed and is ignored gets widened instead.\n */\n scope?: Record<string, string> | null;\n}\n\n/**\n * Mirrors Python's `CreateMemoryResponse`.\n *\n * The first four fields are required there (Pydantic would raise if the server\n * omitted one), so they are required here too. Declaring `ingestion_id` optional\n * made the commonest two-line pattern in the docs fail to compile under strict\n * TypeScript: `wait_for_completion(result.ingestion_id)` cannot take\n * `string | undefined`.\n */\nexport interface CreateMemoryResult {\n ingestion_id: string;\n document_id: string;\n status: string;\n /** ISO-8601. Python types this as a datetime. */\n queued_at: string;\n error_message?: string | null;\n memories_created?: number;\n [key: string]: unknown;\n}\n\nfunction either<T>(o: Record<string, unknown>, snake: string, camel: string): T | undefined {\n const a = o[snake];\n if (a !== undefined) return a as T;\n const b = o[camel];\n return b === undefined ? undefined : (b as T);\n}\n\nfunction isoOrNull(v: string | Date | null | undefined): string | null {\n if (v === undefined || v === null) return null;\n return v instanceof Date ? v.toISOString() : v;\n}\n\n/** Byte-for-byte equivalent of Python's `CreateMemoryRequest.model_dump(mode=\"json\")`. */\nexport function buildCreateBody(options: CreateMemoryOptions): Json {\n if (!options.document) throw new InvalidInputError('document is required');\n\n const o = options as unknown as Record<string, unknown>;\n const documentType = either<string>(o, 'document_type', 'documentType') ?? DEFAULT_DOCUMENT_TYPE;\n const mode = (options.mode as string) ?? DEFAULT_INGEST_MODE;\n\n // Validated client-side because Python's enum coercion raises before any\n // request is sent. A silent pass-through would turn a typo into a server\n // error, or worse, a silently different extraction path.\n if (!(DOCUMENT_TYPES as readonly string[]).includes(documentType)) {\n throw new InvalidInputError(\n `Invalid document_type '${documentType}'. Expected one of: ${DOCUMENT_TYPES.join(', ')}`,\n );\n }\n if (!(INGEST_MODES as readonly string[]).includes(mode)) {\n throw new InvalidInputError(\n `Invalid mode '${mode}'. Expected one of: ${INGEST_MODES.join(', ')}`,\n );\n }\n\n // Key order matches the pydantic model's field order. Not semantically\n // required, but it makes a captured-body diff against Python readable.\n return {\n document: options.document,\n document_type: documentType,\n document_id: either<string>(o, 'document_id', 'documentId') ?? null,\n document_created_at: isoOrNull(\n either<string | Date>(o, 'document_created_at', 'documentCreatedAt'),\n ),\n user_id: either<string>(o, 'user_id', 'userId') ?? null,\n customer_id: either<string>(o, 'customer_id', 'customerId') ?? null,\n // null, never `{}`. An empty path is not absence: the server treats one as\n // an error, because a caller asking to be narrowed and being ignored gets\n // widened instead.\n scope: options.scope ?? null,\n mode,\n metadata: options.metadata ?? {},\n };\n}\n\n/**\n * Result of `memories.batch_create`, mirroring Python's `BatchCreateResponse`.\n * It was `Json`, so `results` came back as `unknown` and could not be iterated.\n */\nexport interface BatchCreateResult {\n batch_id: string;\n total: number;\n succeeded: number;\n failed: number;\n results: CreateMemoryResult[];\n [key: string]: unknown;\n}\n\nexport interface BatchCreateOptions {\n documents: CreateMemoryOptions[];\n fail_fast?: boolean;\n failFast?: boolean;\n}\n\n/** Mirrors the Python ForgetMode enum. */\nexport const FORGET_MODES = ['preview', 'direct'] as const;\nexport type ForgetMode = (typeof FORGET_MODES)[number];\n\nexport const FORGET_DESCRIPTION_MIN = 3;\nexport const FORGET_DESCRIPTION_MAX = 500;\n\nexport interface ForgetOptions {\n /** What to forget, in the user's own words (3 to 500 characters). */\n description: string;\n user_id?: string | null;\n userId?: string | null;\n customer_id?: string | null;\n customerId?: string | null;\n /**\n * The level to search, named in full, one entry per level down to it:\n * `{ customer: \"acme\", team: \"payments\", user: \"dana\" }`. Only needed when\n * your scope ladder has a level between customer and user.\n */\n scope?: Record<string, string> | null;\n /** `\"preview\"` (default) or `\"direct\"`. See `MemoriesInterface.forget`. */\n mode?: ForgetMode | string;\n}\n\n/** A memory the description matched. Mirrors Python's `ForgetCandidate`. */\nexport interface ForgetCandidate {\n memory_id: string;\n content: string;\n memory_type?: string | null;\n /** `\"memory\"`, or where a copy lives: chunk, sentence, entity, triple, profile, candidate. */\n store?: string;\n}\n\nexport type ForgetStatus = 'nothing_matches' | 'preview' | 'deleted' | 'partial';\n\n/** Result of `memories.forget`. Mirrors Python's `ForgetResponse`. */\nexport interface ForgetResult {\n status: ForgetStatus;\n request_id: string | null;\n candidates: ForgetCandidate[];\n deleted_ids: string[];\n failed_ids: string[];\n still_present_in: string[];\n may_be_incomplete: boolean;\n}\n\n/** The body Python builds for `memories.forget`, key order included. */\nexport function buildForgetBody(options: ForgetOptions): Json {\n const description = options.description;\n if (\n typeof description !== 'string' ||\n description.length < FORGET_DESCRIPTION_MIN ||\n description.length > FORGET_DESCRIPTION_MAX\n ) {\n throw new InvalidInputError(\n `description must be between ${FORGET_DESCRIPTION_MIN} and ${FORGET_DESCRIPTION_MAX} characters`,\n );\n }\n const mode = (options.mode as string | undefined) ?? 'preview';\n if (!(FORGET_MODES as readonly string[]).includes(mode)) {\n throw new InvalidInputError(\n `Invalid mode '${mode}'. Expected one of: ${FORGET_MODES.join(', ')}`,\n );\n }\n const o = options as unknown as Record<string, unknown>;\n return {\n description,\n user_id: either<string>(o, 'user_id', 'userId') ?? null,\n customer_id: either<string>(o, 'customer_id', 'customerId') ?? null,\n scope: options.scope ?? null,\n mode,\n };\n}\n\nexport interface MemoriesInterface {\n create(options: CreateMemoryOptions): Promise<CreateMemoryResult>;\n batch_create(options: BatchCreateOptions): Promise<BatchCreateResult>;\n create_from_file(options: CreateFromFileOptions): Promise<CreateMemoryResult>;\n get(memoryId: string): Promise<Json>;\n update(options: UpdateMemoryOptions): Promise<Json>;\n delete(memoryId: string): Promise<Json>;\n /**\n * Forget memories by describing them in the user's own words. Use it when\n * you do not hold memory ids; to delete one memory by id, use `delete()`.\n *\n * The call must be scoped to exactly one user: pass `user_id`, plus\n * `customer_id` on a B2B instance. Otherwise the server answers 400, raised\n * as `InvalidInputError`.\n *\n * Modes:\n * - `\"preview\"` (default): files a pending request that a person approves in\n * the dashboard under Deletion requests. Deletes nothing. `candidates`\n * lists what matched.\n * - `\"direct\"`: finds and soft-deletes in one call. Not idempotent as a\n * request: a repeat is harmless and finds nothing new, but it is a new\n * request.\n *\n * `status` is `\"nothing_matches\"`, `\"preview\"`, `\"deleted\"` or `\"partial\"`.\n * `\"partial\"` means the memories are hidden but a store named in\n * `still_present_in` may still hold the text, so do not tell an end user it\n * was removed unless `status === \"deleted\"`. `failed_ids` lists memories that\n * could not be deleted; `may_be_incomplete` is true when the match may not\n * have covered everything.\n *\n * Errors: `InvalidInputError` for 400/422 or a bad `description`/`mode`\n * caught before sending; `ServiceUnavailableError` for 503 (the matcher was\n * unavailable and nothing was deleted, so trying again is safe; the\n * transport applies its normal retry policy first); `RateLimitError` for 429.\n */\n forget(options: ForgetOptions): Promise<ForgetResult>;\n status(ingestionId: string): Promise<Json>;\n wait_for_completion(ingestionId: string, options?: WaitOptions): Promise<Json>;\n}\n\n/**\n * Options for `create_from_file`.\n *\n * Exactly one of `file_path`, `file`, or `text` must be supplied, matching\n * Python. `file_path` reads from disk and so is Node-only; `file` takes a\n * Blob/File/Uint8Array and works anywhere, which is the portable route for\n * Edge and Workers.\n */\nexport interface CreateFromFileOptions {\n user_id?: string;\n userId?: string;\n customer_id?: string;\n customerId?: string;\n /** \"b2b\" or \"b2c\". Defaults to \"b2c\", as in Python. */\n relationship_type?: string;\n relationshipType?: string;\n /** Node-only: read this path from disk. Uses a lazy `node:fs` import. */\n file_path?: string;\n filePath?: string;\n /** Portable alternative to `file_path`. Pass `filename` alongside a raw buffer. */\n file?: Blob | Uint8Array | ArrayBuffer;\n filename?: string;\n /** Ingest raw text instead of a file. */\n text?: string;\n document_type?: string;\n documentType?: string;\n mode?: IngestMode | string;\n metadata?: Json;\n /**\n * The level this memory belongs to, named in full, one entry per level down\n * to it: `{ customer: \"acme\", team: \"payments\", user: \"dana\" }`.\n *\n * Only needed by an account whose scope ladder has a level between its\n * customer and its user levels. `user_id` and `customer_id` cannot say which\n * one a write belongs to, so on such a ladder the server refuses the write\n * rather than guessing: a wrong guess at or above the customer level crosses\n * a tenant boundary.\n *\n * Omit it and nothing changes. Sending one to an account without nested\n * scoping is an error rather than ignored, because a caller who asks to be\n * narrowed and is ignored gets widened instead.\n */\n scope?: Record<string, string> | null;\n}\n\nexport interface UpdateMemoryOptions {\n memory_id?: string;\n memoryId?: string;\n document: string;\n merge_strategy?: string;\n mergeStrategy?: string;\n document_type?: string;\n documentType?: string;\n metadata?: Json;\n}\n\nexport interface WaitOptions {\n /** Seconds. Matches Python's default. */\n timeout_seconds?: number;\n timeoutSeconds?: number;\n poll_interval_seconds?: number;\n pollIntervalSeconds?: number;\n signal?: AbortSignal;\n}\n\nconst TERMINAL_STATUSES = new Set(['completed', 'failed', 'error', 'cancelled']);\n\nexport function createMemoriesInterface(\n transport: HttpTransport,\n // Called after a forget that deleted something, so the caller can stop serving\n // that user's cached context. Mirrors Python's `_evict_forgotten`.\n onForgotten?: (ids: { userId: string | null; customerId: string | null }) => void,\n): MemoriesInterface {\n return {\n async create(options) {\n const o = options as unknown as Record<string, unknown>;\n transport.checkCustomerId(o['customer_id'] ?? o['customerId'], 'memories.create');\n return transport.request<CreateMemoryResult>('memories_create', { body: buildCreateBody(options) });\n },\n\n async batch_create(options) {\n const docs = options.documents;\n if (!Array.isArray(docs) || docs.length === 0) {\n throw new InvalidInputError('documents must be a non-empty array');\n }\n const failFast = options.fail_fast ?? options.failFast ?? false;\n return transport.request<BatchCreateResult>('memories_batch', {\n body: { documents: docs.map(buildCreateBody), fail_fast: failFast },\n });\n },\n\n async create_from_file(options) {\n const userId = options.user_id ?? options.userId;\n const customerId = options.customer_id ?? options.customerId;\n if (!userId) throw new InvalidInputError('user_id is required');\n // Not required: rejected on B2C, so requiring it made the documented\n // default combination impossible to call.\n transport.checkCustomerId(customerId, 'memories.create_from_file');\n\n const filePath = options.file_path ?? options.filePath;\n const sources = [filePath, options.file, options.text].filter((v) => v !== undefined);\n if (sources.length === 0) {\n // Python raises ValueError here. There is no JS equivalent, and every\n // other validation in this SDK raises InvalidInputError, so the\n // taxonomy wins over a literal transcription of the exception type.\n throw new InvalidInputError('One of file_path, file, or text must be provided.');\n }\n if (sources.length > 1) {\n throw new InvalidInputError(\n 'Provide exactly one of file_path, file, or text, not several.',\n );\n }\n\n const form = new FormData();\n form.set('user_id', userId);\n form.set('customer_id', customerId);\n form.set('relationship_type', options.relationship_type ?? options.relationshipType ?? 'b2c');\n form.set('mode', String(options.mode ?? DEFAULT_INGEST_MODE));\n\n // Python appends these only when truthy, so an empty document_type or an\n // empty metadata object is absent from the form rather than present-empty.\n const documentType = options.document_type ?? options.documentType;\n if (documentType) form.set('document_type', String(documentType));\n if (options.metadata !== undefined && Object.keys(options.metadata).length > 0) {\n form.set('metadata', JSON.stringify(options.metadata));\n }\n\n if (filePath !== undefined) {\n // Lazy, so importing the SDK on Edge or in a Worker stays safe: a\n // top-level `node:fs` import fails the BUILD there, not the call.\n const { readFile } = await import('node:fs/promises');\n const { basename } = await import('node:path');\n const bytes = await readFile(filePath);\n form.set('file', new Blob([bytes as unknown as BlobSource]), basename(filePath));\n } else if (options.file !== undefined) {\n const blob = options.file instanceof Blob\n ? options.file\n : new Blob([options.file as unknown as BlobSource]);\n // Python defaults the name to \"upload\" when `filename` is omitted.\n form.set('file', blob, options.filename ?? 'upload');\n } else {\n form.set('text', String(options.text));\n }\n\n return transport.request<CreateMemoryResult>('memories_upload', {\n body: form,\n rawBody: true,\n });\n },\n\n async get(memoryId) {\n if (!memoryId) throw new InvalidInputError('memory_id is required');\n return transport.request<Json>('memories_get', { pathParams: { memory_id: memoryId } });\n },\n\n async update(options) {\n const memoryId = options.memory_id ?? options.memoryId;\n if (!memoryId) throw new InvalidInputError('memory_id is required');\n if (!options.document) throw new InvalidInputError('document is required');\n const body: Json = {\n document: options.document,\n merge_strategy: options.merge_strategy ?? options.mergeStrategy ?? 'smart-merge',\n };\n const dt = options.document_type ?? options.documentType;\n if (dt !== undefined) body['document_type'] = dt;\n if (options.metadata !== undefined) body['metadata'] = options.metadata;\n // PUT, so idempotent by verb: safe to retry even on an ambiguous failure.\n return transport.request<Json>('memories_update', {\n pathParams: { memory_id: memoryId },\n body,\n });\n },\n\n async delete(memoryId) {\n // The 0.3.x wrapper let you delete \"all of this user's memories\" from a\n // process-local id list, which deleted nothing in serverless while\n // reporting success. Deliberately not ported.\n if (!memoryId) {\n throw new InvalidInputError(\n 'memory_id is required. Deleting by user is no longer supported: the ' +\n 'previous implementation tracked ids in process memory, so it silently ' +\n 'deleted nothing in any serverless or multi-process deployment while ' +\n 'reporting success.',\n );\n }\n return transport.request<Json>('memories_delete', { pathParams: { memory_id: memoryId } });\n },\n\n async forget(options) {\n const o = options as unknown as Record<string, unknown>;\n transport.checkCustomerId(o['customer_id'] ?? o['customerId'], 'memories.forget');\n const body = buildForgetBody(options) as Record<string, unknown>;\n const result = await transport.request<ForgetResult>('memories_forget', { body: body as Json });\n // A preview deletes nothing, so it evicts nothing. Never let a cache\n // problem turn a successful delete into an error.\n // A forget can remove only copies (a profile entry, a chunk) and so return no\n // memory ids; those are served from the same caches.\n const removed = !!result?.deleted_ids?.length\n || result?.status === 'deleted' || result?.status === 'partial';\n if (removed && onForgotten) {\n try {\n onForgotten({\n userId: (body['user_id'] as string | null) ?? null,\n customerId: (body['customer_id'] as string | null) ?? null,\n });\n } catch { /* eviction is best effort */ }\n }\n return result;\n },\n\n async status(ingestionId) {\n if (!ingestionId) throw new InvalidInputError('ingestion_id is required');\n return transport.request<Json>('memories_status', {\n pathParams: { ingestion_id: ingestionId },\n });\n },\n\n async wait_for_completion(ingestionId, options = {}) {\n if (!ingestionId) throw new InvalidInputError('ingestion_id is required');\n const timeoutMs =\n (options.timeout_seconds ?? options.timeoutSeconds ?? 300) * 1000;\n const pollMs =\n (options.poll_interval_seconds ?? options.pollIntervalSeconds ?? 2) * 1000;\n\n const deadline = Date.now() + timeoutMs;\n let last: Json = {};\n for (;;) {\n last = await this.status(ingestionId);\n const status = String(last['status'] ?? '').toLowerCase();\n if (TERMINAL_STATUSES.has(status)) return last;\n if (Date.now() >= deadline) {\n // Python returns the last status rather than raising, so a caller\n // that only checks `.status` behaves identically in both SDKs.\n return last;\n }\n await sleep(Math.min(pollMs, Math.max(0, deadline - Date.now())), options.signal);\n }\n },\n };\n}\n\nfunction sleep(ms: number, signal?: AbortSignal): Promise<void> {\n if (ms <= 0) return Promise.resolve();\n return new Promise((resolve, reject) => {\n const timer = setTimeout(() => {\n signal?.removeEventListener('abort', onAbort);\n resolve();\n }, ms);\n // NOT unref'd. This is the poll interval inside wait_for_completion, which\n // a caller is awaiting: foreground work. Unref'ing it let a process whose\n // only pending work was this sleep exit mid-poll, so an awaited\n // wait_for_completion never settled. Node calls it \"Detected unsettled\n // top-level await\". The unref'd timers in this SDK are the two heartbeats\n // and the request watchdog, all of which are genuinely background.\n function onAbort() {\n clearTimeout(timer);\n reject(signal?.reason ?? new Error('Aborted'));\n }\n signal?.addEventListener('abort', onAbort, { once: true });\n });\n}\n","/**\n * Client-side identifier validation, mirroring Python's\n * ``maximem_synap/utils/validators.py``.\n *\n * These raise the *specific* ``InvalidInputError`` subtypes so callers can\n * catch them directly (``catch (e) { if (e instanceof InvalidConversationIdError) }``)\n * instead of the generic parent. Validation happens before any network call,\n * so a malformed id fails locally rather than becoming a billed round trip.\n *\n * Both errors were exported from day one and thrown at zero sites until this\n * module existed, which meant a typo'd conversation id reached the server and\n * came back as a generic 4xx.\n */\n\nimport { InvalidConversationIdError, InvalidInstanceIdError } from '../errors.js';\n\n// Instance ids are issued as `inst_` followed by 16 hex characters.\nconst INSTANCE_ID_RE = /^inst_[0-9a-fA-F]{16}$/;\n\n// Accepts any RFC 4122 shape. Deliberately laxer than a version-specific\n// check: Python defers to `uuid.UUID(...)`, which likewise accepts any\n// variant, and rejecting a valid-but-unexpected version here would diverge.\nconst UUID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;\n\n/**\n * Throw `InvalidConversationIdError` if a non-empty `conversationId` is not a\n * UUID string.\n *\n * Empty / null / undefined values pass through untouched: callers that require\n * a conversation id surface that separately, so this only rejects a clearly\n * malformed value such as `\"conv_123\"`.\n */\nexport function validateConversationId(conversationId: unknown): void {\n if (conversationId === undefined || conversationId === null || conversationId === '') return;\n if (typeof conversationId !== 'string' || !UUID_RE.test(conversationId)) {\n // Message text matches Python's `f\"Invalid conversation ID: {id}\"` byte for byte.\n throw new InvalidConversationIdError(`Invalid conversation ID: ${String(conversationId)}`);\n }\n}\n\n/**\n * Throw `InvalidInstanceIdError` if a non-empty `instanceId` is not in the\n * `inst_<hex16>` format.\n *\n * Empty values pass through: the instance id is normally resolved from the API\n * key during `initialize()`, so only an explicitly provided malformed id is\n * rejected.\n */\nexport function validateInstanceId(instanceId: unknown): void {\n if (instanceId === undefined || instanceId === null || instanceId === '') return;\n if (typeof instanceId !== 'string' || !INSTANCE_ID_RE.test(instanceId)) {\n throw new InvalidInstanceIdError(`Invalid instance ID: ${String(instanceId)}`);\n }\n}\n","/**\n * Which model the agent runs on, sent with each conversation turn.\n *\n * The server sizes compaction to the agent's context window: it compacts at\n * about 11.7% of the window (15K tokens on a 128K model) and caps the\n * compacted record at half that. It reads the window from the latest turn's\n * metadata, under `agent_model` and `agent_context_window`. A model name is\n * enough: the server has a model -> window table. Give `context_window` only\n * for a model the table would not know, or to override it.\n *\n * Precedence, highest first: the value passed on the call, the client option\n * (`agent_model` / `agent_context_window`), a value already in the caller's\n * own metadata. With none of them the server assumes 128K. Mirrors Python's\n * `agent_model.py`.\n */\nexport const AGENT_MODEL_KEY = 'agent_model';\nexport const AGENT_CONTEXT_WINDOW_KEY = 'agent_context_window';\n\nexport interface AgentModel {\n model?: string | undefined;\n contextWindow?: number | undefined;\n}\n\n/** The per-call `model` / `context_window` (either spelling) off an options object. */\nexport function agentModelOf(options: unknown): AgentModel {\n const o = (options ?? {}) as Record<string, unknown>;\n const window = o['context_window'] ?? o['contextWindow'];\n return {\n model: typeof o['model'] === 'string' ? (o['model'] as string) : undefined,\n contextWindow: typeof window === 'number' ? window : undefined,\n };\n}\n\n/**\n * `metadata` with the agent's model folded in. `asStrings` for the gRPC\n * stream, whose metadata is a string map. Returns the input unchanged when\n * there is nothing to add.\n */\nexport function withAgentModel<T extends Record<string, unknown>>(\n metadata: T | undefined,\n perCall: AgentModel,\n defaults: AgentModel,\n asStrings = false,\n): T | undefined {\n const model = perCall.model || defaults.model;\n const window = perCall.contextWindow || defaults.contextWindow;\n if (!model && !window) return metadata;\n const out: Record<string, unknown> = { ...(metadata ?? {}) };\n if (model) out[AGENT_MODEL_KEY] = String(model);\n if (window) out[AGENT_CONTEXT_WINDOW_KEY] = asStrings ? String(Math.trunc(window)) : Math.trunc(window);\n return out as T;\n}\n","/**\n * Conversation namespace. Mirrors `ConversationInterface` and\n * `ConversationContextInterface` in the Python SDK.\n */\n\nimport { InvalidInputError } from '../errors.js';\nimport { validateConversationId } from '../util/validators.js';\nimport type { HttpTransport } from '../transport/http.js';\nimport { fetchContext } from '../context/fetch.js';\nimport { agentModelOf, withAgentModel, type AgentModel } from '../agent-model.js';\nimport type { FetchOptions, Json, RawContext } from '../context/types.js';\n\nexport interface RecordMessageOptions {\n conversation_id?: string;\n conversationId?: string;\n role: string;\n content: string;\n user_id?: string;\n userId?: string;\n customer_id?: string;\n customerId?: string;\n session_id?: string;\n sessionId?: string;\n metadata?: Json;\n /**\n * The rung this conversation belongs to, named in full, e.g.\n * `{ tenant: 'acme', practice: 'high_street' }`.\n *\n * Only relevant to a client whose ladder is deeper than the default three\n * rungs (root, tenant, person). On the default ladder `customer_id` and\n * `user_id` already say everything a path could.\n *\n * Sent as `scope` on the wire, matching Python's `scope_path`. Sending it\n * once per conversation is enough: the server fills a missing path and never\n * overwrites one.\n */\n scope_path?: Record<string, string>;\n scopePath?: Record<string, string>;\n /**\n * The model the agent runs on, e.g. `'gpt-4o'`. Overrides the client's\n * `agent_model` for this turn. The server sizes compaction to this model's\n * context window. Python's `model=`.\n */\n model?: string;\n /** The agent model's context window in tokens, for a model the server would not know. */\n context_window?: number;\n contextWindow?: number;\n}\n\nexport interface TranscriptTurn {\n role: string;\n content: string;\n [key: string]: unknown;\n}\n\nexport interface IngestTranscriptOptions {\n conversation_id?: string;\n conversationId?: string;\n user_id?: string;\n userId?: string;\n transcript: string | TranscriptTurn[];\n customer_id?: string;\n customerId?: string;\n conversation_type?: string;\n conversationType?: string;\n analysis?: Json;\n metadata?: Json;\n started_at?: string | Date;\n startedAt?: string | Date;\n ended_at?: string | Date;\n endedAt?: string | Date;\n /**\n * The rung this conversation belongs to, named in full, e.g.\n * `{ tenant: 'acme', practice: 'high_street' }`.\n *\n * Only relevant to a client whose ladder is deeper than the default three\n * rungs (root, tenant, person). On the default ladder `customer_id` and\n * `user_id` already say everything a path could.\n *\n * Sent as `scope` on the wire, matching Python's `scope_path`. Sending it\n * once per conversation is enough: the server fills a missing path and never\n * overwrites one.\n */\n scope_path?: Record<string, string>;\n scopePath?: Record<string, string>;\n}\n\n/**\n * Compaction levels, mirroring Python's `CompactionLevel` enum. The JS SDK\n * exported no equivalent, so `compaction_level` was an untyped `string` and a\n * typo reached the server instead of the compiler.\n */\nexport const COMPACTION_LEVELS = [\n 'low', 'medium', 'high', 'adaptive', 'aggressive', 'balanced', 'conservative',\n] as const;\n\nexport type CompactionLevel = (typeof COMPACTION_LEVELS)[number];\n\nexport interface CompactOptions {\n conversation_id?: string;\n conversationId?: string;\n strategy?: string;\n compaction_level?: CompactionLevel | string;\n compactionLevel?: CompactionLevel | string;\n target_tokens?: number;\n targetTokens?: number;\n force?: boolean;\n}\n\nfunction either<T>(o: Record<string, unknown>, snake: string, camel: string): T | undefined {\n const a = o[snake];\n if (a !== undefined) return a as T;\n const b = o[camel];\n return b === undefined ? undefined : (b as T);\n}\n\nfunction iso(v: string | Date | undefined): string | undefined {\n if (v === undefined) return undefined;\n return v instanceof Date ? v.toISOString() : v;\n}\n\nexport function buildRecordMessageBody(options: RecordMessageOptions): Json {\n const o = options as unknown as Record<string, unknown>;\n const conversationId = either<string>(o, 'conversation_id', 'conversationId');\n const userId = either<string>(o, 'user_id', 'userId');\n const customerId = either<string>(o, 'customer_id', 'customerId');\n\n if (!conversationId) throw new InvalidInputError('conversation_id is required');\n // Python validates the format here (and in record_messages_batch, which\n // reuses this builder per message). Note it deliberately does NOT validate\n // in ingest_transcript.\n validateConversationId(conversationId);\n if (!options.role) throw new InvalidInputError('role is required');\n if (options.content === undefined || options.content === null) {\n throw new InvalidInputError('content is required');\n }\n if (!userId) throw new InvalidInputError('user_id is required');\n // customer_id is NOT required. A B2C instance rejects one, so demanding it\n // here made the only correct B2C shape impossible to express.\n // checked by the caller, which holds the transport; see createConversationNamespace\n\n const body: Json = {\n conversation_id: conversationId,\n role: options.role,\n content: options.content,\n user_id: userId,\n customer_id: customerId,\n // Python always sends this, defaulting to {}.\n metadata: options.metadata ?? {},\n };\n const sessionId = either<string>(o, 'session_id', 'sessionId');\n if (sessionId !== undefined) body['session_id'] = sessionId;\n // `scope` on the wire, `scope_path` in the SDK, the same split Python makes.\n // Omitted when unset so a client on the default ladder sends exactly the\n // body it sends today.\n const scopePath = either<Record<string, string>>(o, 'scope_path', 'scopePath');\n if (scopePath !== undefined) body['scope'] = scopePath;\n return body;\n}\n\nexport function buildIngestTranscriptBody(options: IngestTranscriptOptions): Json {\n const o = options as unknown as Record<string, unknown>;\n const conversationId = either<string>(o, 'conversation_id', 'conversationId');\n const userId = either<string>(o, 'user_id', 'userId');\n if (!conversationId) throw new InvalidInputError('conversation_id is required');\n if (!userId) throw new InvalidInputError('user_id is required');\n if (options.transcript === undefined || options.transcript === null) {\n throw new InvalidInputError('transcript is required');\n }\n\n const body: Json = {\n conversation_id: conversationId,\n user_id: userId,\n transcript: options.transcript,\n };\n // Unset optionals are omitted so the server applies its own defaults, which\n // is what Python does here (and notably NOT what it does for memories.create).\n const customerId = either<string>(o, 'customer_id', 'customerId');\n const conversationType = either<string>(o, 'conversation_type', 'conversationType');\n const startedAt = iso(either<string | Date>(o, 'started_at', 'startedAt'));\n const endedAt = iso(either<string | Date>(o, 'ended_at', 'endedAt'));\n if (customerId !== undefined) body['customer_id'] = customerId;\n if (conversationType !== undefined) body['conversation_type'] = conversationType;\n if (options.analysis !== undefined) body['analysis'] = options.analysis;\n if (options.metadata !== undefined) body['metadata'] = options.metadata;\n if (startedAt !== undefined) body['started_at'] = startedAt;\n if (endedAt !== undefined) body['ended_at'] = endedAt;\n // Python 0.4.10 sends this on `ingest_transcript` and JS did not, so a\n // TypeScript client on a deep ladder had no route that could name a rung.\n const scopePath = either<Record<string, string>>(o, 'scope_path', 'scopePath');\n if (scopePath !== undefined) body['scope'] = scopePath;\n return body;\n}\n\nexport function buildCompactBody(options: CompactOptions): Json {\n const o = options as unknown as Record<string, unknown>;\n const conversationId = either<string>(o, 'conversation_id', 'conversationId');\n if (!conversationId) throw new InvalidInputError('conversation_id is required');\n validateConversationId(conversationId);\n\n const body: Json = { conversation_id: conversationId, force: options.force ?? false };\n const strategy = options.strategy ?? either<string>(o, 'compaction_level', 'compactionLevel');\n const targetTokens = either<number>(o, 'target_tokens', 'targetTokens');\n if (strategy !== undefined) body['strategy'] = strategy;\n if (targetTokens !== undefined) body['target_tokens'] = targetTokens;\n return body;\n}\n\n/**\n * Result of `conversation.ingest_transcript`, mirroring Python's\n * `TranscriptIngestResponse`. It was `Json` here, so `ingestion_id` came back\n * as `unknown` and could not be handed to `wait_for_completion` without a cast.\n */\nexport interface TranscriptIngestResult {\n conversation_id: string;\n /** The raw client-supplied id, echoed back. */\n external_conversation_id: string;\n /** Always set on a 2xx, including the `duplicate` branch. */\n ingestion_id: string;\n status: 'queued' | 'duplicate';\n turns_recorded: number;\n summary_status: 'in_progress' | 'already_compacted' | 'skipped';\n /** ISO-8601. */\n queued_at: string;\n [key: string]: unknown;\n}\n\nexport interface ConversationNamespace {\n record_message(options: RecordMessageOptions): Promise<Json>;\n record_messages_batch(messages: RecordMessageOptions[]): Promise<Json>;\n ingest_transcript(options: IngestTranscriptOptions): Promise<TranscriptIngestResult>;\n context: {\n fetch(options?: FetchOptions): Promise<RawContext>;\n get_context_for_prompt(options?: {\n conversation_id?: string;\n conversationId?: string;\n style?: string;\n /** Search the earlier turns locally after a compaction (default true). */\n search_history?: boolean;\n searchHistory?: boolean;\n /** What to search for; defaults to the latest user message. */\n query?: string;\n }): Promise<Json>;\n compact(options: CompactOptions): Promise<Json>;\n get_compacted(options: { conversation_id?: string; conversationId?: string; version?: number; format?: string }): Promise<Json | null>;\n get_compaction_status(options: { conversation_id?: string; conversationId?: string } | string): Promise<Json>;\n subscribe_to_compaction_updates(\n conversationId: string,\n callback: CompactionCallback,\n ): () => void;\n unsubscribe_all_compaction_updates(conversationId?: string): number;\n };\n}\n\nexport type CompactionCallback = (bundle: Json) => unknown;\n\n/**\n * Registry of per-conversation compaction subscribers.\n *\n * Split out from the namespace so the instance stream can dispatch into it\n * without the two namespaces importing each other.\n */\nexport class CompactionSubscribers {\n readonly #byConversation = new Map<string, CompactionCallback[]>();\n\n add(conversationId: string, callback: CompactionCallback): () => void {\n const list = this.#byConversation.get(conversationId);\n if (list === undefined) this.#byConversation.set(conversationId, [callback]);\n else list.push(callback);\n\n let unsubscribed = false;\n return () => {\n // Idempotent, matching Python: calling the thunk twice is a no-op.\n if (unsubscribed) return;\n unsubscribed = true;\n const current = this.#byConversation.get(conversationId);\n if (current === undefined) return;\n const at = current.indexOf(callback);\n if (at !== -1) current.splice(at, 1);\n if (current.length === 0) this.#byConversation.delete(conversationId);\n };\n }\n\n removeAll(conversationId?: string): number {\n if (conversationId === undefined) {\n let count = 0;\n for (const list of this.#byConversation.values()) count += list.length;\n this.#byConversation.clear();\n return count;\n }\n const list = this.#byConversation.get(conversationId);\n if (list === undefined) return 0;\n this.#byConversation.delete(conversationId);\n return list.length;\n }\n\n /** Fires subscribers in registration order. A throwing listener must not break the stream reader. */\n dispatch(conversationId: string, bundle: Json, onError: (e: unknown) => void): void {\n // Copy first: a callback that unsubscribes itself would otherwise mutate\n // the array mid-iteration and skip the next listener.\n for (const callback of [...(this.#byConversation.get(conversationId) ?? [])]) {\n try {\n const result = callback(bundle) as unknown;\n // An async callback returns a promise; swallow its rejection the way\n // Python swallows one from an asyncio task.\n if (result instanceof Promise) result.catch(onError);\n } catch (error) {\n onError(error);\n }\n }\n }\n}\n\n/**\n * Pair each message a batch write stored with the server's row for it.\n *\n * The server's `results` holds the messages that were stored, in input order,\n * and leaves the failed ones out; those are in `errors`, each with the `index`\n * it had in the request. Buffering every body, or pairing `results` against\n * the whole input by position, puts a message the server refused into the\n * caller's next prompt.\n *\n * Returns nothing when the two cannot be lined up, as Python does. A row with\n * no `message_id` is treated as not stored.\n */\nexport function storedBatchRows(\n bodies: Array<Record<string, unknown>>,\n result: unknown,\n): Array<[Record<string, unknown>, Record<string, unknown>]> {\n const r = (result ?? {}) as Record<string, unknown>;\n const results = Array.isArray(r['results']) ? (r['results'] as Array<Record<string, unknown>>) : [];\n const errors = Array.isArray(r['errors']) ? (r['errors'] as Array<Record<string, unknown>>) : [];\n const failed = new Set(errors.map((e) => e?.['index']));\n const stored = bodies.filter((_, index) => !failed.has(index));\n if (stored.length !== results.length) return [];\n const pairs: Array<[Record<string, unknown>, Record<string, unknown>]> = [];\n stored.forEach((body, i) => {\n const row = results[i];\n if (row && typeof row === 'object' && row['message_id']) pairs.push([body, row]);\n });\n return pairs;\n}\n\n/**\n * Called after a successful write with the ids that were written for.\n *\n * Backs `SYNAP_SDK_CACHE_INVALIDATE_ON_WRITE`. The cloud invalidates its own\n * Redis caches on ingest, but that stops at the process boundary: without this\n * the in-SDK anticipation cache keeps serving the pre-write view for the rest\n * of its TTL. Python calls its equivalent from these same three write paths.\n */\nexport type WriteInvalidator = (ids: {\n userId?: string | undefined;\n customerId?: string | undefined;\n}) => void;\n\n/**\n * Called with each turn a write path sends, so it can be buffered locally.\n *\n * This is what makes the next fetch see the turn that was just recorded, ahead\n * of the server having persisted and compacted it.\n */\nexport type TurnRecorder = (turn: {\n conversationId: string;\n role: string;\n content: string;\n timestamp?: string | undefined;\n /** The server's id for the turn, from the write's response. */\n messageId?: string | undefined;\n /**\n * The conversation's message count after this write, as the server reported\n * it. This is what tells the local store whether it holds the whole\n * conversation. Absent from a server that predates it.\n */\n serverMessageCount?: number | null | undefined;\n /** Other writes this process still has outstanding for the conversation. */\n inFlight?: number | undefined;\n}) => void;\n\nexport function createConversationNamespace(\n transport: HttpTransport,\n subscribers: CompactionSubscribers = new CompactionSubscribers(),\n onWrite: WriteInvalidator = () => {},\n onTurn: TurnRecorder = () => {},\n /**\n * Applied to a conversation-scoped response before it is returned.\n *\n * `scopePath` is carried because one of the things this hook does is splice\n * in a cached user summary, and a summary prefetched at one rung must not be\n * spliced into a response for a caller standing at another. Python reads the\n * path off the fetch arguments at that call site; here the call site is a\n * callback, so the path has to travel with it or the rung check is silently\n * skipped on this one path.\n */\n onFetched: (\n response: RawContext,\n conversationId: string | undefined,\n userId: string | undefined,\n scopePath: Record<string, string> | undefined,\n ) => void = () => {},\n /**\n * SDK-authoritative short-term context. Returns a locally rendered response\n * when the flag is on AND the store is warm for this conversation, which\n * skips the cloud round trip entirely; null falls through to the network.\n */\n localShortTerm: {\n prompt: (conversationId: string, style: string) => Json | null;\n compacted: (conversationId: string, format: string) => Json | null;\n /**\n * Handed the server's `/context-for-prompt` answer after the local store\n * could not answer, so the store can be brought back in step.\n */\n reseed?: (conversationId: string, serverResponse: Json) => void;\n /**\n * The response with the earlier turns that best match `query` appended,\n * found by a local search of the turn archive. Python:\n * `MaximemSynapSDK._with_history_excerpts`.\n */\n historyExcerpts?: (conversationId: string, response: Json, query: string | undefined) => Promise<Json>;\n } = { prompt: () => null, compacted: () => null },\n /** The client's `agent_model` / `agent_context_window`, folded into each turn's metadata. */\n agentModel: () => AgentModel = () => ({}),\n): ConversationNamespace {\n // conversation_id -> REST writes sent and not yet buffered locally. Lets the\n // local store tell two of this process's own concurrent writes apart from a\n // second writer.\n const writesInFlight = new Map<string, number>();\n const withTurnModel = (body: Json, options: unknown): Json => {\n const metadata = withAgentModel(\n body['metadata'] as Record<string, unknown> | undefined, agentModelOf(options), agentModel(),\n );\n return metadata === undefined ? body : { ...body, metadata: metadata as Json };\n };\n const writeStarted = (conversationId: string): void => {\n writesInFlight.set(conversationId, (writesInFlight.get(conversationId) ?? 0) + 1);\n };\n const writeLanded = (conversationId: string): void => {\n const remaining = (writesInFlight.get(conversationId) ?? 0) - 1;\n if (remaining > 0) writesInFlight.set(conversationId, remaining);\n else writesInFlight.delete(conversationId);\n };\n const confirmation = (\n result: unknown,\n conversationId: string,\n ): { messageId?: string; serverMessageCount?: number | null; inFlight: number } => {\n const r = (result ?? {}) as Record<string, unknown>;\n const count = r['conversation_message_count'];\n return {\n ...(typeof r['message_id'] === 'string' ? { messageId: r['message_id'] } : {}),\n serverMessageCount: typeof count === 'number' ? count : null,\n inFlight: writesInFlight.get(conversationId) ?? 0,\n };\n };\n const convId = (v: { conversation_id?: string; conversationId?: string } | string): string => {\n const id = typeof v === 'string' ? v : (v.conversation_id ?? v.conversationId);\n if (!id) throw new InvalidInputError('conversation_id is required');\n // Covers context.fetch, get_compacted, get_compaction_status and\n // get_context_for_prompt: the four routes Python validates through this\n // same shape. ingest_transcript does not go through here, which is correct.\n validateConversationId(id);\n return id;\n };\n\n return {\n async record_message(options) {\n transport.checkCustomerId(\n (options as unknown as Record<string, unknown>)['customer_id'] ??\n (options as unknown as Record<string, unknown>)['customerId'],\n 'conversation.record_message',\n );\n const body = withTurnModel(buildRecordMessageBody(options), options);\n const conversationId = String(body['conversation_id'] ?? '');\n writeStarted(conversationId);\n let result: Json;\n try {\n result = await transport.request<Json>('conversations_messages', { body });\n } finally {\n writeLanded(conversationId);\n }\n // Buffer locally only AFTER the server accepted it. Recording a turn the\n // write failed on would surface a message that does not exist.\n onTurn({\n conversationId,\n role: String(body['role'] ?? 'user'),\n content: String(body['content'] ?? ''),\n timestamp: body['timestamp'] as string | undefined,\n ...confirmation(result, conversationId),\n });\n onWrite({\n userId: body['user_id'] as string | undefined,\n customerId: body['customer_id'] as string | undefined,\n });\n return result;\n },\n\n async record_messages_batch(messages) {\n if (!Array.isArray(messages) || messages.length === 0) {\n throw new InvalidInputError('messages must be a non-empty array');\n }\n // Every message checked BEFORE any is sent: a batch that fails on its\n // fifth message would otherwise be half-applied.\n for (const m of messages) {\n const mo = m as unknown as Record<string, unknown>;\n transport.checkCustomerId(\n mo['customer_id'] ?? mo['customerId'], 'conversation.record_messages_batch',\n );\n }\n const bodies = messages.map((m) => withTurnModel(buildRecordMessageBody(m), m));\n const result = await transport.request<Json>('conversations_messages_batch', {\n body: { messages: bodies },\n });\n // Buffer only what the server stored, each paired with its own row.\n for (const [body, row] of storedBatchRows(bodies, result)) {\n const conversationId = String(body['conversation_id'] ?? '');\n onTurn({\n conversationId,\n role: String(body['role'] ?? 'user'),\n content: String(body['content'] ?? ''),\n timestamp: body['timestamp'] as string | undefined,\n ...confirmation(row, conversationId),\n });\n }\n // Every distinct entity in the batch, not just the first message's.\n for (const body of bodies) {\n onWrite({\n userId: body['user_id'] as string | undefined,\n customerId: body['customer_id'] as string | undefined,\n });\n }\n return result;\n },\n\n async ingest_transcript(options) {\n const body = buildIngestTranscriptBody(options);\n const result = await transport.request<TranscriptIngestResult>('conversations_ingest', { body });\n onWrite({\n userId: body['user_id'] as string | undefined,\n customerId: body['customer_id'] as string | undefined,\n });\n return result;\n },\n\n context: {\n async fetch(options = {}) {\n // Conversation-scope fetch is one of Python's seven validation sites.\n // It does not route through convId() because the id travels in the\n // body rather than the path, so validate it explicitly.\n const id =\n (options as { conversation_id?: string; conversationId?: string }).conversation_id ??\n (options as { conversationId?: string }).conversationId;\n validateConversationId(id);\n const response = await fetchContext(transport, 'conversation', options);\n // Read-your-writes. This is the path where it matters most: a fast\n // follow-up on the same conversation would otherwise retrieve context\n // missing the turn just recorded.\n onFetched(\n response,\n id,\n (options as { user_id?: string; userId?: string }).user_id ??\n (options as { userId?: string }).userId,\n (options as { scope_path?: Record<string, string> }).scope_path ??\n (options as { scopePath?: Record<string, string> }).scopePath,\n );\n return response;\n },\n\n get_context_for_prompt: async (options = {}) => {\n const id = convId(options);\n const search = (options.search_history ?? options.searchHistory ?? true) && localShortTerm.historyExcerpts;\n const withHistory = async (response: Json): Promise<Json> => {\n if (!search) return response;\n try {\n return await localShortTerm.historyExcerpts!(id, response, options.query);\n } catch {\n return response; // never fail the prompt over its extras\n }\n };\n const local = localShortTerm.prompt(id, options.style ?? 'structured');\n if (local !== null) return withHistory(local);\n const result = await transport.request<Json>('context_for_prompt', {\n pathParams: { conversation_id: id },\n query: { style: options.style ?? 'structured' },\n });\n const response = (result?.['context_for_prompt'] as Json) ?? result ?? {};\n localShortTerm.reseed?.(id, response);\n return withHistory(response);\n },\n\n async compact(options) {\n return transport.request<Json>('conversations_compact', { body: buildCompactBody(options) });\n },\n\n get_compacted: async (options) => {\n const id = convId(options);\n // Python only takes the local path when no explicit version is asked\n // for: a pinned version is a request for a specific server artefact.\n if (options.version === undefined) {\n const local = localShortTerm.compacted(id, options.format ?? 'structured');\n if (local !== null) return local;\n }\n const query: Record<string, string | number> = { format: options.format ?? 'structured' };\n if (options.version !== undefined) query['version'] = options.version;\n try {\n return await transport.request<Json>('conversations_compacted', {\n pathParams: { conversation_id: id },\n query,\n });\n } catch (error) {\n // Python returns None when there is nothing compacted yet, rather\n // than surfacing a 404. Matching that keeps callers identical.\n if ((error as { code?: string }).code === 'context_not_found') return null;\n throw error;\n }\n },\n\n async get_compaction_status(options) {\n return transport.request<Json>('conversations_compaction_status', {\n pathParams: { conversation_id: convId(options) },\n });\n },\n\n /**\n * Fire `callback` whenever a `compaction_update` bundle arrives on the\n * Listen stream for this conversation.\n *\n * Requires an active `client.instance.listen()`; without it no bundles\n * arrive and the callback never fires. Registering before listening is\n * fine, which is why this does not throw when the stream is down.\n *\n * Returns an idempotent unsubscribe thunk, matching Python.\n */\n subscribe_to_compaction_updates(conversationId, callback) {\n if (!conversationId) throw new InvalidInputError('conversation_id is required');\n if (typeof callback !== 'function') throw new InvalidInputError('callback is required');\n return subscribers.add(conversationId, callback);\n },\n\n /** Remove subscribers for one conversation, or all of them. Returns the count removed. */\n unsubscribe_all_compaction_updates(conversationId?: string) {\n return subscribers.removeAll(conversationId);\n },\n },\n };\n}\n","/** User namespace. Mirrors `UserInterface` in the Python SDK. */\n\nimport { InvalidInputError } from '../errors.js';\nimport type { HttpTransport } from '../transport/http.js';\nimport { fetchContext, type FetchHooks } from '../context/fetch.js';\nimport type { FetchOptions, Json, RawContext } from '../context/types.js';\n\nexport interface UserNamespace {\n get_profile(options: { user_id?: string; userId?: string; customer_id?: string; customerId?: string } | string): Promise<Json>;\n context: { fetch(options?: FetchOptions): Promise<RawContext> };\n}\n\nexport function createUserNamespace(\n transport: HttpTransport,\n hooks: FetchHooks = {},\n): UserNamespace {\n return {\n async get_profile(options) {\n const userId = typeof options === 'string' ? options : (options.user_id ?? options.userId);\n if (!userId) throw new InvalidInputError('user_id is required');\n const customerId =\n typeof options === 'string' ? undefined : (options.customer_id ?? options.customerId);\n return transport.request<Json>('user_profile', {\n pathParams: { user_id: userId },\n // Sent only when provided, matching Python. Note the server returns\n // 500 rather than a clean error if it needs one and does not get it.\n ...(customerId !== undefined ? { query: { customer_id: customerId } } : {}),\n });\n },\n context: { async fetch(options = {}) { return fetchContext(transport, 'user', options, hooks); } },\n };\n}\n","/** Credits namespace. Mirrors `CreditsInterface` in the Python SDK. */\n\nimport { InvalidInputError } from '../errors.js';\nimport type { HttpTransport } from '../transport/http.js';\nimport type { Json } from '../context/types.js';\n\nexport interface EstimateOptions {\n metric_type?: string;\n metricType?: string;\n units?: number;\n item_count?: number;\n itemCount?: number;\n endpoint?: string;\n mode?: string;\n}\n\nexport interface CreditsNamespace {\n get_balance(): Promise<CreditBalance>;\n get_ledger(options?: LedgerOptions): Promise<CreditLedgerPage>;\n estimate(options: EstimateOptions): Promise<CreditEstimate>;\n redeem(code: string): Promise<RedeemResult>;\n}\n\n/**\n * Return types for the credits namespace, mirroring Python's dataclasses in\n * `maximem_synap/credits.py`. These were all `Json` (`Record<string, unknown>`)\n * before, so every field read was `unknown` and no documented example compiled.\n */\nexport interface CreditBucket {\n source_type: string;\n balance: number;\n /** ISO-8601, or null when the bucket does not expire. */\n expires_at: string | null;\n}\n\nexport interface CreditBalance {\n client_id: string;\n balance_credits: number;\n warning_low: boolean;\n buckets: CreditBucket[];\n [key: string]: unknown;\n}\n\nexport interface CreditLedgerEntry {\n ledger_id: string;\n entry_type: string;\n delta: number;\n metric_type: string | null;\n category: string | null;\n /** ISO-8601. */\n created_at: string;\n}\n\nexport interface CreditLedgerPage {\n entries: CreditLedgerEntry[];\n total: number;\n limit: number;\n offset: number;\n [key: string]: unknown;\n}\n\nexport interface CreditEstimate {\n credits_estimate: number;\n [key: string]: unknown;\n}\n\nexport interface RedeemResult {\n redemption_id: string;\n credits_granted: number;\n new_balance_credits: number;\n /** ISO-8601, or null when the granted credits do not expire. */\n expires_at: string | null;\n [key: string]: unknown;\n}\n\n/** Options for `credits.get_ledger`. Mirrors Python's keyword arguments. */\nexport interface LedgerOptions {\n /** Filter to one ledger entry type. */\n entry_type?: string;\n entryType?: string;\n /** Inclusive lower bound. ISO-8601 string or Date. Sent as `from`. */\n from_time?: string | Date;\n fromTime?: string | Date;\n /** Inclusive upper bound. ISO-8601 string or Date. Sent as `to`. */\n to_time?: string | Date;\n toTime?: string | Date;\n /** Defaults to 100, as in Python. */\n limit?: number;\n offset?: number;\n}\n\nfunction iso(v: string | Date): string {\n return v instanceof Date ? v.toISOString() : v;\n}\n\nexport function createCreditsNamespace(transport: HttpTransport): CreditsNamespace {\n return {\n async get_balance() { return transport.request<CreditBalance>('credits_balance'); },\n\n async get_ledger(options: LedgerOptions = {}) {\n // Mirrors Python's CreditsInterface.get_ledger: the three filters were\n // missing here entirely, and the default limit was 50 against Python's\n // 100, so the same call returned different pages in the two SDKs.\n const query: Record<string, string | number> = {\n limit: options.limit ?? 100,\n offset: options.offset ?? 0,\n };\n const entryType = options.entry_type ?? options.entryType;\n if (entryType !== undefined) query['entry_type'] = entryType;\n const from = options.from_time ?? options.fromTime;\n if (from !== undefined) query['from'] = iso(from);\n const to = options.to_time ?? options.toTime;\n if (to !== undefined) query['to'] = iso(to);\n return transport.request<CreditLedgerPage>('credits_ledger', { query });\n },\n\n async estimate(options) {\n return transport.request<CreditEstimate>('credits_estimate', {\n body: {\n metric_type: options.metric_type ?? options.metricType ?? null,\n units: options.units ?? null,\n item_count: options.item_count ?? options.itemCount ?? null,\n endpoint: options.endpoint ?? null,\n mode: options.mode ?? null,\n },\n });\n },\n\n async redeem(code) {\n if (!code) throw new InvalidInputError('code is required');\n // Non-idempotent by contract: redeeming twice consumes two codes.\n return transport.request<RedeemResult>('credits_redeem', { body: { code } });\n },\n };\n}\n","/**\n * Cache namespace. Mirrors `CacheInterface` in the Python SDK.\n *\n * This is a deliberately narrow facade over the AnticipationCache rather than\n * the cache object itself. Exposing the instance directly leaked internals\n * (`client.cache.now` was reachable) and gave JS a different surface from\n * Python for no benefit. The cache is still reachable as\n * `client.anticipationCache` for advanced use.\n */\n\nimport type { AnticipationCache } from '../context/anticipation-cache.js';\nimport { InvalidInputError } from '../errors.js';\n\nexport interface CacheStats {\n bundles: number;\n items: number;\n [key: string]: unknown;\n}\n\nexport interface CacheNamespace {\n clear(): void;\n clear_user(userId: string): void;\n clear_customer(customerId: string): void;\n stats(): CacheStats;\n}\n\nexport function createCacheNamespace(cache: AnticipationCache): CacheNamespace {\n return {\n clear: () => cache.clear(),\n\n clear_user: (userId) => {\n if (!userId) throw new InvalidInputError('user_id is required');\n cache.dropEntity(userId);\n },\n\n clear_customer: (customerId) => {\n if (!customerId) throw new InvalidInputError('customer_id is required');\n cache.dropEntity(customerId);\n },\n\n stats: () => ({ bundles: cache.size, items: cache.itemCount }),\n };\n}\n","/**\n * Instance namespace. Mirrors `InstanceInterface` in the Python SDK.\n *\n * Everything here rides the bidirectional gRPC Listen stream. The stream module\n * is loaded through a lazy `import()` so that importing this SDK on Edge or in\n * a Worker stays safe: grpc-js is built on `node:http2`/`node:net`/`node:tls`\n * and a static import fails the BUILD there, not the call.\n */\n\nimport { agentModelOf, withAgentModel, type AgentModel } from '../agent-model.js';\nimport {\n ListeningAlreadyActiveError, ListeningNotActiveError, SynapError,\n} from '../errors.js';\nimport type { AnticipationCache } from '../context/anticipation-cache.js';\nimport type { StreamCredentials, StreamState } from '../grpc/stream-client.js';\nimport { getEnv } from '../util/env.js';\nimport { newCorrelationId } from '../util/correlation.js';\n\nexport interface ListenOptions {\n /** Called with the attempt count each time the stream reconnects. */\n on_reconnect?: (attempt: number) => void;\n onReconnect?: (attempt: number) => void;\n /** Called with a reason string when the stream drops. */\n on_disconnect?: (reason: string) => void;\n onDisconnect?: (reason: string) => void;\n /**\n * Called for every bundle that arrives. Bundles are ALSO stored in the\n * anticipation cache automatically, so `fetch()` finds them without a round\n * trip; this callback is for observing, not for wiring up storage.\n */\n on_context?: (bundle: Record<string, unknown>) => void;\n onContext?: (bundle: Record<string, unknown>) => void;\n /** Overrides `SYNAP_GRPC_HOST`. */\n host?: string;\n /** Overrides `SYNAP_GRPC_PORT`. */\n port?: number;\n /** Overrides `SYNAP_GRPC_USE_TLS`. Plaintext only makes sense against localhost. */\n use_tls?: boolean;\n useTls?: boolean;\n}\n\nexport interface SendMessageOptions {\n content: string;\n role?: string;\n conversation_id?: string;\n conversationId?: string;\n user_id?: string;\n userId?: string;\n customer_id?: string;\n customerId?: string;\n session_id?: string;\n sessionId?: string;\n event_type?: string;\n eventType?: string;\n metadata?: Record<string, string>;\n /** The agent's model, as on `conversation.record_message`. Python's `model=`. */\n model?: string;\n context_window?: number;\n contextWindow?: number;\n /**\n * Report a tool call on the stream. Python's `send_message` takes both, and\n * the proto has `tool_name` / `tool_args_json`; the JS surface simply never\n * exposed them, so a tool call could not be reported from JavaScript.\n */\n tool_name?: string;\n toolName?: string;\n tool_args?: Record<string, unknown>;\n toolArgs?: Record<string, unknown>;\n /**\n * What a tool returned. Travels in its own proto field, not in `content`: a\n * result is not something a person said, and everything that filters on\n * messages reads `content`.\n */\n tool_result?: unknown;\n toolResult?: unknown;\n /** Ties a result to the call that asked for it. */\n tool_call_id?: string;\n toolCallId?: string;\n search_queries?: string[];\n searchQueries?: string[];\n context_types?: string[];\n contextTypes?: string[];\n}\n\nexport interface RecordThinkingOptions {\n content: string;\n /**\n * Ordinal of this thought within the current turn. Sent as a `step_index`\n * metadata key, which is how Python's `record_thinking` transmits it: the\n * proto has no dedicated field.\n */\n step_index?: number;\n stepIndex?: number;\n /** Free-form label for the kind of reasoning. Sent as `thought_type`. */\n thought_type?: string;\n thoughtType?: string;\n conversation_id?: string;\n conversationId?: string;\n user_id?: string;\n userId?: string;\n customer_id?: string;\n customerId?: string;\n session_id?: string;\n sessionId?: string;\n metadata?: Record<string, string>;\n}\n\nexport interface RecordToolCallOptions {\n tool_name?: string;\n toolName?: string;\n tool_args?: Record<string, unknown>;\n toolArgs?: Record<string, unknown>;\n tool_call_id?: string;\n toolCallId?: string;\n conversation_id?: string;\n conversationId?: string;\n user_id?: string;\n userId?: string;\n customer_id?: string;\n customerId?: string;\n session_id?: string;\n sessionId?: string;\n metadata?: Record<string, string>;\n}\n\nexport interface RecordToolResultOptions {\n /** JSON-encodable, or a plain string. A string is sent as itself. */\n result: unknown;\n tool_name?: string;\n toolName?: string;\n tool_call_id?: string;\n toolCallId?: string;\n conversation_id?: string;\n conversationId?: string;\n user_id?: string;\n userId?: string;\n customer_id?: string;\n customerId?: string;\n session_id?: string;\n sessionId?: string;\n metadata?: Record<string, string>;\n}\n\nexport interface InstanceNamespace {\n listen(options?: ListenOptions): Promise<void>;\n stop_listening(): Promise<void>;\n send_message(options: SendMessageOptions): Promise<void>;\n record_thinking(options: RecordThinkingOptions): Promise<void>;\n record_tool_call(options: RecordToolCallOptions): Promise<void>;\n record_tool_result(options: RecordToolResultOptions): Promise<void>;\n end_session(conversationId: string): Promise<void>;\n /** Property, not a method, matching Python's InstanceInterface.is_listening. */\n readonly is_listening: boolean;\n /** Not in Python: lets the compaction subscriptions share this one stream. */\n readonly _internal: InstanceInternals;\n}\n\n/** @internal Shared with the conversation namespace so both use one stream. */\nexport interface InstanceInternals {\n onCompactionUpdate(conversationId: string, bundle: Record<string, unknown>): void;\n /** Set by the client so a compaction can prune the locally buffered tail. */\n setCompactionApplier(apply: (bundle: Record<string, unknown>) => void): void;\n /**\n * Set by the client: called when `listen()` starts, which is when prefetched\n * bundles begin to arrive, so the local embedding model loads then.\n */\n setOnListen(handle: () => void): void;\n /**\n * Set by the client. Receives the server's message count for a conversation\n * after each streamed write has been stored.\n */\n setConversationStateHandler(\n handle: (conversationId: string, messageCount: number) => void,\n ): void;\n /** Learning-loop event, fired after a fetch is served from the cache. */\n sendContextUsed(event: Record<string, unknown>): void;\n /** Audit event, fired after every fetch resolves. */\n sendContextAssembled(event: Record<string, unknown>): void;\n setCompactionDispatcher(\n dispatch: (conversationId: string, bundle: Record<string, unknown>) => void,\n ): void;\n isListening(): boolean;\n}\n\ntype StreamClient = import('../grpc/stream-client.js').GrpcStreamClient;\n\nconst DEFAULT_GRPC_HOST = 'synap-cloud-prod.maximem.ai';\nconst DEFAULT_GRPC_PORT = 443;\n\nfunction envPort(): number | undefined {\n const raw = getEnv('SYNAP_GRPC_PORT');\n if (raw === undefined || raw === '') return undefined;\n const parsed = Number(raw);\n return Number.isFinite(parsed) ? parsed : undefined;\n}\n\nfunction envTls(): boolean | undefined {\n // SYNAP_GRPC_TLS is the 0.3.x wrapper's spelling. Honoured as a fallback,\n // because silently ignoring it turns a working plaintext deployment into one\n // that attempts TLS and cannot connect, with nothing pointing at the cause.\n const raw = getEnv('SYNAP_GRPC_USE_TLS') ?? getEnv('SYNAP_GRPC_TLS');\n if (raw === undefined || raw === '') return undefined;\n return !['0', 'false', 'no', 'off'].includes(raw.trim().toLowerCase());\n}\n\n/**\n * Fold `step_index` and `thought_type` into the metadata map.\n *\n * Python does exactly this (`sdk.py`, record_thinking): the proto carries no\n * dedicated fields, so the values ride in metadata under those two keys. A\n * caller-supplied metadata entry of the same name is not overwritten.\n */\nfunction thinkingMetadata(options: RecordThinkingOptions): Record<string, string> {\n const md: Record<string, string> = { ...(options.metadata ?? {}) };\n const step = options.step_index ?? options.stepIndex;\n if (step !== undefined && md['step_index'] === undefined) {\n md['step_index'] = String(step);\n }\n const kind = options.thought_type ?? options.thoughtType;\n if (kind && md['thought_type'] === undefined) {\n md['thought_type'] = kind;\n }\n return md;\n}\n\n/**\n * Event types the server has a reader for. Mirrors `KNOWN_EVENT_TYPES` in the\n * Python SDK. An unknown type falls through every classifier on the server into\n * UNKNOWN, where nothing reads it, so a typo used to cost the whole signal\n * without saying so.\n */\nconst KNOWN_EVENT_TYPES: ReadonlySet<string> = new Set([\n 'user_message',\n 'assistant_message',\n 'tool_call',\n 'tool_result',\n 'agent_thinking',\n 'context_request',\n 'context_fetch',\n 'session_start',\n 'session_end',\n]);\n\n/**\n * Serialise a tool result the way Python does: a string travels as itself,\n * anything else as JSON. `JSON.stringify` on a plain-text answer would wrap it\n * in quotes and the agent would read the quotes as part of the result.\n */\nfunction toolResultJson(result: unknown): string {\n if (result === undefined || result === null) return '';\n return typeof result === 'string' ? result : jsonOrText(result);\n}\n\n/**\n * JSON for whatever serialises, the value's text form for whatever does not.\n * Mirrors Python's `_json_or_text` in `sdk.py`.\n *\n * ⚠ This was a bare `JSON.stringify` and that lost events. It throws on a\n * circular object and on a `BigInt`, and it returns `undefined` rather than a\n * string for a function or a bare `undefined`. Every integration reports\n * through a helper that swallows exceptions on purpose, so a telemetry call can\n * never break somebody's agent loop, which means the throw went nowhere and the\n * whole tool event was silently never sent. Losing the exact shape of an object\n * is a far smaller loss than losing the event.\n */\nfunction jsonOrText(value: unknown): string {\n try {\n const out = JSON.stringify(\n value,\n (_k, v: unknown) => (typeof v === 'bigint' ? String(v) : v),\n );\n // `undefined` for a function, a symbol, or `undefined` itself.\n return out === undefined ? String(value) : out;\n } catch {\n // A cycle, or a `toJSON` of the caller's that throws.\n try {\n return String(value);\n } catch {\n return '';\n }\n }\n}\n\nexport function createInstanceNamespace(\n credentials: () => StreamCredentials,\n cache: AnticipationCache,\n /** Buffers a stream-sent turn locally, same as the HTTP write paths. */\n onTurn?: (turn: { conversationId: string; role: string; content: string }) => void,\n /** The client's `agent_model` / `agent_context_window`, folded into each event's metadata. */\n agentModel: () => AgentModel = () => ({}),\n /** Keeps a reported tool result in the local turn archive (`archive_tool_result`). */\n onToolResult?: (r: { conversationId: string; result: unknown; toolName: string }) => void,\n): InstanceNamespace {\n let client: StreamClient | null = null;\n // conversationId -> sessionId, for conversations this stream has opened a\n // session for. Cleared on reconnect: a session belongs to the stream that\n // carried it, and the server's new stream has never seen it.\n const openSessions = new Map<string, string>();\n let dispatchCompaction:\n | ((conversationId: string, bundle: Record<string, unknown>) => void)\n | null = null;\n\n let applyCompaction: ((bundle: Record<string, unknown>) => void) | null = null;\n let onListen: (() => void) | null = null;\n let onConversationState: ((conversationId: string, messageCount: number) => void) | null =\n null;\n\n const internals: InstanceInternals = {\n onCompactionUpdate(conversationId, bundle) {\n // Prune the local tail FIRST, then notify subscribers, so a subscriber\n // that immediately fetches sees the post-compaction view.\n applyCompaction?.(bundle);\n dispatchCompaction?.(conversationId, bundle);\n },\n setCompactionApplier(apply) {\n applyCompaction = apply;\n },\n setOnListen(handle) {\n onListen = handle;\n },\n setConversationStateHandler(handle) {\n onConversationState = handle;\n },\n sendContextUsed(event) {\n // Fire-and-forget: the stream drops it silently when disconnected, which\n // is correct for telemetry that must never affect the caller's turn.\n client?.sendContextUsed(event);\n },\n sendContextAssembled(event) {\n client?.sendContextAssembled(event);\n },\n setCompactionDispatcher(dispatch) {\n dispatchCompaction = dispatch;\n },\n isListening() {\n return client !== null && client.isConnected;\n },\n };\n\n /**\n * Open a session for this conversation if the stream has not yet.\n *\n * The developer never calls this. `session_start` is what makes the server\n * run its warm-up prefetch before the first message arrives, and asking every\n * caller to remember a lifecycle call is how the first turn of every\n * conversation stayed a cold fetch: neither SDK sent one, ever.\n *\n * Best effort. A session that cannot be opened must not stop the event that\n * triggered it, so the conversation stays unmarked and the next event retries.\n */\n function ensureSession(\n conversationId: string, userId: string, customerId: string,\n ): void {\n // Both ids, or no session. The server refuses a session_start with no\n // user_id, and writing one only says it went out, not that it was\n // accepted — so opening on an event with no user_id marked the\n // conversation as open, never retried, and left `end_session` referring to\n // a session the server never had. The next event that carries a user_id\n // opens it.\n if (!conversationId || !userId || client === null) return;\n if (openSessions.has(conversationId)) return;\n // The same helper the correlation ids use: a session id is an identifier\n // for tracing, never a security value, and that helper is the one that is\n // safe on Edge and Workers, where `node:crypto` breaks the build.\n const sessionId = newCorrelationId();\n try {\n client.sendSessionControl({\n action: 'start',\n session_id: sessionId,\n conversation_id: conversationId,\n ...(userId ? { user_id: userId } : {}),\n ...(customerId ? { customer_id: customerId } : {}),\n });\n openSessions.set(conversationId, sessionId);\n } catch {\n // Leave it unmarked so the next event opens it.\n }\n }\n\n const namespace: InstanceNamespace = {\n async listen(options: ListenOptions = {}) {\n if (client !== null) {\n // Python raises this from InstanceController when a second listen()\n // lands on an already-active stream.\n throw new ListeningAlreadyActiveError('Listening is already active on this instance');\n }\n\n try {\n onListen?.();\n } catch {\n /* loading the local model must never stop the stream */\n }\n const { GrpcStreamClient } = await import('../grpc/stream-client.js');\n const onContext = options.on_context ?? options.onContext;\n const onDisconnect = options.on_disconnect ?? options.onDisconnect;\n const onReconnect = options.on_reconnect ?? options.onReconnect;\n let reconnectAttempts = 0;\n\n // Assigned immediately below; the callbacks that read it only run later,\n // once the stream is live.\n let stream: InstanceType<typeof GrpcStreamClient> | null = null;\n\n const created = new GrpcStreamClient(credentials(), cache, {\n host: options.host ?? getEnv('SYNAP_GRPC_HOST') ?? DEFAULT_GRPC_HOST,\n port: options.port ?? envPort() ?? DEFAULT_GRPC_PORT,\n useTls: options.use_tls ?? options.useTls ?? envTls() ?? true,\n ...(onContext !== undefined ? { onContext } : {}),\n onCompactionUpdate: (conversationId, bundle) => {\n internals.onCompactionUpdate(conversationId, bundle);\n },\n onConversationState: (conversationId, messageCount) => {\n onConversationState?.(conversationId, messageCount);\n },\n onStateChange: (state: StreamState) => {\n // Any state change away from connected retires the sessions this\n // stream opened: the server's next stream has no memory of them, and\n // a client that believes one is open never warms up again.\n if (state !== 'connected') openSessions.clear();\n if (state === 'reconnecting') {\n reconnectAttempts += 1;\n onReconnect?.(reconnectAttempts);\n } else if (state === 'disconnected') {\n // A credit refusal names itself, the way Python's transport does,\n // so a handler can tell \"the server ran out of patience\" from\n // \"this account cannot pay for the stream\".\n const stopped = stream?.lastError ?? null;\n const reason = stopped === null\n ? 'stream disconnected'\n : `credit_stop:${(stopped as { reason?: string | null }).reason ?? 'unknown'}`;\n onDisconnect?.(reason);\n }\n },\n });\n stream = created;\n\n try {\n await created.connect();\n } catch (error) {\n // Leave `client` null so a retry is possible rather than being\n // rejected as \"already active\" forever.\n if (error instanceof SynapError) throw error;\n throw new SynapError(\n `Could not open the Synap anticipation stream: ${(error as Error)?.message ?? String(error)}`,\n { cause: error },\n );\n }\n client = created;\n },\n\n async stop_listening() {\n // Resolves rather than rejecting: stopping something that was never\n // started is a no-op in Python too, and a cleanup path that throws is\n // worse than useless in a finally block.\n if (client === null) return;\n // Close what this stream opened. A session that is never closed leaves\n // the turn it was in the middle of uncommitted on the server: that row is\n // written when the NEXT user message arrives, and the last turn of a\n // conversation has no next one.\n for (const conversationId of [...openSessions.keys()]) {\n await namespace.end_session(conversationId);\n }\n const active = client;\n client = null;\n await active.disconnect();\n },\n\n async end_session(conversationId: string) {\n const sessionId = openSessions.get(conversationId);\n if (sessionId === undefined || client === null) return;\n openSessions.delete(conversationId);\n try {\n client.sendSessionControl({\n action: 'end',\n session_id: sessionId,\n conversation_id: conversationId,\n });\n } catch {\n // A teardown path must not throw.\n }\n },\n\n async send_message(options: SendMessageOptions) {\n if (client === null) throw new ListeningNotActiveError('Listening is not active');\n if (options.content === undefined) {\n throw new ListeningNotActiveError('content is required');\n }\n const eventType = options.event_type ?? options.eventType ?? 'user_message';\n if (!KNOWN_EVENT_TYPES.has(eventType)) {\n throw new SynapError(\n `unknown event_type '${eventType}'. Expected one of: `\n + [...KNOWN_EVENT_TYPES].sort().join(', '),\n );\n }\n const conversationId = options.conversation_id ?? options.conversationId ?? '';\n ensureSession(\n conversationId,\n options.user_id ?? options.userId ?? '',\n options.customer_id ?? options.customerId ?? '',\n );\n // Only conversational turns belong in the local short-term buffer. A tool\n // call or a result is not a turn, and buffering one splices it into the\n // caller's own prompt as if somebody had said it. Python mirrors user and\n // assistant messages only; this side mirrored everything.\n if (eventType === 'user_message' || eventType === 'assistant_message') {\n onTurn?.({\n conversationId,\n role: options.role ?? 'user',\n content: options.content,\n });\n }\n client.sendConversationEvent({\n event_type: eventType,\n content: options.content,\n role: options.role ?? 'user',\n conversation_id: options.conversation_id ?? options.conversationId ?? '',\n user_id: options.user_id ?? options.userId ?? '',\n customer_id: options.customer_id ?? options.customerId ?? '',\n session_id: options.session_id ?? options.sessionId\n ?? openSessions.get(conversationId) ?? '',\n metadata: withAgentModel(options.metadata, agentModelOf(options), agentModel(), true) ?? {},\n timestamp_ms: Date.now(),\n tool_name: options.tool_name ?? options.toolName ?? '',\n // Python serialises tool_args with json.dumps into tool_args_json.\n tool_args_json: (() => {\n const a = options.tool_args ?? options.toolArgs;\n return a === undefined ? '' : jsonOrText(a);\n })(),\n tool_result_json: toolResultJson(options.tool_result ?? options.toolResult),\n tool_call_id: options.tool_call_id ?? options.toolCallId ?? '',\n search_queries: options.search_queries ?? options.searchQueries ?? [],\n context_types: options.context_types ?? options.contextTypes ?? [],\n });\n },\n\n async record_tool_call(options: RecordToolCallOptions) {\n // Prefer this over `send_message({ event_type: 'tool_call' })`. The event\n // carries both an event_type and a role and the two have to agree;\n // getting the role wrong was silent, and our own published example got it\n // wrong. Here you cannot: the method sets both.\n await namespace.send_message({\n content: '',\n role: 'assistant',\n event_type: 'tool_call',\n tool_name: options.tool_name ?? options.toolName ?? '',\n ...(options.tool_args ?? options.toolArgs\n ? { tool_args: options.tool_args ?? options.toolArgs }\n : {}),\n tool_call_id: options.tool_call_id ?? options.toolCallId ?? '',\n conversation_id: options.conversation_id ?? options.conversationId ?? '',\n user_id: options.user_id ?? options.userId ?? '',\n customer_id: options.customer_id ?? options.customerId ?? '',\n session_id: options.session_id ?? options.sessionId ?? '',\n metadata: options.metadata ?? {},\n });\n },\n\n async record_tool_result(options: RecordToolResultOptions) {\n // ⚠ A tool result is usually the caller's customer data. It is an\n // anticipation hint and is not written into long-term memory, but it does\n // reach us: send what the agent needs, not the whole row.\n const conversationId = options.conversation_id ?? options.conversationId ?? '';\n if (conversationId) {\n onToolResult?.({ conversationId, result: options.result, toolName: options.tool_name ?? options.toolName ?? '' });\n }\n await namespace.send_message({\n content: '',\n role: 'tool',\n event_type: 'tool_result',\n tool_result: options.result,\n tool_name: options.tool_name ?? options.toolName ?? '',\n tool_call_id: options.tool_call_id ?? options.toolCallId ?? '',\n conversation_id: options.conversation_id ?? options.conversationId ?? '',\n user_id: options.user_id ?? options.userId ?? '',\n customer_id: options.customer_id ?? options.customerId ?? '',\n session_id: options.session_id ?? options.sessionId ?? '',\n metadata: options.metadata ?? {},\n });\n },\n\n async record_thinking(options: RecordThinkingOptions) {\n if (client === null) throw new ListeningNotActiveError('Listening is not active');\n const conversationId = options.conversation_id ?? options.conversationId ?? '';\n ensureSession(\n conversationId,\n options.user_id ?? options.userId ?? '',\n options.customer_id ?? options.customerId ?? '',\n );\n client.sendConversationEvent({\n // Python emits reasoning as an `agent_thinking` conversation event.\n event_type: 'agent_thinking',\n content: options.content,\n // Deliberately no role. A thought is not the assistant's reply, and\n // this went out as 'assistant' from the day it shipped: the server\n // checked role before event_type, so every reasoning step any client\n // sent was filed as the final answer and the reasoning path never ran.\n // Sending no role is correct against both the fixed server and one that\n // has not been updated yet.\n role: '',\n conversation_id: options.conversation_id ?? options.conversationId ?? '',\n user_id: options.user_id ?? options.userId ?? '',\n customer_id: options.customer_id ?? options.customerId ?? '',\n session_id: options.session_id ?? options.sessionId ?? '',\n metadata: thinkingMetadata(options),\n timestamp_ms: Date.now(),\n });\n },\n\n get is_listening() {\n return client !== null && client.isConnected;\n },\n\n _internal: internals,\n };\n\n return namespace;\n}\n","/**\n * Kept in sync with package.json by the release workflow, in the\n * \"Sync SDK_VERSION to the resolved version\" step of publish-npm-js-sdk.yml.\n *\n * Until that step existed nothing kept it in sync, so this constant sat at\n * 1.0.0-beta.0 while the package shipped 0.4.x, and every request's User-Agent\n * and every telemetry event reported a version that was never released.\n *\n * ⚠ The workflow only covers a workflow release. 0.4.8 was published by hand\n * and shipped reporting 0.4.7, so every request from it carries a User-Agent\n * for a version it is not. `version-matches-package.test.ts` now fails the\n * build when the two disagree, which is the only thing that makes a manual\n * publish safe.\n */\nexport const SDK_VERSION = '0.5.10';\n","/**\n * Client registry: one shared client per identity, per process.\n *\n * Mirrors Python's `registry.py`. Without it, every `new SynapClient()` gets\n * its own anticipation cache, so a framework integration that constructs a\n * client per request or per agent never gets a cache hit and pays for a fresh\n * retrieval every time. Python has shared state here since 0.2; matching it is\n * the difference between a warm cache and a metered fetch on every turn.\n *\n * JavaScript reaches the same end differently. Python does\n * `self.__dict__ = existing.__dict__`, aliasing state between distinct\n * objects. A JS constructor can simply RETURN the existing instance, so the\n * two callers hold the same object rather than two objects sharing a state\n * bag. That is stricter than Python and avoids its `__dict__`-identity\n * comparisons entirely.\n */\n\n/** Kept structural so the registry does not depend on the client's type. */\nexport interface RegistrableClient {\n shutdown(): Promise<void>;\n}\n\nconst instances = new Map<string, RegistrableClient>();\n\n/**\n * Which registry slot an identity belongs in.\n *\n * `instanceId` keys the singleton when present, but it is usually empty at\n * construction and only resolved from the API key during `initialize()`.\n * Keying on it directly would put every `new SynapClient({ apiKey })` in one\n * process onto the single `''` slot, where the second would adopt the first's\n * credentials. So fall back to the credential that WILL resolve the identity,\n * stored as a truncated SHA-256 digest rather than plaintext, so a registry\n * dump or a log line cannot leak a key.\n *\n * `SYNAP_INSTANCE_ID` is deliberately not consulted: the client applies that\n * fallback after the lookup, and honouring it here would put two different\n * credentials back onto one slot whenever the variable happens to be set.\n */\nexport function buildRegistryKey(instanceId: string, apiKey: string | undefined): string {\n if (instanceId) return instanceId;\n const key = apiKey ?? '';\n // Nothing to key on. The client cannot initialise without a credential\n // anyway, so preserve the empty slot rather than inventing one.\n if (!key) return '';\n return `apikey:${digest(key)}`;\n}\n\n/**\n * 64-bit FNV-1a over the credential.\n *\n * Python uses SHA-256 here. This cannot: the digest is needed synchronously\n * inside a constructor, `node:crypto` is unavailable on Edge and Workers (and\n * a static import of it would fail those builds outright), and Web Crypto's\n * `subtle.digest` is async. So this is a non-cryptographic digest, chosen for\n * the one property that actually matters here: the raw key never becomes a Map\n * key that something could enumerate or log.\n *\n * It is not collision-resistant against an adversary, and it does not need to\n * be. A collision would make two clients share state, so the bound that\n * matters is accidental collision between the handful of distinct API keys\n * present in a single process, which at 64 bits is negligible. It is never\n * persisted, never sent anywhere, and never compared against anything outside\n * this module.\n */\nfunction digest(value: string): string {\n // FNV-1a, 64-bit. BigInt keeps the arithmetic exact; Number would lose\n // precision past 2^53 and collapse the space.\n const PRIME = 1099511628211n;\n const MASK = 0xffffffffffffffffn;\n let hash = 14695981039346656037n;\n for (let i = 0; i < value.length; i += 1) {\n hash ^= BigInt(value.charCodeAt(i));\n hash = (hash * PRIME) & MASK;\n }\n return hash.toString(16).padStart(16, '0');\n}\n\nexport function get(key: string): RegistrableClient | undefined {\n return instances.get(key);\n}\n\n/**\n * Claim `key` for `client`, or hand back whoever already holds it.\n *\n * A separate `get` then `set` would let two callers constructing the first\n * client for one identity both miss the lookup, with the second overwriting\n * the first. JavaScript's single-threaded execution makes that impossible\n * between two synchronous statements, so this is a single operation for\n * clarity rather than for locking: there is no equivalent of Python's mutex\n * to hold, and none is needed.\n *\n * Returns `undefined` when `client` won the slot, or the incumbent when not.\n */\nexport function registerIfAbsent(\n key: string,\n client: RegistrableClient,\n): RegistrableClient | undefined {\n const existing = instances.get(key);\n if (existing !== undefined) return existing;\n instances.set(key, client);\n return undefined;\n}\n\n/**\n * Point `key` at `client` as well, unless the slot is taken.\n *\n * Used once `initialize()` resolves the real instance id. The client was keyed\n * on its credential because the id did not exist yet, so a later\n * `new SynapClient({ instanceId })` for that same instance would miss and\n * build a second client: two anticipation caches and two Listen streams for\n * one instance. The credential slot is KEPT rather than moved, so constructing\n * by API key keeps returning the same client too.\n *\n * An occupied target is never clobbered. Evicting a live client is worse than\n * the duplication it would fix.\n */\nexport function aliasIfAbsent(key: string, client: RegistrableClient): boolean {\n if (!key || instances.has(key)) return false;\n instances.set(key, client);\n return true;\n}\n\n/**\n * Drop `key`, but only if it still points at `client`.\n *\n * A shutting-down client must not evict whoever currently holds its old slot.\n */\nexport function unregisterIfOwner(key: string, client: RegistrableClient): void {\n if (instances.get(key) === client) instances.delete(key);\n}\n\n/** Test seam. Never call this from library code. */\nexport function clear(): void {\n instances.clear();\n}\n\n/** Diagnostics only. */\nexport function size(): number {\n return instances.size;\n}\n","/**\n * Per-conversation short-term context, held locally.\n *\n * Mirrors Python's `cache/short_term_store.py`. This is what makes reads see\n * your own writes: a turn recorded a moment ago is buffered here and spliced\n * onto the next fetch, so a fast follow-up does not retrieve context that omits\n * what the user just said. Python's own comment on the overlay puts it plainly:\n * the pre-compaction window \"is exactly when the playground's fast follow-ups\n * happen.\"\n *\n * LRU by conversation, with an absolute age cap. Everything here is per process\n * and holds only what this process wrote, so nothing is authoritative: the\n * server's view wins for the summary fields, and this only ever contributes the\n * verbatim tail.\n *\n * Holding an entry is not the same as holding the conversation. A process that\n * restarted, evicted the entry, or shares the conversation with another process\n * has only the turns it recorded itself. So the server reports how many\n * messages it holds, on every write and on every compaction bundle, and\n * `isComplete()` says whether the entry can answer on its own. When it cannot,\n * the caller asks the server and hands the answer to `reseed`.\n */\n\nimport type { TurnStore } from './turn-store.js';\n\nconst DEFAULT_MAX_CONVERSATIONS = 100;\n/** 12 hours, as in Python. */\nconst DEFAULT_MAX_AGE_MS = 12 * 60 * 60 * 1000;\n\nexport interface ShortTermTurn {\n role: string;\n content: string;\n /** ISO-8601. */\n timestamp: string;\n /** The server's id for the turn, when the write returned one. */\n message_id?: string;\n}\n\n/**\n * How every v3 context_block begins (server: compaction/v3/render.py). A\n * summary that begins this way IS the block, which is how a REST-fed entry is\n * recognised. Same constant as Python's `CONTEXT_BLOCK_PREFIX`.\n */\nexport const CONTEXT_BLOCK_PREFIX = '## Conversation so far (compacted';\n\n/** The v3 block in a conversation_context payload, or null (v2 server). */\nexport function contextBlockOf(cc: Record<string, unknown>): string | null {\n const block = cc['context_block'];\n if (typeof block === 'string' && block) return block;\n const summary = cc['summary'];\n if (typeof summary === 'string' && summary.startsWith(CONTEXT_BLOCK_PREFIX)) return summary;\n return null;\n}\n\nexport interface ShortTermEntry {\n conversationId: string;\n summary: string | null;\n /**\n * Compaction v3: the server's one framed block, printed as received and\n * once. When set, the paragraphs are not used (the server sends them empty).\n */\n contextBlock: string | null;\n factualParagraph: string | null;\n conversationalParagraph: string | null;\n currentState: Record<string, unknown>;\n keyExtractions: Record<string, unknown>;\n compactionId: string | null;\n compactedAt: string | null;\n endTimestamp: string | null;\n recentTurns: ShortTermTurn[];\n lastActivityAt: number;\n // --- Completeness bookkeeping. See `isComplete`. ---\n /** Messages the cached summary covers, from the start of the conversation. */\n compactedMessageCount: number;\n /** The highest message count the server has reported. null = never heard. */\n serverMessageCount: number | null;\n /**\n * The server has confirmed at least one write made by this process. A\n * process that only reads never hears a count again after it is seeded, so\n * it could not notice the conversation moving on without it.\n */\n participant: boolean;\n /** At some point this entry held everything the server held. */\n synced: boolean;\n /**\n * The server's count ran ahead of an entry that was in sync, so something\n * else is writing to this conversation. Latched: this process cannot see\n * those writes when they happen, only at its own next write.\n */\n shared: boolean;\n}\n\n/** What a write learned from the server about the turn and the conversation. */\nexport interface TurnConfirmation {\n /** The server's id for the turn. A turn already held under it is not appended twice. */\n messageId?: string | undefined;\n /** The conversation's message count as the server reported it on this write. */\n serverMessageCount?: number | null | undefined;\n /** Other writes this process still has outstanding for the conversation. */\n inFlight?: number | undefined;\n}\n\n/** What `/context-for-prompt` returned, as far as `reseed` needs it. */\nexport interface ServerPromptState {\n compactionId: string | null;\n recentMessages: ShortTermTurn[];\n compactedMessageCount: number;\n totalMessageCount: number;\n}\n\nexport function localMessageCount(entry: ShortTermEntry): number {\n return entry.compactedMessageCount + entry.recentTurns.length;\n}\n\n/**\n * Whether an entry can stand in for the server's short-term context.\n *\n * True when the server holds nothing this entry lacks. Holding MORE than the\n * server has confirmed is fine: those are this process's own turns, sent and\n * not yet counted.\n */\nexport function isComplete(entry: ShortTermEntry): boolean {\n return (\n entry.participant &&\n entry.synced &&\n !entry.shared &&\n entry.serverMessageCount !== null &&\n entry.serverMessageCount <= localMessageCount(entry)\n );\n}\n\n/**\n * Whether `turns` already holds `turn`.\n *\n * By message id where both sides have one. Otherwise by role and text: a\n * streamed turn has no id (the server assigns it in a background write), and\n * the turns in a `fetch` response carry none either.\n */\nfunction heldIn(turn: ShortTermTurn, turns: ShortTermTurn[]): boolean {\n return turns.some((held) =>\n turn.message_id && held.message_id\n ? held.message_id === turn.message_id\n : held.role === turn.role && held.content === turn.content,\n );\n}\n\n/**\n * The local turns that come after everything in `serverTurns`.\n *\n * Walks back from the newest local turn and stops at the first one the\n * server's list already holds. What is left is what this process wrote or sent\n * after the server's list was built.\n *\n * A turn the server rewrote before storing it (a privacy policy) does not\n * match its local copy by text, so it can come back from here as well as from\n * the server. That shows one turn twice; it never loses one.\n */\nexport function turnsAfter(\n localTurns: readonly ShortTermTurn[],\n serverTurns: ShortTermTurn[],\n): ShortTermTurn[] {\n const newer: ShortTermTurn[] = [];\n for (let i = localTurns.length - 1; i >= 0; i -= 1) {\n const turn = localTurns[i] as ShortTermTurn;\n if (heldIn(turn, serverTurns)) break;\n newer.unshift(turn);\n }\n return newer;\n}\n\nexport interface ShortTermStoreOptions {\n maxConversations?: number;\n maxAgeMs?: number;\n /** Injected in tests. Wall clock, because entries are compared to ISO stamps. */\n now?: () => number;\n}\n\n/** Parse an ISO-8601 stamp to epoch millis, or null. */\nexport function parseIso(value: unknown): number | null {\n if (value === null || value === undefined || value === '') return null;\n if (value instanceof Date) return value.getTime();\n if (typeof value !== 'string') return null;\n const parsed = Date.parse(value);\n return Number.isNaN(parsed) ? null : parsed;\n}\n\n/**\n * True when a turn is strictly after the cutoff.\n *\n * An unparseable timestamp KEEPS the turn. Dropping it would silently lose a\n * message, which is worse than briefly showing one the server has already\n * folded into its summary.\n */\nfunction isAfter(turnTimestamp: unknown, cutoffMs: number): boolean {\n const parsed = parseIso(turnTimestamp);\n if (parsed === null) return true;\n return parsed > cutoffMs;\n}\n\nexport class ShortTermStore {\n // Insertion order IS the LRU order: re-inserting on touch moves an entry to\n // the end, so the first key Map yields is the least recently used.\n readonly #entries = new Map<string, ShortTermEntry>();\n readonly #maxConversations: number;\n readonly #maxAgeMs: number;\n readonly #now: () => number;\n\n /**\n * Every turn appended here is also kept in the archive (a TurnStore), which\n * a compaction does not prune: the turns it covers stay searchable locally\n * (history-search.ts). Python: `ShortTermContextStore.attach_archive`.\n */\n #archive: TurnStore | null = null;\n\n constructor(options: ShortTermStoreOptions = {}) {\n this.#maxConversations = options.maxConversations ?? DEFAULT_MAX_CONVERSATIONS;\n this.#maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;\n this.#now = options.now ?? (() => Date.now());\n }\n\n attachArchive(archive: TurnStore | null): void {\n this.#archive = archive;\n }\n\n get archive(): TurnStore | null {\n return this.#archive;\n }\n\n /** The entry, or null. Age-evicts on read, as Python does. */\n get(conversationId: string): ShortTermEntry | null {\n if (!conversationId) return null;\n const entry = this.#entries.get(conversationId);\n if (entry === undefined) return null;\n if (this.#now() - entry.lastActivityAt > this.#maxAgeMs) {\n this.#entries.delete(conversationId);\n return null;\n }\n this.#touch(conversationId, entry);\n return entry;\n }\n\n has(conversationId: string): boolean {\n return this.get(conversationId) !== null;\n }\n\n get size(): number {\n return this.#entries.size;\n }\n\n /**\n * Append a raw turn.\n *\n * Called from `record_message` and the stream's `send_message`. When no entry\n * exists yet (the first turn, before any compaction has arrived) a fresh one\n * is created with an empty summary; the next `compaction_update` fills the\n * summary fields in and prunes the turns it covers.\n *\n * `confirmation` is what the write learned from the server. See\n * `TurnConfirmation` and `noteServerCount`.\n */\n appendTurn(\n conversationId: string,\n role: string,\n content: string,\n timestamp?: string,\n confirmation: TurnConfirmation = {},\n ): void {\n if (!conversationId) return;\n const stamp = timestamp ?? new Date(this.#now()).toISOString();\n if (this.#archive !== null) {\n try {\n this.#archive.add(conversationId, role, content, stamp, { messageId: confirmation.messageId ?? null });\n } catch {\n // The archive never blocks the prompt path.\n }\n }\n const entry = this.#entries.get(conversationId) ?? this.#blank(conversationId);\n const messageId = confirmation.messageId;\n const alreadyHeld =\n Boolean(messageId) && entry.recentTurns.some((t) => t.message_id === messageId);\n if (!alreadyHeld) {\n const turn: ShortTermTurn = { role, content, timestamp: stamp };\n if (messageId) turn.message_id = messageId;\n entry.recentTurns.push(turn);\n }\n const count = confirmation.serverMessageCount;\n if (count !== null && count !== undefined) {\n this.#noteCount(entry, Number(count), confirmation.inFlight ?? 0);\n }\n entry.lastActivityAt = parseIso(stamp) ?? this.#now();\n this.#touch(conversationId, entry);\n this.#evict();\n }\n\n /**\n * Record the server's message count for a conversation.\n *\n * The streamed write path's half of `appendTurn`'s confirmation: a streamed\n * turn is appended when it is sent, and the count arrives later, once the\n * server has written it. A count for a conversation this process holds\n * nothing for is ignored.\n */\n noteServerCount(conversationId: string, count: number): void {\n if (!conversationId) return;\n const entry = this.#entries.get(conversationId);\n if (entry !== undefined) this.#noteCount(entry, Number(count), 0);\n }\n\n /**\n * Compare the server's count with what the entry holds.\n *\n * The count is taken after one of this process's own writes, so it proves\n * the process takes part in the conversation.\n *\n * Server ahead of an entry that was in sync: something else wrote, and the\n * entry is marked shared for good. Server ahead of an entry that was never\n * in sync: this process joined late (a restart, an eviction), and `reseed`\n * will fix it. `inFlight` covers this process's own writes that the server\n * has counted and the entry has not appended yet, so two concurrent writes\n * do not look like a second writer.\n */\n #noteCount(entry: ShortTermEntry, count: number, inFlight: number): void {\n entry.participant = true;\n if (count > localMessageCount(entry) + inFlight) {\n if (entry.synced) entry.shared = true;\n } else if (entry.serverMessageCount === null) {\n // First word from the server, and nothing is missing.\n entry.synced = true;\n }\n entry.serverMessageCount = Math.max(entry.serverMessageCount ?? 0, count);\n }\n\n /**\n * Apply a `compaction_update` bundle.\n *\n * Replaces the summary fields from the bundle's `conversation_context` and\n * drops the buffered turns the new summary covers.\n *\n * Which turns those are is decided by count, not by clock. The bundle says\n * how many messages the summary covers and how many the conversation held\n * when the bundle was built, so the last `total - compacted` of its\n * `recent_turns` are exactly the uncovered messages. Turns this process\n * appended after the server took that count are kept behind them. Timestamps\n * cannot do this job: the REST write returns the time of the response and\n * not of the stored row, a caller may supply its own message time, and a\n * streamed turn is stamped when a background write runs.\n *\n * A bundle from a server that sends no counts falls back to the older\n * timestamp prune, and the entry is left out of sync so it is never used to\n * answer on its own.\n */\n applyCompaction(bundle: Record<string, unknown>): void {\n if (typeof bundle !== 'object' || bundle === null) return;\n const cc = (bundle['conversation_context'] ?? {}) as Record<string, unknown>;\n const conversationId = String(\n cc['conversation_id'] ??\n bundle['anticipation_conversation_id'] ??\n bundle['conversation_id'] ??\n '',\n );\n if (!conversationId) return;\n\n const endTs = parseIso(cc['end_timestamp'] ?? cc['compacted_at']);\n const compactedAt = parseIso(cc['compacted_at']);\n const compactedCount = Number(cc['compacted_message_count'] ?? 0) || 0;\n const totalCount = Number(cc['total_message_count'] ?? 0) || 0;\n const entry = this.#entries.get(conversationId) ?? this.#blank(conversationId);\n\n const heldAt = parseIso(entry.compactedAt);\n // An older compaction must not replace a newer one.\n if (compactedAt !== null && heldAt !== null && compactedAt < heldAt) return;\n\n // `x || existing` throughout, matching Python: a blank field from the\n // server must not erase what is already held.\n const block = contextBlockOf(cc);\n if (block) {\n // A v3 compaction replaces the whole record. Keeping a v2 paragraph\n // cached from an earlier push would print it next to the block, so the\n // paragraphs are cleared, not kept.\n entry.contextBlock = block;\n entry.summary = (cc['summary'] as string) || block;\n entry.factualParagraph = null;\n entry.conversationalParagraph = null;\n } else {\n entry.summary = (cc['summary'] as string) || entry.summary;\n entry.factualParagraph = (cc['factual_paragraph'] as string) || entry.factualParagraph;\n entry.conversationalParagraph =\n (cc['conversational_paragraph'] as string) || entry.conversationalParagraph;\n }\n entry.currentState = { ...((cc['current_state'] as Record<string, unknown>) ?? {}) };\n entry.keyExtractions = { ...((cc['key_extractions'] as Record<string, unknown>) ?? {}) };\n entry.compactionId = (cc['compaction_id'] as string) || entry.compactionId;\n entry.compactedAt = (cc['compacted_at'] as string) || entry.compactedAt;\n entry.endTimestamp = (cc['end_timestamp'] as string) || entry.endTimestamp;\n\n if (compactedCount > 0 && compactedCount <= totalCount) {\n const uncovered = totalCount - compactedCount;\n const serverTurns = Array.isArray(cc['recent_turns'])\n ? (cc['recent_turns'] as ShortTermTurn[])\n : [];\n const tail: ShortTermTurn[] =\n uncovered > 0 ? serverTurns.slice(-uncovered).map((t) => ({ ...t })) : [];\n // Turns appended here after the server counted. Only an entry that was\n // in sync knows how many of its own turns those are.\n const ahead = localMessageCount(entry) - totalCount;\n const newer =\n entry.synced && ahead > 0\n ? entry.recentTurns.slice(-ahead).filter((t) => !heldIn(t, tail))\n : [];\n entry.recentTurns = [...tail, ...newer];\n entry.compactedMessageCount = compactedCount;\n entry.serverMessageCount = Math.max(entry.serverMessageCount ?? 0, totalCount);\n // A short tail means the bundle did not carry every uncovered message,\n // so the entry cannot claim to hold them.\n entry.synced = tail.length === uncovered;\n } else {\n if (endTs !== null && entry.recentTurns.length > 0) {\n entry.recentTurns = entry.recentTurns.filter((t) => isAfter(t.timestamp, endTs));\n }\n entry.synced = false;\n }\n entry.lastActivityAt = this.#now();\n this.#touch(conversationId, entry);\n this.#evict();\n }\n\n /**\n * Bring an entry back in step from a `/context-for-prompt` response.\n *\n * Called after the server was asked because the entry was not complete. The\n * response lists every message the server's summary does not cover, which is\n * what `recentTurns` is meant to hold.\n *\n * Only an entry that already exists is reseeded. Creating one here would let\n * a process that never writes to the conversation answer from a snapshot it\n * has no way to refresh.\n *\n * If the server's summary is not the one cached here, the entry cannot\n * render it, so it stays out of sync until the next bundle brings the\n * summary itself.\n *\n * Turns held locally and absent from the response are kept when they come\n * after everything the response has: they were written or sent after the\n * server read its messages.\n */\n reseed(conversationId: string, state: ServerPromptState): void {\n if (!conversationId) return;\n const entry = this.#entries.get(conversationId);\n if (entry === undefined || entry.shared) return;\n entry.serverMessageCount = Math.max(\n entry.serverMessageCount ?? 0,\n Number(state.totalMessageCount) || 0,\n );\n if ((state.compactionId || null) !== entry.compactionId) {\n entry.synced = false;\n return;\n }\n const serverTurns = state.recentMessages.map((m) => ({ ...m }));\n entry.recentTurns = [...serverTurns, ...turnsAfter(entry.recentTurns, serverTurns)];\n entry.compactedMessageCount = Number(state.compactedMessageCount) || 0;\n entry.synced = true;\n }\n\n invalidate(conversationId: string): void {\n this.#entries.delete(conversationId);\n }\n\n clear(): void {\n this.#entries.clear();\n }\n\n #blank(conversationId: string): ShortTermEntry {\n return {\n conversationId,\n summary: null,\n contextBlock: null,\n factualParagraph: null,\n conversationalParagraph: null,\n currentState: {},\n keyExtractions: {},\n compactionId: null,\n compactedAt: null,\n endTimestamp: null,\n recentTurns: [],\n lastActivityAt: this.#now(),\n compactedMessageCount: 0,\n serverMessageCount: null,\n participant: false,\n synced: false,\n shared: false,\n };\n }\n\n /** Re-insert so this key becomes the most recently used. */\n #touch(conversationId: string, entry: ShortTermEntry): void {\n this.#entries.delete(conversationId);\n this.#entries.set(conversationId, entry);\n }\n\n #evict(): void {\n while (this.#entries.size > this.#maxConversations) {\n const oldest = this.#entries.keys().next();\n if (oldest.done === true) break;\n this.#entries.delete(oldest.value);\n }\n }\n}\n","/**\n * Local store of a conversation's raw turns, for search after compaction.\n *\n * Port of `maximem_synap/cache/turn_store.py`. The short-term store keeps\n * only the turns a compaction has not covered yet; this store keeps every\n * turn this process saw or fetched, so the details a compaction leaves out\n * can be found again by a local search (history-search.ts).\n *\n * Python keeps it in a SQLite file under ~/.synap. This one holds it in\n * memory, with the same limits and the same answers, because the SDK also\n * runs in browsers and edge runtimes, and the main entry may not load a node\n * builtin eagerly (bundle-hygiene.test.ts). A turn this process did not see\n * arrives through `addMany` from the turns route, with its position.\n */\n\nimport { HISTORY_SEARCH } from '../behavior/contract.js';\n\nexport interface StoreLimits {\n max_conversations: number;\n max_turns_per_conversation: number;\n max_bytes_per_conversation: number;\n idle_ttl_seconds: number;\n max_file_bytes: number;\n}\n\nexport interface StoredTurn {\n /** Insertion order in this store. */\n seq: number;\n role: string;\n content: string;\n timestamp: string;\n message_id: string | null;\n /** 1-based turn number on the server, when known. */\n position: number | null;\n}\n\nexport interface TurnInput {\n role?: string;\n content?: string;\n timestamp?: string | Date | null;\n message_id?: string | null;\n position?: number | string | null;\n}\n\nconst SWEEP_EVERY_WRITES = 50;\n\ninterface Conversation {\n lastActivity: number;\n turns: StoredTurn[];\n}\n\nfunction bytesOf(text: string): number {\n return new TextEncoder().encode(text).length;\n}\n\nexport class TurnStore {\n readonly limits: StoreLimits;\n readonly onDisk = false;\n #now: () => number;\n #seq = 0;\n #writes = 0;\n #conversations = new Map<string, Conversation>();\n #vectors = new Map<string, Map<number, Float32Array>>();\n\n constructor(options: { limits?: StoreLimits; now?: () => number } = {}) {\n this.limits = options.limits ?? { ...HISTORY_SEARCH.store_limits };\n this.#now = options.now ?? (() => Date.now() / 1000);\n }\n\n add(\n conversationId: string,\n role: string,\n content: string,\n timestamp?: string | null,\n options: { messageId?: string | null; position?: number | null } = {},\n ): void {\n this.addMany(conversationId, [\n { role, content, timestamp: timestamp ?? '', message_id: options.messageId ?? null, position: options.position ?? null },\n ]);\n }\n\n /**\n * Insert turns not held yet; returns how many were new. A turn held without\n * an id or position (a streamed write) gets them when the same role and\n * text come back from the server.\n */\n addMany(conversationId: string, turns: Iterable<TurnInput>): number {\n if (!conversationId) return 0;\n const conv = this.#conversations.get(conversationId) ?? { lastActivity: 0, turns: [] };\n let added = 0;\n for (const turn of turns) {\n const role = String(turn.role || 'user');\n const content = String(turn.content ?? '');\n if (!content) continue;\n const messageId = turn.message_id ? String(turn.message_id) : null;\n const raw = turn.position;\n const position =\n typeof raw === 'number' && Number.isInteger(raw) ? raw : /^\\d+$/.test(String(raw ?? '')) ? Number(raw) : null;\n const ts = turn.timestamp instanceof Date ? turn.timestamp.toISOString() : String(turn.timestamp ?? '');\n if (messageId !== null) {\n const held = conv.turns.find((t) => t.message_id === messageId);\n if (held !== undefined) {\n if (position !== null) held.position = position;\n continue;\n }\n }\n const same = conv.turns.find(\n (t) => t.role === role && t.content === content && (t.message_id === null || t.position === null),\n );\n if (same !== undefined) {\n same.message_id = same.message_id ?? messageId;\n same.position = position ?? same.position;\n continue;\n }\n conv.turns.push({ seq: ++this.#seq, role, content, timestamp: ts, message_id: messageId, position });\n added += 1;\n }\n conv.lastActivity = this.#now();\n this.#conversations.set(conversationId, conv);\n this.#trim(conv);\n this.#writes += 1;\n if (this.#writes % SWEEP_EVERY_WRITES === 0) this.sweep();\n return added;\n }\n\n #trim(conv: Conversation): void {\n const limits = this.limits;\n let size = conv.turns.reduce((n, t) => n + bytesOf(t.content), 0);\n while (conv.turns.length > limits.max_turns_per_conversation || size > limits.max_bytes_per_conversation) {\n const excess = Math.max(conv.turns.length - limits.max_turns_per_conversation, 1);\n const order = [...conv.turns].sort(\n (a, b) => (a.position ?? Number.MAX_SAFE_INTEGER) - (b.position ?? Number.MAX_SAFE_INTEGER) || a.seq - b.seq,\n );\n const gone = new Set(order.slice(0, excess).map((t) => t.seq));\n if (gone.size === 0) break;\n for (const t of conv.turns) if (gone.has(t.seq)) size -= bytesOf(t.content);\n conv.turns = conv.turns.filter((t) => !gone.has(t.seq));\n for (const vectors of this.#vectors.values()) for (const seq of gone) vectors.delete(seq);\n }\n }\n\n /** Idle conversations, then the least recently used beyond the count, then more until the bytes fit. */\n sweep(): void {\n const limits = this.limits;\n const now = this.#now();\n for (const [id, conv] of this.#conversations) {\n if (conv.lastActivity < now - limits.idle_ttl_seconds) this.drop(id);\n }\n const byRecent = [...this.#conversations.entries()].sort((a, b) => b[1].lastActivity - a[1].lastActivity);\n for (const [id] of byRecent.slice(limits.max_conversations)) this.drop(id);\n while (this.sizeBytes() > limits.max_file_bytes) {\n const oldest = this.conversations()[0];\n if (oldest === undefined) break;\n this.drop(oldest);\n }\n }\n\n drop(conversationId: string): void {\n const conv = this.#conversations.get(conversationId);\n if (conv === undefined) return;\n for (const vectors of this.#vectors.values()) for (const t of conv.turns) vectors.delete(t.seq);\n this.#conversations.delete(conversationId);\n }\n\n clear(): void {\n this.#conversations.clear();\n this.#vectors.clear();\n }\n\n close(): void {\n // Nothing to release in memory; kept for parity with the Python store.\n }\n\n /** In conversation order: by server position where known, else as written here. */\n turns(conversationId: string): StoredTurn[] {\n const conv = this.#conversations.get(conversationId);\n if (conv === undefined) return [];\n const bySeq = [...conv.turns].sort((a, b) => a.seq - b.seq);\n const key = new Map<number, number>();\n let top = 0;\n let step = 0;\n for (const t of bySeq) {\n if (t.position !== null) {\n key.set(t.seq, t.position);\n top = Math.max(top, t.position);\n step = 0;\n } else {\n step += 1;\n key.set(t.seq, top + step * 1e-6);\n }\n }\n return bySeq\n .sort((a, b) => (key.get(a.seq) ?? 0) - (key.get(b.seq) ?? 0) || a.seq - b.seq)\n .map((t) => ({ ...t }));\n }\n\n /** Turns the server also holds: archived tool results are local only. Python: `count`. */\n count(conversationId: string): number {\n return (this.#conversations.get(conversationId)?.turns ?? []).filter((t) => t.role !== 'tool').length;\n }\n\n sizeBytes(): number {\n let n = 0;\n for (const conv of this.#conversations.values()) for (const t of conv.turns) n += bytesOf(t.content);\n return n;\n }\n\n /** Conversation ids, least recently active first. */\n conversations(): string[] {\n return [...this.#conversations.entries()].sort((a, b) => a[1].lastActivity - b[1].lastActivity).map(([id]) => id);\n }\n\n vectors(seqs: readonly number[], model: string): Map<number, Float32Array> {\n const held = this.#vectors.get(model);\n const out = new Map<number, Float32Array>();\n if (held === undefined) return out;\n for (const seq of seqs) {\n const v = held.get(seq);\n if (v !== undefined) out.set(seq, v);\n }\n return out;\n }\n\n putVectors(model: string, items: Map<number, Float32Array>): void {\n const held = this.#vectors.get(model) ?? new Map<number, Float32Array>();\n for (const [seq, vec] of items) held.set(seq, vec);\n this.#vectors.set(model, held);\n }\n}\n","/**\n * Local search over a conversation's earlier turns.\n *\n * Port of `maximem_synap/cache/history_search.py`; see it for the design.\n * Cascade, cheapest first: literal values in the query, then BM25 (the\n * anticipation cache's tokenizer and parameters), then an embedding model\n * when one is supplied. Ranks are fused by reciprocal rank with a small recency prior, and\n * excerpts are shown in conversation order. The rendered block reaches the\n * LLM prompt and is byte-identical to Python's.\n */\n\nimport { HISTORY_SEARCH } from '../behavior/contract.js';\nimport { BM25, tokenize as latinTokenize } from './bm25.js';\nimport type { StoredTurn, TurnStore } from './turn-store.js';\n\n/** A local embedding model. `model` names it, so vectors from another model are never compared. */\nexport interface Encoder {\n model: string;\n encode(texts: string[]): number[][] | Promise<number[][]>;\n /** Optional: a model trained with a query prefix (e5) encodes queries here and turns with `encode`. */\n encodeQuery?(texts: string[]): number[][] | Promise<number[][]>;\n}\n\nexport interface Excerpt {\n position: number | null;\n role: string;\n text: string;\n score: number;\n timestamp: string;\n /** \"literal\", \"bm25\" or \"embedding\": the strongest signal that found it. */\n via: string;\n}\n\n// Any script's letters and digits; Latin words still go through the\n// anticipation tokenizer for its stop words and stemming. Python: `tokenize`.\nconst WORD_RE = /[\\p{L}\\p{N}\\p{M}]+/gu;\n\nconst FORMS: Readonly<Record<string, string>> = HISTORY_SEARCH.irregular_forms ?? {};\n\nfunction tokenize(text: string): string[] {\n const out: string[] = [];\n for (const word of (text || '').toLowerCase().match(WORD_RE) ?? []) {\n // \"sold\" and \"sell\" are one word to a search; the stemmer only strips regular endings.\n if (/^[\\x00-\\x7f]+$/.test(word)) out.push(...latinTokenize(FORMS[word] ?? word));\n else out.push(word);\n }\n return out;\n}\n\nconst LITERAL_SOURCE = String.raw`\"([^\"]{3,})\"|\\b([A-Z0-9][A-Z0-9-]{3,})\\b|(\\d[\\d,.:/-]*\\d|\\d)`;\nconst SENTENCE_RE = /(?<=[.!?])\\s+/;\n\nfunction literalsOf(query: string): string[] {\n const out: string[] = [];\n for (const m of (query || '').matchAll(new RegExp(LITERAL_SOURCE, 'g'))) {\n const quoted = m[1];\n const value = quoted || m[2] || m[3];\n if (value && (quoted || /\\d/.test(value))) out.push(value.toLowerCase());\n }\n return out;\n}\n\nfunction cosine(a: ArrayLike<number>, b: ArrayLike<number>): number {\n let dot = 0;\n let na = 0;\n let nb = 0;\n const n = Math.min(a.length, b.length);\n for (let i = 0; i < n; i++) {\n dot += a[i]! * b[i]!;\n na += a[i]! * a[i]!;\n nb += b[i]! * b[i]!;\n }\n return na && nb ? dot / (Math.sqrt(na) * Math.sqrt(nb)) : 0;\n}\n\nfunction words(text: string): string[] {\n return text.split(/\\s+/).filter(Boolean);\n}\n\nfunction excerptOf(content: string, terms: Set<string>, literals: string[], maxWords: number): string {\n let sentences = content.trim().split(SENTENCE_RE).filter((s) => s.trim());\n if (sentences.length === 0) sentences = [content.trim()];\n const score = (sentence: string): number => {\n const low = sentence.toLowerCase();\n const held = new Set(tokenize(sentence));\n let n = 0;\n for (const t of terms) if (held.has(t)) n += 1;\n for (const lit of literals) if (low.includes(lit)) n += 2;\n return n;\n };\n let best = 0;\n for (let i = 1; i < sentences.length; i++) if (score(sentences[i]!) > score(sentences[best]!)) best = i;\n let lo = best;\n let hi = best;\n let count = words(sentences[best]!).length;\n for (;;) {\n let grew = false;\n for (const j of [hi + 1, lo - 1]) {\n if (j >= 0 && j < sentences.length && count + words(sentences[j]!).length <= maxWords) {\n count += words(sentences[j]!).length;\n lo = Math.min(lo, j);\n hi = Math.max(hi, j);\n grew = true;\n }\n }\n if (!grew) break;\n }\n let text = sentences.slice(lo, hi + 1).join(' ');\n if (words(text).length > maxWords) text = words(text).slice(0, maxWords).join(' ') + ' ...';\n return (lo > 0 ? '... ' : '') + text + (hi < sentences.length - 1 && !text.endsWith('...') ? ' ...' : '');\n}\n\n/**\n * The best-matching earlier turns for `query`. `exclude`: message ids or\n * exact texts already in the prompt (the recent turns).\n */\nconst FENCE = /^\\s*(?:```|~~~)/;\nconst TABLE_RULE = /^\\s*\\|?\\s*:?-{2,}/;\nconst SENTENCE = /(?<=[.!?])\\s+/;\n\n/**\n * A long turn as passages of up to about `maxWords` words: table rows carry the\n * table's header row, fenced blocks and other line-shaped text go a few lines\n * at a time, prose a few sentences at a time. Python: `split_passages`.\n */\nexport function splitPassages(content: string, maxWords: number): string[] {\n const text = (content ?? '').trim();\n if (words(text).length <= maxWords) return text ? [text] : [];\n const out: string[] = [];\n const lines = text.split('\\n');\n let i = 0;\n while (i < lines.length) {\n const line = lines[i]!;\n if (FENCE.test(line)) {\n let j = i + 1;\n while (j < lines.length && !FENCE.test(lines[j]!)) j += 1;\n out.push(...windowsOf(lines.slice(i + 1, j), maxWords));\n i = j + 1;\n continue;\n }\n if (line.trim().startsWith('|')) {\n let j = i;\n while (j < lines.length && lines[j]!.trim().startsWith('|')) j += 1;\n const rows = lines.slice(i, j).filter((r) => !TABLE_RULE.test(r)).map((r) => r.trim());\n const [header, body] = rows.length > 1 ? [rows[0]!, rows.slice(1)] : ['', rows];\n for (const row of body) {\n const both = `${header} ${row}`.trim();\n out.push(words(both).length <= Math.trunc((maxWords * 3) / 2) ? both : row);\n }\n i = j;\n continue;\n }\n let j = i;\n while (j < lines.length && lines[j]!.trim() && !FENCE.test(lines[j]!) && !lines[j]!.trim().startsWith('|')) j += 1;\n if (j === i) {\n i += 1;\n continue;\n }\n const block = lines.slice(i, j);\n if (block.length > 1 && block.every((b) => words(b).length <= 25)) {\n out.push(...windowsOf(block, maxWords));\n } else {\n const sentences = block.map((b) => b.trim()).join(' ').split(SENTENCE).filter((x) => x.trim());\n out.push(...windowsOf(sentences, maxWords));\n }\n i = j;\n }\n const kept = out.filter((x) => x.trim());\n return kept.length ? kept : [text];\n}\n\nfunction windowsOf(pieces: readonly string[], maxWords: number): string[] {\n const out: string[] = [];\n let cur: string[] = [];\n let used = 0;\n for (const raw of pieces) {\n const piece = raw.trim();\n if (!piece) continue;\n const n = words(piece).length;\n if (cur.length && used + n > maxWords) {\n out.push(cur.join(' '));\n cur = [];\n used = 0;\n }\n if (n > maxWords) {\n const w = words(piece);\n for (let k = 0; k < w.length; k += maxWords) out.push(w.slice(k, k + maxWords).join(' '));\n continue;\n }\n cur.push(piece);\n used += n;\n }\n if (cur.length) out.push(cur.join(' '));\n return out;\n}\n\n/** What search scores: a whole turn, or one passage of a long turn. */\ninterface Unit {\n seq: number;\n content: string;\n turn: number;\n first: boolean;\n last: boolean;\n /** The passage's index, -1 for a whole turn (its vector is the turn's). */\n part: number;\n}\n\n/** The value patterns of the kinds of value `query` asks for. Python: `_asked_kinds`. */\nfunction askedKinds(query: string): RegExp[] {\n const out: RegExp[] = [];\n for (const [name, spec] of Object.entries(HISTORY_SEARCH.asked_kinds ?? {})) {\n if (name.startsWith('$') || typeof spec !== 'object') continue;\n if (new RegExp(spec.query, 'i').test(query ?? '')) out.push(new RegExp(spec.value));\n }\n return out;\n}\n\n/** A question about how things started gets no recency prior. Python: `_asks_how_it_started`. */\nfunction asksHowItStarted(query: string): boolean {\n const spec = HISTORY_SEARCH.early_query;\n if (!spec?.pattern) return false;\n return new RegExp(spec.pattern, (spec.flags ?? '').includes('i') ? 'i' : '').test(query ?? '');\n}\n\nexport async function searchTurns(\n turns: readonly StoredTurn[],\n query: string,\n options: { exclude?: Iterable<string>; encoder?: Encoder | null; store?: TurnStore | null } = {},\n): Promise<Excerpt[]> {\n const params = HISTORY_SEARCH;\n const k = params.max_excerpts;\n const skip = new Set(options.exclude ?? []);\n const pool = turns.filter((t) => t.content && !skip.has(t.content) && !skip.has(t.message_id ?? ''));\n const terms = new Set(tokenize(query || ''));\n const literals = literalsOf(query);\n if (pool.length === 0 || (terms.size === 0 && literals.length === 0 && !options.encoder)) return [];\n\n const maxWords = params.max_words_per_excerpt;\n const units: Unit[] = [];\n pool.forEach((t, ti) => {\n if (!params.passages) {\n units.push({ seq: t.seq, content: t.content, turn: ti, first: true, last: true, part: -1 });\n return;\n }\n const parts = splitPassages(t.content, maxWords);\n parts.forEach((part, pi) => {\n // A one-passage turn keeps the turn's vector; passages of a longer one are stored per passage.\n units.push({\n seq: t.seq, content: part, turn: ti, first: pi === 0, last: pi === parts.length - 1,\n part: parts.length > 1 ? pi : -1,\n });\n });\n });\n\n const docs = units.map((u) => tokenize(u.content));\n const bm = new BM25(docs);\n const termList = [...terms];\n const rows = units.map((turn, i) => {\n const held = new Set(docs[i]);\n let matched = 0;\n for (const t of terms) if (held.has(t)) matched += 1;\n const low = turn.content.toLowerCase();\n let literal = 0;\n for (const lit of literals) if (low.includes(lit)) literal += 1;\n return { i, bm25: matched ? bm.score(termList, i) : 0, matched, literal };\n });\n const need = Math.min(params.min_matched_terms, Math.max(1, terms.size));\n // One query word is enough when few turns use it, not when many do.\n const rareCap = Math.max(1, Math.trunc((params.rare_term_max_share ?? 0) * units.length));\n const rare = new Set<string>();\n for (const t of terms) {\n let n = 0;\n for (const doc of docs) if (doc.includes(t)) n += 1;\n if (n > 0 && n <= rareCap) rare.add(t);\n }\n let eligible;\n if (params.passages) {\n // A passage of a turn that matches enough query words as a whole.\n const turnTerms = new Map<number, Set<string>>();\n for (const r of rows) {\n const held = turnTerms.get(units[r.i]!.turn) ?? new Set<string>();\n for (const t of docs[r.i]!) if (terms.has(t)) held.add(t);\n turnTerms.set(units[r.i]!.turn, held);\n }\n eligible = rows.filter(\n (r) =>\n r.literal > 0 ||\n (r.matched > 0 &&\n (turnTerms.get(units[r.i]!.turn)!.size >= need || docs[r.i]!.some((t) => rare.has(t)))),\n );\n } else {\n eligible = rows.filter(\n (r) => r.literal > 0 || r.matched >= need || (r.matched > 0 && docs[r.i]!.some((t) => rare.has(t))),\n );\n }\n\n const rrf = params.rrf_k;\n const fused = new Map<number, number>();\n const via = new Map<number, string>();\n for (const r of eligible) fused.set(r.i, 0);\n const add = (i: number, rank: number, how: string): void => {\n fused.set(i, (fused.get(i) ?? 0) + 1 / (rrf + rank + 1));\n if (!via.has(i)) via.set(i, how);\n };\n eligible\n .filter((r) => r.literal > 0)\n .sort((a, b) => b.literal - a.literal || a.i - b.i)\n .forEach((r, rank) => add(r.i, rank, 'literal'));\n eligible\n .filter((r) => r.bm25 > 0)\n .sort((a, b) => b.bm25 - a.bm25 || a.i - b.i)\n .forEach((r, rank) => add(r.i, rank, 'bm25'));\n\n // A question asking for a kind of value also ranks the passages holding one.\n const kinds = askedKinds(query);\n if (kinds.length) {\n rows\n .filter((r) => r.matched > 0 && kinds.some((k) => k.test(units[r.i]!.content)))\n .sort((a, b) => b.bm25 - a.bm25 || a.i - b.i)\n .forEach((r, rank) => add(r.i, rank, 'kind'));\n }\n\n // Fused on every query when a model is supplied (Python: measured 19 Oct).\n if (options.encoder) {\n const ranked = await embeddingRanks(units, query, options.encoder, options.store ?? null, params.min_cosine ?? 0.4);\n ranked.forEach(([i], rank) => add(i, rank, 'embedding'));\n }\n if (fused.size === 0) return [];\n\n const n = Math.max(pool.length - 1, 1);\n const weight = asksHowItStarted(query) ? 0 : params.recency_weight;\n // A turn ranks by its best unit; its best units are what is shown of it.\n const top = new Map<number, [number, number]>();\n const ranked = new Map<number, [number, number][]>();\n for (const [u, v] of fused) {\n const ti = units[u]!.turn;\n const score = v + (weight * (ti / n)) / (rrf + 1);\n ranked.set(ti, [...(ranked.get(ti) ?? []), [score, u]]);\n const held = top.get(ti);\n if (!held || score > held[0]) top.set(ti, [score, u]);\n }\n const best = [...top.keys()].sort((a, b) => top.get(b)![0] - top.get(a)![0] || a - b).slice(0, k);\n const perTurn = params.passages_per_turn ?? 1;\n const shown = (ti: number): string => {\n const unit = units[top.get(ti)![1]]!;\n if (!params.passages || (unit.first && unit.last)) return excerptOf(pool[ti]!.content, terms, literals, maxWords);\n const picked = [...ranked.get(ti)!]\n .sort((a, b) => b[0] - a[0] || b[1] - a[1])\n .slice(0, perTurn)\n .map(([, u]) => u)\n .sort((a, b) => a - b);\n const parts: string[] = [];\n for (const u of picked) parts.push((units[u]!.first || parts.length ? '' : '... ') + units[u]!.content);\n return parts.join(' ... ') + (units[picked[picked.length - 1]!]!.last ? '' : ' ...');\n };\n const found = new Map<number, Excerpt>();\n for (const ti of best) {\n found.set(ti, {\n position: pool[ti]!.position,\n role: pool[ti]!.role,\n timestamp: pool[ti]!.timestamp,\n score: top.get(ti)![0],\n via: via.get(top.get(ti)![1]) ?? '',\n text: shown(ti),\n });\n }\n // The turn before a hit is often what it answers or confirms; it comes along, cut to its end.\n const before = params.previous_turns ?? 0;\n const tail = params.previous_turn_words ?? 40;\n for (const i of best) {\n for (let j = i - before; j < i; j++) {\n if (j < 0 || found.has(j)) continue;\n const hit = pool[i]!.position;\n const prev = pool[j]!.position;\n if (hit && prev && hit - prev > before) continue; // the turn just before is in the prompt\n const w = words(pool[j]!.content);\n found.set(j, {\n position: prev,\n role: pool[j]!.role,\n timestamp: pool[j]!.timestamp,\n score: 0,\n via: 'previous',\n text: w.length <= tail ? w.join(' ') : '... ' + w.slice(-tail).join(' '),\n });\n }\n }\n return [...found.keys()].sort((a, b) => a - b).map((i) => found.get(i)!);\n}\n\nasync function embeddingRanks(\n pool: readonly { seq: number; content: string; part?: number }[],\n query: string,\n encoder: Encoder,\n store: TurnStore | null,\n minCosine: number,\n): Promise<[number, number][]> {\n // Stored under the turn's seq and the model's name (a passage's: name + \"#p<index>\"). Python: `_embedding_ranks`.\n const keyOf = (u: { part?: number }): string => ((u.part ?? -1) < 0 ? encoder.model : `${encoder.model}#p${u.part}`);\n const vectors = new Map<number, ArrayLike<number>>();\n if (store !== null) {\n const byKey = new Map<string, number[]>();\n pool.forEach((u, i) => byKey.set(keyOf(u), [...(byKey.get(keyOf(u)) ?? []), i]));\n for (const [key, idx] of byKey) {\n const held = store.vectors(idx.map((i) => pool[i]!.seq), key);\n for (const i of idx) {\n const v = held.get(pool[i]!.seq);\n if (v) vectors.set(i, v);\n }\n }\n }\n const missing = pool.map((_, i) => i).filter((i) => !vectors.has(i));\n if (missing.length > 0) {\n const fresh = await encoder.encode(missing.map((i) => pool[i]!.content));\n const put = new Map<string, Map<number, Float32Array>>();\n missing.forEach((i, j) => {\n const v = Float32Array.from(fresh[j] ?? []);\n vectors.set(i, v);\n const key = keyOf(pool[i]!);\n put.set(key, (put.get(key) ?? new Map()).set(pool[i]!.seq, v));\n });\n if (store) for (const [key, items] of put) store.putVectors(key, items);\n }\n const q = (await (encoder.encodeQuery ?? encoder.encode).call(encoder, [query]))[0] ?? [];\n return pool\n .map((_, i): [number, number] => [i, cosine(q, vectors.get(i) ?? [])])\n .filter(([, c]) => c >= minCosine)\n .sort((a, b) => b[1] - a[1]);\n}\n\n/**\n * How an archived tool result reads in the turn store and in excerpts: compact\n * JSON, cut to the contract's size. Python: `tool_result_text`.\n */\nexport function toolResultText(result: unknown, toolName?: string | null): string {\n let text = typeof result === 'string' ? result : JSON.stringify(result) ?? String(result);\n const limit = HISTORY_SEARCH.tool_result_max_chars ?? 16_000;\n if (text.length > limit) text = text.slice(0, limit) + ' ...[cut]';\n return `[tool result: ${toolName || 'tool'}] ${text}`;\n}\n\n/** The block appended after the compaction and the recent turns. */\nexport function renderExcerpts(excerpts: readonly Excerpt[], latest = ''): string {\n if (excerpts.length === 0) return '';\n const lines = [HISTORY_SEARCH.heading];\n for (const e of excerpts) {\n let where = e.position ? `turn ${e.position}, ${e.role}` : `${e.role}, earlier`;\n if (e.timestamp) {\n const ago = before(e.timestamp, latest);\n where += `, ${e.timestamp}` + (ago ? ` (${ago})` : '');\n }\n lines.push(`- ${where}: \"${e.text}\"`);\n }\n return lines.join('\\n');\n}\n\nconst ZONED = /(?:Z|[+-]\\d{2}:\\d{2})$/;\n\n/** Seconds since the epoch, for an ISO-8601 timestamp with a zone only. Python: `_instant`. */\nfunction instant(stamp: string): number | null {\n const text = (stamp ?? '').trim();\n if (!ZONED.test(text)) return null;\n const ms = Date.parse(text);\n return Number.isNaN(ms) ? null : ms / 1000;\n}\n\n/** \"3 days before the latest message\", or \"\" when either is unreadable. Python: `_before`. */\nfunction before(stamp: string, latest: string): string {\n const at = instant(stamp);\n const now = instant(latest);\n if (at === null || now === null || now < at) return '';\n const seconds = Math.trunc(now - at);\n for (const [unit, size] of [['day', 86_400], ['hour', 3_600], ['minute', 60]] as const) {\n if (seconds >= size) {\n const n = Math.trunc(seconds / size);\n return `${n} ${unit}${n !== 1 ? 's' : ''} before the latest message`;\n }\n }\n return 'less than a minute before the latest message';\n}\n","/**\n * Read-your-writes overlay for fetch responses.\n *\n * Ports Python's `_overlay_local_recent_turns` and `_budget_recent_turns`.\n *\n * The server's `recent_turns` reflects what it has persisted and compacted. A\n * turn recorded moments ago may not be in there yet, so a fast follow-up would\n * fetch context missing the thing the user just said. The overlay replaces\n * `recent_turns` with the locally buffered tail while leaving every summary\n * field to the server.\n *\n * On by default, matching Python. `SYNAP_ST_VERBATIM_OVERLAY` is the\n * kill-switch.\n */\n\nimport type { Json, RawContext } from './types.js';\nimport { isComplete, turnsAfter } from '../cache/short-term-store.js';\nimport type { ShortTermStore, ShortTermTurn } from '../cache/short-term-store.js';\nimport { getEnv } from '../util/env.js';\n\n/**\n * The server trims `recent_turns` to this window before returning it. The\n * overlay re-applies the same budget so a long pre-compaction local tail\n * cannot silently re-inflate the prompt past what the server would have\n * allowed.\n */\nconst MAX_TURNS = 20;\nconst TOKEN_BUDGET = 2000;\n\n/**\n * Whether to splice the local tail in.\n *\n * Default ON: this is the correctness switch (a just-written turn must surface\n * on the next fetch), separate from the cost-side `st_authoritative` flag that\n * tells the server to skip assembling short-term context at all.\n */\nexport function verbatimOverlayEnabled(configured?: boolean): boolean {\n // Resolution order matches Python's `_is_st_verbatim_overlay`: the env var\n // is an explicit override / kill-switch and wins over the config field.\n const raw = (getEnv('SYNAP_ST_VERBATIM_OVERLAY') ?? '').trim().toLowerCase();\n if (['0', 'false', 'no', 'off'].includes(raw)) return false;\n if (['1', 'true', 'yes', 'on'].includes(raw)) return true;\n return configured ?? true;\n}\n\n/** ~4 characters per token, matching the server-side heuristic. */\nfunction estimateTokens(turn: ShortTermTurn): number {\n return Math.max(1, Math.floor(String(turn.content ?? '').length / 4));\n}\n\n/**\n * Cap the tail to the server's window, then drop OLDEST turns until it fits\n * the token budget. Oldest-first because the newest turn is the one the caller\n * is most likely to be following up on.\n */\nexport function budgetRecentTurns(turns: readonly ShortTermTurn[]): ShortTermTurn[] {\n const windowed = turns.slice(-MAX_TURNS);\n let total = windowed.reduce((n, t) => n + estimateTokens(t), 0);\n while (windowed.length > 0 && total > TOKEN_BUDGET) {\n const dropped = windowed.shift() as ShortTermTurn;\n total -= estimateTokens(dropped);\n }\n return windowed;\n}\n\n/**\n * Splice the local tail into a fetch response, in place.\n *\n * Deliberately never throws: this runs inside the fetch path, and a bookkeeping\n * problem here must not fail a retrieval that otherwise succeeded.\n */\nexport function overlayLocalRecentTurns(\n response: RawContext,\n store: ShortTermStore,\n /** `null` too: callers spell \"no conversation\" as `convId || null`. */\n conversationId: string | null | undefined,\n /** `st_verbatim_overlay` from the client options. The env var still wins. */\n configured?: boolean,\n): void {\n if (conversationId === undefined || conversationId === null || conversationId === '') return;\n try {\n const entry = store.get(conversationId);\n // Cold store: nothing locally, so leave the server's response untouched.\n if (entry === null) return;\n\n const existing = (response.conversation_context ?? null) as Json | null;\n\n // The server returned a conversation_context AND the overlay is switched\n // off: leave it exactly as sent. When the server OMITTED one we still\n // splice from local, so the skip-server-ST optimisation keeps working\n // regardless of this switch.\n if (existing !== null && !verbatimOverlayEnabled(configured)) return;\n\n // Which turns the tail is built from. The local list replaces the\n // server's only when it is known to be the whole conversation and to\n // follow the same summary the server sent. Otherwise the server's turns\n // stand, and the local list adds only what comes after them. A process\n // that has just restarted holds one turn, and replacing the server's list\n // with it dropped every turn before the restart.\n const serverCompaction = existing === null ? null : existing['compaction_id'];\n const localIsWhole =\n isComplete(entry) && (!serverCompaction || serverCompaction === entry.compactionId);\n let sourceTurns: ShortTermTurn[] = entry.recentTurns;\n if (existing !== null && !localIsWhole) {\n const raw = Array.isArray(existing['recent_turns']) ? existing['recent_turns'] : [];\n const serverTurns: ShortTermTurn[] = (raw as Array<Record<string, unknown>>).map((t) => ({\n role: String(t['role'] ?? 'user'),\n content: String(t['content'] ?? ''),\n timestamp: String(t['timestamp'] ?? ''),\n }));\n sourceTurns = [...serverTurns, ...turnsAfter(entry.recentTurns, serverTurns)];\n }\n // Role, text and time only, as Python sends: the server's id for a turn is\n // bookkeeping for the store, not part of the response.\n const localTurns = budgetRecentTurns(sourceTurns).map((t) => ({\n role: t.role,\n content: t.content,\n timestamp: t.timestamp,\n }));\n\n // `x || fallback`, not `??`, matching Python: the server sends '' and {}\n // for absent fields rather than null, and those must not win over a local\n // value that actually has content.\n const serverField = (name: string): unknown =>\n existing === null ? undefined : existing[name];\n const orLocal = <T>(name: string, local: T): unknown => {\n const value = serverField(name);\n if (value === undefined || value === null || value === '') return local;\n if (typeof value === 'object' && Object.keys(value as object).length === 0) return local;\n return value;\n };\n\n // Server wins on every summary field; local only ever supplies the tail.\n response.conversation_context = {\n summary: orLocal('summary', entry.summary),\n current_state: orLocal('current_state', { ...entry.currentState }),\n key_extractions: orLocal('key_extractions', { ...entry.keyExtractions }),\n recent_turns: localTurns,\n compaction_id: orLocal('compaction_id', entry.compactionId),\n compacted_at: orLocal('compacted_at', entry.compactedAt),\n conversation_id: conversationId,\n } as NonNullable<RawContext['conversation_context']>;\n } catch {\n // Non-fatal by design. See the note above.\n }\n}\n","/**\n * Render `get_context_for_prompt` locally from the short-term store.\n *\n * A direct port of Python's `formatter/context_for_prompt.py`. It exists for\n * the same reason: when the SDK is authoritative for short-term context, a\n * warm conversation can answer this call without a cloud round trip at all,\n * which is a latency saving and a metered-call saving both.\n *\n * The three styles and their exact heading text are copied from Python\n * deliberately. A prompt assembled by one SDK and one assembled by the other\n * must be the same string, or an application that switches languages silently\n * changes what its model sees.\n */\n\nimport { CONTEXT_BLOCK_PREFIX } from '../cache/short-term-store.js';\nimport type { ShortTermEntry, ShortTermTurn } from '../cache/short-term-store.js';\nimport type { Json } from './types.js';\n\nexport const SUPPORTED_STYLES = ['structured', 'narrative', 'bullet_points'] as const;\nexport type PromptStyle = (typeof SUPPORTED_STYLES)[number];\nexport const DEFAULT_STYLE: PromptStyle = 'structured';\n\nexport interface LocalRecentMessage {\n role: string;\n content: string;\n /** ISO-8601. */\n timestamp: string;\n message_id: string;\n}\n\nexport interface LocalPromptResponse {\n formatted_context: string | null;\n available: boolean;\n is_stale: boolean;\n compression_ratio: number | null;\n validation_score: number | null;\n compaction_age_seconds: number | null;\n quality_warning: boolean;\n recent_messages: LocalRecentMessage[];\n recent_message_count: number;\n compacted_message_count: number;\n total_message_count: number;\n /**\n * The framed compaction block alone (also inside formatted_context), for a\n * framework that keeps its own message list: a system message ahead of\n * recent_messages, in place of the turns it covers.\n */\n context_block: string | null;\n compaction_id: string | null;\n [key: string]: unknown;\n}\n\nfunction stringify(value: unknown): string {\n if (value === null || value === undefined) return '';\n const t = typeof value;\n if (t === 'string' || t === 'number' || t === 'boolean') return String(value);\n try {\n return JSON.stringify(value) ?? '';\n } catch {\n return String(value);\n }\n}\n\n/** Key extractions arrive as a list, a bare string, or a single object. */\nfunction asListOfObjects(value: unknown): Record<string, unknown>[] {\n if (!value) return [];\n if (Array.isArray(value)) {\n const out: Record<string, unknown>[] = [];\n for (const v of value) {\n if (v && typeof v === 'object' && !Array.isArray(v)) out.push(v as Record<string, unknown>);\n else if (typeof v === 'string') out.push({ content: v });\n }\n return out;\n }\n if (typeof value === 'object') return [value as Record<string, unknown>];\n return [];\n}\n\nfunction itemText(item: Record<string, unknown>): string {\n return (item['content'] as string) || (item['text'] as string) || stringify(item);\n}\n\nfunction buildRecentMessages(turns: readonly ShortTermTurn[]): LocalRecentMessage[] {\n return (turns ?? []).map((t, idx) => ({\n role: t.role || 'user',\n content: t.content ?? '',\n timestamp: t.timestamp,\n message_id: t.message_id || `local-${idx}`,\n }));\n}\n\n/**\n * The SDK does not know how many messages a compaction covered: the server\n * does not put that count in the bundle. Python reports 0 here for the same\n * reason, and both must agree.\n */\nfunction compactedMessageCount(_entry: ShortTermEntry): number {\n return 0;\n}\n\n/** The v3 block, which is printed as received, once, under its own header. */\nfunction blockOf(entry: ShortTermEntry): string | null {\n if (entry.contextBlock) return entry.contextBlock.trim();\n if (entry.summary && entry.summary.startsWith(CONTEXT_BLOCK_PREFIX)) return entry.summary.trim();\n return null;\n}\n\nfunction formatStructured(entry: ShortTermEntry, recent: LocalRecentMessage[]): string {\n const parts: string[] = [];\n const block = blockOf(entry);\n if (block) {\n parts.push(block);\n if (recent.length > 0) {\n parts.push(`## Recent Turns (${recent.length})`);\n for (const m of recent) parts.push(`**${m.role}**: ${m.content}`);\n }\n return parts.filter(Boolean).join('\\n\\n').trim();\n }\n if (entry.summary) { parts.push('## Summary'); parts.push(entry.summary.trim()); }\n if (entry.factualParagraph) { parts.push('## Facts'); parts.push(entry.factualParagraph.trim()); }\n if (entry.conversationalParagraph) {\n parts.push('## Conversation'); parts.push(entry.conversationalParagraph.trim());\n }\n\n const cs = entry.currentState ?? {};\n if (Object.keys(cs).length > 0) {\n parts.push('## Current State');\n for (const [k, v] of Object.entries(cs)) parts.push(`- ${k}: ${stringify(v)}`);\n }\n\n const ke = entry.keyExtractions ?? {};\n if (Object.keys(ke).length > 0) {\n parts.push('## Key Extractions');\n for (const [cat, items] of Object.entries(ke)) {\n if (!items) continue;\n parts.push(`### ${cat}`);\n for (const it of asListOfObjects(items)) parts.push(`- ${itemText(it)}`);\n }\n }\n\n if (recent.length > 0) {\n parts.push(`## Recent Turns (${recent.length})`);\n for (const m of recent) parts.push(`**${m.role}**: ${m.content}`);\n }\n\n return parts.filter(Boolean).join('\\n\\n').trim();\n}\n\nfunction formatNarrative(entry: ShortTermEntry, recent: LocalRecentMessage[]): string {\n const parts: string[] = [];\n const block = blockOf(entry);\n if (block) parts.push(block);\n else if (entry.summary) parts.push(entry.summary.trim());\n if (!block && entry.conversationalParagraph) parts.push(entry.conversationalParagraph.trim());\n else if (!block && entry.factualParagraph) parts.push(entry.factualParagraph.trim());\n\n if (recent.length > 0) {\n const tail = recent.map((m) => `${m.role}: ${m.content}`).join('\\n');\n parts.push(`Recent exchanges:\\n${tail}`);\n }\n return parts.filter(Boolean).join('\\n\\n').trim();\n}\n\nfunction formatBullets(entry: ShortTermEntry, recent: LocalRecentMessage[]): string {\n const parts: string[] = [];\n const block = blockOf(entry);\n if (block) {\n // The block is structured text already; bulleting it would mangle it.\n return [block, ...recent.map((m) => `- ${m.role}: ${m.content}`)].join('\\n').trim();\n }\n if (entry.summary) parts.push(`- Summary: ${entry.summary.trim()}`);\n\n for (const [k, v] of Object.entries(entry.currentState ?? {})) {\n parts.push(`- ${k}: ${stringify(v)}`);\n }\n for (const [cat, items] of Object.entries(entry.keyExtractions ?? {})) {\n for (const it of asListOfObjects(items)) parts.push(`- (${cat}) ${itemText(it)}`);\n }\n for (const m of recent) parts.push(`- ${m.role}: ${m.content}`);\n\n return parts.join('\\n').trim();\n}\n\nfunction formatBlock(entry: ShortTermEntry, recent: LocalRecentMessage[], style: string): string {\n if (style === 'narrative') return formatNarrative(entry, recent);\n if (style === 'bullet_points') return formatBullets(entry, recent);\n return formatStructured(entry, recent);\n}\n\n/** Build a context-for-prompt response from a cached entry. */\nexport function renderForPrompt(\n entry: ShortTermEntry,\n style: string = DEFAULT_STYLE,\n now: () => number = Date.now,\n): LocalPromptResponse {\n const chosen: string = (SUPPORTED_STYLES as readonly string[]).includes(style)\n ? style\n : DEFAULT_STYLE;\n\n const recent = buildRecentMessages(entry.recentTurns);\n const formatted = formatBlock(entry, recent, chosen);\n const available = entry.compactionId !== null || entry.recentTurns.length > 0;\n\n let compactionAge: number | null = null;\n if (entry.compactedAt !== null) {\n const parsed = Date.parse(entry.compactedAt);\n if (!Number.isNaN(parsed)) compactionAge = Math.floor((now() - parsed) / 1000);\n }\n\n const compacted = compactedMessageCount(entry);\n return {\n formatted_context: available ? formatted : null,\n available,\n is_stale: false,\n compression_ratio: null,\n validation_score: null,\n compaction_age_seconds: compactionAge,\n quality_warning: false,\n recent_messages: recent,\n recent_message_count: recent.length,\n compacted_message_count: compacted,\n total_message_count: recent.length + compacted,\n context_block: blockOf(entry),\n compaction_id: entry.compactionId,\n };\n}\n\n/** Build a compaction response from a cached entry, or null if none landed. */\nexport function renderCompacted(\n entry: ShortTermEntry,\n format = 'structured',\n correlationId = '',\n): Json | null {\n if (entry.compactionId === null) return null;\n\n const formatted = formatBlock(\n entry,\n buildRecentMessages(entry.recentTurns),\n format === 'narrative' ? 'narrative' : 'structured',\n );\n\n return {\n compacted_context: formatted,\n compaction_id: entry.compactionId,\n compacted_at: entry.compactedAt,\n conversation_id: entry.conversationId,\n current_state: entry.currentState,\n correlation_id: correlationId,\n original_token_count: null,\n compacted_token_count: null,\n compression_ratio: null,\n level_applied: null,\n validation_score: null,\n validation_passed: null,\n quality_warning: false,\n };\n}\n","/**\n * Periodic user-summary injection.\n *\n * Ports Python's `_should_inject_user_summary` / `_merge_user_summary_into_response`.\n *\n * Every Nth turn, a cached `user_summary` bundle is folded into the fetch\n * response. It is what keeps a long conversation grounded in who the user is\n * without paying for a broader retrieval on every turn. Without it, JS returns\n * less context than Python from identical usage, and the gap widens the longer\n * the conversation runs.\n */\n\nimport type { Json, RawContext, RawContextItem } from './types.js';\n\n/** Python's `_user_summary_interval`. */\nexport const USER_SUMMARY_INTERVAL = 5;\n/** Python's `max_summary_items`. */\nconst MAX_SUMMARY_ITEMS = 3;\n\nconst COLLECTIONS = ['facts', 'preferences', 'episodes', 'emotions', 'temporal_events'] as const;\n\n/** Per-conversation turn counters, so the cadence is per conversation. */\nexport class TurnCounter {\n readonly #counts = new Map<string, number>();\n\n /** Increment and return the new count. Python buckets a missing id under `_global`. */\n increment(conversationId: string | undefined): number {\n const key = conversationId !== undefined && conversationId !== '' ? conversationId : '_global';\n const next = (this.#counts.get(key) ?? 0) + 1;\n this.#counts.set(key, next);\n return next;\n }\n\n /**\n * True on every Nth turn, and never on the first.\n *\n * Python: `count > 0 and count % interval == 0`. Turn 5 injects, turn 1 does\n * not, which matters because the first turn is usually the one that just\n * triggered a fresh retrieval anyway.\n */\n shouldInject(conversationId: string | undefined): boolean {\n const key = conversationId !== undefined && conversationId !== '' ? conversationId : '_global';\n const count = this.#counts.get(key) ?? 0;\n return count > 0 && count % USER_SUMMARY_INTERVAL === 0;\n }\n\n clear(): void {\n this.#counts.clear();\n }\n}\n\nfunction itemId(item: Json): string {\n return String(item['item_id'] ?? item['id'] ?? '');\n}\n\nfunction pick(item: Json, ...names: string[]): unknown {\n for (const name of names) {\n const value = item[name];\n if (value !== undefined && value !== null) return value;\n }\n return undefined;\n}\n\n/**\n * Normalise a summary-bundle item to the response item shape.\n *\n * The bundle carries the proto's field names, which differ from the REST\n * response's. Python spells the whole mapping out rather than passing items\n * through, because a consumer reading `.content` on an episode would otherwise\n * get undefined.\n */\nfunction normalise(item: Json): RawContextItem {\n const content = pick(item, 'content') ?? '';\n const confidence = pick(item, 'confidence') ?? 0;\n const extractedAt = pick(item, 'extracted_at', 'created_at') ?? null;\n return {\n id: itemId(item),\n content: String(content),\n confidence: Number(confidence),\n source: String(pick(item, 'source') ?? ''),\n category: String(pick(item, 'context_type', 'category') ?? ''),\n emotion_type: String(pick(item, 'context_type', 'emotion_type') ?? ''),\n strength: Number(pick(item, 'strength', 'confidence') ?? 0),\n summary: String(pick(item, 'summary', 'content') ?? ''),\n significance: Number(pick(item, 'significance', 'confidence') ?? 0),\n intensity: Number(pick(item, 'intensity', 'confidence') ?? 0),\n context: String(pick(item, 'context', 'content') ?? ''),\n participants: (pick(item, 'participants') ?? []) as unknown[],\n extracted_at: extractedAt as string | null,\n occurred_at: (pick(item, 'occurred_at', 'extracted_at', 'created_at') ?? null) as string | null,\n detected_at: (pick(item, 'detected_at', 'extracted_at', 'created_at') ?? null) as string | null,\n metadata: (pick(item, 'metadata') ?? {}) as Json,\n event_date: (pick(item, 'event_date') ?? null) as string | null,\n said_at: (pick(item, 'said_at') ?? null) as string | null,\n valid_until: (pick(item, 'valid_until') ?? null) as string | null,\n temporal_category: (pick(item, 'temporal_category') ?? null) as string | null,\n temporal_confidence: Number(pick(item, 'temporal_confidence') ?? 0),\n };\n}\n\n/**\n * Fold a cached summary bundle into a response, in place.\n *\n * Appends at most `MAX_SUMMARY_ITEMS` per collection, skipping anything whose\n * id the response already carries: the summary supplements a retrieval, it does\n * not replace or duplicate it.\n */\nexport function mergeUserSummary(response: RawContext, bundle: Json): RawContext {\n const itemsByType = (bundle['items_by_type'] ?? {}) as Record<string, unknown>;\n for (const collection of COLLECTIONS) {\n const items = itemsByType[collection];\n if (!Array.isArray(items)) continue;\n\n const existing = (response[collection] ?? []) as RawContextItem[];\n const existingIds = new Set(existing.map((i) => String(i.id ?? i.item_id ?? '')));\n const fresh = (items as Json[])\n .filter((i) => typeof i === 'object' && i !== null && !existingIds.has(itemId(i)))\n .slice(0, MAX_SUMMARY_ITEMS)\n .map(normalise);\n if (fresh.length === 0) continue;\n\n response[collection] = [...existing, ...fresh];\n }\n return response;\n}\n","/**\n * Serving a fetch from the anticipation cache.\n *\n * Ports Python's `_build_anticipation_response` and the cache-first branch each\n * scope fetch runs before going to the network.\n *\n * This is the point of the whole anticipation feature. The gRPC stream pushes\n * bundles ahead of time; without a lookup on the read path the cache is\n * write-only, every fetch is a billed round trip, and the stream does nothing\n * but consume bandwidth. That was the state of this SDK before: `store()` was\n * wired, `lookup()` was never called.\n */\n\nimport type { Json, RawContext } from './types.js';\nimport type { AnticipationCache, LookupResult } from './anticipation-cache.js';\n\nexport type Scope = 'conversation' | 'user' | 'customer' | 'client';\n\nexport interface AnticipationLookupParams {\n // `| undefined` explicitly, because exactOptionalPropertyTypes distinguishes\n // an absent key from one set to undefined, and callers build these by\n // spreading optionals.\n searchQuery?: readonly string[] | null | undefined;\n /** The user. Absent for customer- and client-scope fetches. */\n entityId?: string | null | undefined;\n customerId?: string | null | undefined;\n clientId?: string | null | undefined;\n conversationId?: string | null | undefined;\n /**\n * The rung the request names, from its `scope_path`. Passed through every\n * scope unchanged: unlike the ids below it, a rung never widens, so there is\n * no scope for which dropping it would be the safe thing to do.\n */\n scopeRung?: string | null | undefined;\n}\n\n/**\n * Which identifiers a scope may widen its lookup across.\n *\n * A customer-scope request widens to client-shared bundles but explicitly NOT\n * to user-scoped ones: those would leak one visitor's data into another\n * visitor's customer fetch. Client scope narrows furthest for the same reason.\n * This mirrors the scoping notes on Python's four fetch methods.\n */\nexport function scopeLookupParams(\n scope: Scope,\n params: AnticipationLookupParams,\n): AnticipationLookupParams {\n const { searchQuery, entityId, customerId, clientId, conversationId, scopeRung } = params;\n switch (scope) {\n case 'conversation':\n return { searchQuery, entityId, customerId, clientId, conversationId, scopeRung };\n case 'user':\n // No conversationId: a user-scope fetch is not bound to one thread.\n return { searchQuery, entityId, customerId, clientId, scopeRung };\n case 'customer':\n return { searchQuery, entityId: null, customerId, clientId, scopeRung };\n case 'client':\n return { searchQuery, entityId: null, customerId: null, clientId, scopeRung };\n }\n}\n\n/** Turn a cache hit into the same raw shape the network path returns. */\nexport function buildAnticipationResponse(hit: LookupResult): RawContext {\n const response: RawContext = {};\n for (const [type, items] of Object.entries(hit.itemsByType)) {\n (response as Json)[type] = items;\n }\n // `source: \"anticipation\"` is how a caller (and the metering that reads it)\n // can tell a locally served fetch from a billed one.\n response.metadata = {\n source: 'anticipation',\n ttl_seconds: 0,\n cache_hit: true,\n retrieved_at: new Date().toISOString(),\n };\n return response;\n}\n\n/** Every item id in a hit, for the `context_used` event's `served_item_ids`. */\nexport function servedItemIds(hit: LookupResult): string[] {\n const ids: string[] = [];\n for (const items of Object.values(hit.itemsByType)) {\n for (const item of items) {\n const id = item.item_id ?? (item as Json)['id'];\n if (typeof id === 'string' && id !== '') ids.push(id);\n }\n }\n return ids;\n}\n\nexport interface AnticipationAttempt {\n /** The response to return, or null to fall through to the network. */\n response: RawContext | null;\n hit: LookupResult | null;\n}\n\n/**\n * Try to serve this fetch locally.\n *\n * Deliberately swallows its own errors: a fault in the cache must degrade to a\n * network fetch, never fail a retrieval that the server could have answered.\n */\nexport async function tryServeFromCache(\n cache: AnticipationCache,\n scope: Scope,\n params: AnticipationLookupParams,\n maxItems: number,\n): Promise<AnticipationAttempt> {\n try {\n const scoped = scopeLookupParams(scope, params);\n const hit = await cache.lookupAsync({\n searchQuery: scoped.searchQuery ?? null,\n entityId: scoped.entityId ?? null,\n customerId: scoped.customerId ?? null,\n clientId: scoped.clientId ?? null,\n conversationId: scoped.conversationId ?? null,\n scopeRung: scoped.scopeRung ?? null,\n maxItems,\n });\n if (hit === null) return { response: null, hit: null };\n return { response: buildAnticipationResponse(hit), hit };\n } catch {\n return { response: null, hit: null };\n }\n}\n\n/**\n * How many distinct, non-empty search queries a fetch carried, for the\n * `context_used` event's `query_count`. Matches the Python SDK and the count\n * the server itself uses when it answers a fetch: the same query sent twice is\n * one, and an empty string is none.\n */\nexport function distinctQueryCount(searchQuery: unknown): number {\n if (!Array.isArray(searchQuery)) return 0;\n return new Set(searchQuery.filter((q) => typeof q === 'string' && q.length > 0)).size;\n}\n","/**\n * `context_assembled`: what the SDK actually composed for this turn.\n *\n * Ports Python's `_emit_context_assembled_event`. Fires after every fetch\n * resolves, whether it was served from the cache, the local short-term store or\n * the network, and records what the caller actually received. That can differ\n * from what the server returned, because of SDK-side trimming, the overlay, or\n * a cache hit that skipped the server entirely.\n *\n * It drives the Requests-page audit. The server backfills a synthetic row if no\n * event arrives within its window, so a missing event is not silently lost, it\n * just produces a less accurate one.\n *\n * Privacy: ids, counts and assembly metadata only. Never prompt or item content.\n */\n\nimport type { Json, RawContext } from './types.js';\n\nconst COLLECTIONS = ['facts', 'preferences', 'episodes', 'emotions', 'temporal_events'] as const;\n\nexport interface AssembledEvent extends Json {\n correlation_id: string;\n conversation_id: string;\n user_id: string;\n customer_id: string;\n final_item_ids: string[];\n final_total_tokens: number;\n compaction_id: string;\n recent_turn_count: number;\n compaction_end_timestamp: string;\n assembly_source: string;\n assembly_duration_ms: number;\n cache_hit: boolean;\n sdk_version: string;\n}\n\n/** Every item id the caller ended up with, across all five collections. */\nexport function finalItemIds(response: RawContext): string[] {\n const ids: string[] = [];\n for (const collection of COLLECTIONS) {\n const items = response[collection];\n if (!Array.isArray(items)) continue;\n for (const item of items) {\n const id = (item as Json)['id'] ?? (item as Json)['item_id'];\n if (typeof id === 'string' && id !== '') ids.push(id);\n }\n }\n return ids;\n}\n\n/**\n * Normalise the assembly source and derive `cache_hit` from it.\n *\n * `ResponseMetadata` uses `cache` as shorthand for the local HTTP cache; the\n * proto wants the explicit name. Three sources imply a hit regardless of what\n * the metadata's own flag says, because in each the server was not consulted.\n */\nexport function resolveSource(\n response: RawContext,\n override?: string,\n): { source: string; cacheHit: boolean } {\n const metadata = (response.metadata ?? {}) as Json;\n let source = override ?? String(metadata['source'] ?? '') ?? '';\n if (source === '') source = 'cloud';\n if (source === 'cache') source = 'http_cache';\n const impliesHit = ['anticipation', 'anticipation_cache', 'http_cache', 'sdk_authoritative'];\n const cacheHit = impliesHit.includes(source) ? true : Boolean(metadata['cache_hit']);\n return { source, cacheHit };\n}\n\nexport function buildAssembledEvent(params: {\n correlationId: string;\n response: RawContext;\n conversationId?: string | undefined;\n userId?: string | undefined;\n customerId?: string | undefined;\n startedAt: number;\n sdkVersion: string;\n sourceOverride?: string | undefined;\n}): AssembledEvent {\n const { response } = params;\n const { source, cacheHit } = resolveSource(response, params.sourceOverride);\n const conversationContext = (response.conversation_context ?? {}) as Json;\n const recentTurns = conversationContext['recent_turns'];\n const metadata = (response.metadata ?? {}) as Json;\n\n return {\n correlation_id: params.correlationId,\n conversation_id: params.conversationId ?? '',\n user_id: params.userId ?? '',\n customer_id: params.customerId ?? '',\n final_item_ids: finalItemIds(response),\n final_total_tokens: Number(metadata['total_tokens'] ?? 0),\n compaction_id: String(conversationContext['compaction_id'] ?? ''),\n recent_turn_count: Array.isArray(recentTurns) ? recentTurns.length : 0,\n compaction_end_timestamp: String(\n conversationContext['end_timestamp'] ?? conversationContext['compacted_at'] ?? '',\n ),\n assembly_source: source,\n assembly_duration_ms: Math.max(0, Date.now() - params.startedAt),\n cache_hit: cacheHit,\n sdk_version: params.sdkVersion,\n };\n}\n","/**\n * Cross-scope merge + prompt formatting for `client.fetch()`.\n *\n * Ports Python's `UnifiedContextResponse.merge` and `format_for_prompt` from\n * `models/context.py`. The formatted string is the part customers actually\n * paste into a system prompt, so it is reproduced character for character:\n * section order, the `- ` bullet, the `[scope: x]` / `strength: 0.8` bracket\n * suffixes, the `## User Context` wrapper, and the blank-line joins. A\n * cosmetic drift here shows up as a diff in every prompt a customer builds.\n */\n\nimport { rankedLabel, sortByRankScore } from './ranked.js';\nimport type { Json, RawContext, RawContextItem } from './types.js';\n\n/** The five item collections, in Python's iteration order. */\nexport const ITEM_COLLECTIONS = [\n 'facts',\n 'preferences',\n 'episodes',\n 'emotions',\n 'temporal_events',\n] as const;\n\nexport type ItemCollection = (typeof ITEM_COLLECTIONS)[number];\n\nexport interface UnifiedContext {\n facts: RawContextItem[];\n preferences: RawContextItem[];\n episodes: RawContextItem[];\n emotions: RawContextItem[];\n temporal_events: RawContextItem[];\n /**\n * `response_shape: \"ranked\"` only: every item once, best first across\n * scopes and types, renumbered from 1. Empty for a by-type fetch.\n */\n items: RawContextItem[];\n /** `response_shape: \"ranked\"` only: the chunks (quotes from source documents). */\n excerpts: RawContextItem[];\n /** item id -> the scope that supplied it. */\n scope_map: Record<string, string>;\n conversation_context: Json | null;\n profile: Json | null;\n conversations: Json[] | null;\n formatted_context: string;\n scopes_queried: string[];\n total_items: number;\n metadata: Json | null;\n}\n\nfunction itemId(item: RawContextItem): string | undefined {\n const id = item.id ?? item.item_id;\n return typeof id === 'string' && id !== '' ? id : undefined;\n}\n\n/**\n * Merge per-scope responses, deduplicating by item id.\n *\n * First scope wins on a duplicate, so the caller's scope order is significant.\n * An item with no usable id is kept rather than dropped: Python indexes\n * `scope_map` by `item.id`, and a missing id there would collide every such\n * item onto one key, so keeping-and-not-mapping is the closest faithful\n * behaviour.\n */\nexport function mergeScopeResults(\n scopeResults: ReadonlyArray<readonly [string, RawContext]>,\n): UnifiedContext {\n const seen = new Set<string>();\n const out: UnifiedContext = {\n facts: [], preferences: [], episodes: [], emotions: [], temporal_events: [],\n items: [], excerpts: [],\n scope_map: {},\n conversation_context: null,\n profile: null,\n conversations: null,\n formatted_context: '',\n scopes_queried: [],\n total_items: 0,\n metadata: null,\n };\n\n const rankedSeen = new Set<string>();\n const rankedItems: RawContextItem[] = [];\n\n for (const [scopeName, response] of scopeResults) {\n out.scopes_queried.push(scopeName);\n if (out.metadata === null && response.metadata !== undefined) {\n out.metadata = response.metadata as Json;\n }\n // The C4 summary sections ride up from whichever scope produced them,\n // which is always the user-scope sub-fetch. See the fan-out rule in fetch().\n if (out.profile === null && response['profile'] !== undefined && response['profile'] !== null) {\n out.profile = response['profile'] as Json;\n }\n if (\n out.conversations === null &&\n response['conversations'] !== undefined &&\n response['conversations'] !== null\n ) {\n out.conversations = response['conversations'] as Json[];\n }\n\n for (const collection of ITEM_COLLECTIONS) {\n const items = response[collection];\n if (!Array.isArray(items)) continue;\n for (const item of items as RawContextItem[]) {\n const id = itemId(item);\n if (id !== undefined) {\n if (seen.has(id)) continue;\n seen.add(id);\n out.scope_map[id] = scopeName;\n }\n out[collection].push(item);\n }\n }\n\n // Ranked responses only, and in Python's order: excerpts share the typed\n // lists' dedup and scope map; the ranked items have a dedup of their own,\n // because they are the same items again, in order.\n const excerpts = response['excerpts'];\n if (Array.isArray(excerpts)) {\n for (const item of excerpts as RawContextItem[]) {\n const id = itemId(item);\n if (id !== undefined) {\n if (seen.has(id)) continue;\n seen.add(id);\n out.scope_map[id] = scopeName;\n }\n out.excerpts.push(item);\n }\n }\n const ranked = response['items'];\n if (Array.isArray(ranked)) {\n for (const item of ranked as RawContextItem[]) {\n const id = itemId(item);\n if (id !== undefined) {\n if (rankedSeen.has(id)) continue;\n rankedSeen.add(id);\n }\n rankedItems.push(item);\n }\n }\n }\n\n // One order across scopes, by each item's rank_score. The sort is stable, so\n // equal scores keep scope order, as Python's does, and unscored items go last.\n out.items = sortByRankScore(rankedItems).map((item, n) => ({ ...item, rank: n + 1 }));\n out.total_items =\n ITEM_COLLECTIONS.reduce((n, c) => n + out[c].length, 0) + out.excerpts.length;\n return out;\n}\n\n/** Python's `f\"{x:.1f}\"`. */\nfunction oneDecimal(value: number): string {\n return value.toFixed(1);\n}\n\n/** Python's `datetime.strftime(\"%Y-%m-%d\")` on an ISO string. */\nfunction isoDate(value: unknown): string | null {\n if (typeof value !== 'string' || value === '') return null;\n const parsed = new Date(value);\n if (Number.isNaN(parsed.getTime())) return null;\n return parsed.toISOString().slice(0, 10);\n}\n\nfunction numberOf(value: unknown): number | null {\n return typeof value === 'number' && Number.isFinite(value) ? value : null;\n}\n\nfunction scopeOf(ctx: UnifiedContext, item: RawContextItem): string {\n const id = itemId(item);\n return (id !== undefined ? ctx.scope_map[id] : undefined) ?? 'unknown';\n}\n\n/** `- text [a, b]`, with the bracket omitted when there are no extras. */\nfunction bulletWithExtras(text: string, extras: string[]): string {\n return extras.length > 0 ? `- ${text} [${extras.join(', ')}]` : `- ${text}`;\n}\n\nexport interface FormatOptions {\n includeScope?: boolean;\n includeConversationContext?: boolean;\n}\n\n/**\n * Render the merged context for prompt injection.\n *\n * Mirrors `UnifiedContextResponse.format_for_prompt`. Note the two different\n * bracket styles: facts and episodes only ever carry `[scope: x]` appended\n * directly, while preferences, emotions and temporal events build a joined\n * extras list. That asymmetry is in Python and is preserved.\n */\nexport function formatForPrompt(ctx: UnifiedContext, options: FormatOptions = {}): string {\n const includeScope = options.includeScope ?? false;\n const includeConversationContext = options.includeConversationContext ?? true;\n const sections: string[] = [];\n\n // A ranked response is one numbered list in rank order, as in Python: the\n // typed sections below would split the order back up by type.\n const ranked = ctx.items ?? [];\n if (ranked.length > 0) {\n const lines = ranked.map((it, n) => {\n let label = rankedLabel(String(it['category'] ?? ''));\n if (includeScope) label += `, scope: ${scopeOf(ctx, it)}`;\n return `${n + 1}. [${label}] ${String(it.content ?? '')}`;\n });\n sections.push(`### Memories\\n${lines.join('\\n')}`);\n }\n const typed = ranked.length === 0;\n\n if (typed && ctx.facts.length > 0) {\n const lines = ctx.facts.map((f) => {\n let line = `- ${String(f.content ?? '')}`;\n if (includeScope) line += ` [scope: ${scopeOf(ctx, f)}]`;\n return line;\n });\n sections.push(`### Facts\\n${lines.join('\\n')}`);\n }\n\n if (typed && ctx.preferences.length > 0) {\n const lines = ctx.preferences.map((p) => {\n const extras: string[] = [];\n if (includeScope) extras.push(`scope: ${scopeOf(ctx, p)}`);\n const strength = numberOf(p['strength']);\n // Python: `if p.strength and p.strength >= 0.7` -- 0 is falsy there, so a\n // zero strength is omitted rather than printed.\n if (strength !== null && strength !== 0 && strength >= 0.7) {\n extras.push(`strength: ${oneDecimal(strength)}`);\n }\n return bulletWithExtras(String(p.content ?? ''), extras);\n });\n sections.push(`### Preferences\\n${lines.join('\\n')}`);\n }\n\n if (typed && ctx.episodes.length > 0) {\n const lines = ctx.episodes.map((e) => {\n // Episodes render `summary`, not `content`.\n let line = `- ${String(e['summary'] ?? '')}`;\n if (includeScope) line += ` [scope: ${scopeOf(ctx, e)}]`;\n return line;\n });\n sections.push(`### Episodes\\n${lines.join('\\n')}`);\n }\n\n if (typed && ctx.emotions.length > 0) {\n const lines = ctx.emotions.map((em) => {\n const extras: string[] = [];\n if (includeScope) extras.push(`scope: ${scopeOf(ctx, em)}`);\n const intensity = numberOf(em['intensity']);\n if (intensity !== null && intensity !== 0 && intensity >= 0.5) {\n extras.push(`intensity: ${oneDecimal(intensity)}`);\n }\n // Emotions render \"type: context\", not a bare content field.\n const text = `${String(em['emotion_type'] ?? '')}: ${String(em['context'] ?? '')}`;\n return bulletWithExtras(text, extras);\n });\n sections.push(`### Emotions\\n${lines.join('\\n')}`);\n }\n\n if (typed && ctx.temporal_events.length > 0) {\n const lines = ctx.temporal_events.map((te) => {\n const extras: string[] = [];\n if (includeScope) extras.push(`scope: ${scopeOf(ctx, te)}`);\n const validUntil = isoDate(te['valid_until']);\n if (validUntil !== null) extras.push(`valid until: ${validUntil}`);\n return bulletWithExtras(String(te.content ?? ''), extras);\n });\n sections.push(`### Temporal Events\\n${lines.join('\\n')}`);\n }\n\n if (includeConversationContext && ctx.conversation_context !== null) {\n const formatted = ctx.conversation_context['formatted_context'];\n if (typeof formatted === 'string' && formatted !== '') {\n sections.push(`### Conversation History\\n${formatted}`);\n }\n }\n\n const blocks: string[] = [];\n if (sections.length > 0) blocks.push(`## User Context\\n${sections.join('\\n\\n')}`);\n\n const profileBlock = formatProfileBlock(ctx.profile);\n if (profileBlock !== null) blocks.push(profileBlock);\n\n const conversationsBlock = formatConversationsBlock(ctx.conversations);\n if (conversationsBlock !== null) blocks.push(conversationsBlock);\n\n return blocks.length > 0 ? blocks.join('\\n\\n') : '';\n}\n\n/** `## Caller Profile`, or null when there is nothing to show. */\nfunction formatProfileBlock(profile: Json | null): string | null {\n if (profile === null) return null;\n const attributes = profile['attributes'];\n const lines: string[] = [];\n if (typeof attributes === 'object' && attributes !== null) {\n for (const [name, attr] of Object.entries(attributes as Json)) {\n const value =\n typeof attr === 'object' && attr !== null\n ? (attr as Json)['value']\n : undefined;\n let valueStr: string;\n if (Array.isArray(value)) valueStr = value.map((v) => String(v)).join(', ');\n else if (value === null || value === undefined) valueStr = '';\n else valueStr = String(value);\n // Python does `f\"- {name}: {value}\".rstrip()`, so an empty value leaves\n // \"- name:\" with no trailing space.\n lines.push(`- ${name}: ${valueStr}`.replace(/\\s+$/, ''));\n }\n }\n const bodyParts: string[] = [];\n if (lines.length > 0) bodyParts.push(lines.join('\\n'));\n const overview = profile['overview'];\n if (typeof overview === 'string' && overview !== '') bodyParts.push(overview);\n if (bodyParts.length === 0) return null;\n return `## Caller Profile\\n${bodyParts.join('\\n\\n')}`;\n}\n\n/** `## Previous Conversations`, or null. */\nfunction formatConversationsBlock(conversations: Json[] | null): string | null {\n if (conversations === null || conversations.length === 0) return null;\n const callBlocks = conversations.map((conv) => {\n const when =\n isoDate(conv['started_at']) ?? isoDate(conv['last_message_at']) ?? isoDate(conv['ended_at']);\n let header = `### Call on ${when ?? 'unknown date'}`;\n const convType = conv['conversation_type'];\n if (typeof convType === 'string' && convType !== '') header += ` (${convType})`;\n const callLines = [header];\n const overview = overviewText(conv);\n if (overview !== null) callLines.push(`Overview: ${overview}`);\n const outcome = outcomeText(conv);\n if (outcome !== null) callLines.push(`Outcome: ${outcome}`);\n if (overview === null && outcome === null) {\n // No compaction yet, or it failed. Still tell the model the call\n // happened so it can acknowledge the prior contact.\n // Python's model defaults summary_status to \"pending\", so an absent\n // field renders as \"pending\", not as an empty string.\n callLines.push(`Status: ${String(conv['summary_status'] ?? 'pending')}`);\n }\n return callLines.join('\\n');\n });\n return `## Previous Conversations\\n${callBlocks.join('\\n\\n')}`;\n}\n\n/**\n * Mirrors `ConversationSummaryModel.overview_text()`.\n *\n * Reads the nested `summary` OBJECT (not a string) and takes the first of four\n * keys that holds non-blank text, in this exact order.\n */\nfunction overviewText(conv: Json): string | null {\n const summary = conv['summary'];\n if (typeof summary !== 'object' || summary === null || Array.isArray(summary)) return null;\n for (const key of ['narrative_summary', 'overview', 'narrative', 'summary'] as const) {\n const val = (summary as Json)[key];\n if (typeof val === 'string' && val.trim() !== '') return val.trim();\n }\n return null;\n}\n\n/**\n * Mirrors `ConversationSummaryModel.outcome_text()`.\n *\n * Joins up to three classification fields with \" / \", skipping blanks, and\n * returns null when nothing survives.\n */\nfunction outcomeText(conv: Json): string | null {\n const classification = conv['classification'];\n if (typeof classification !== 'object' || classification === null || Array.isArray(classification)) {\n return null;\n }\n const c = classification as Json;\n const parts = [c['primary_category'], c['subcategory'], c['objective']]\n .filter((p): p is string => typeof p === 'string' && p.trim() !== '');\n return parts.length > 0 ? parts.join(' / ') : null;\n}\n","/**\n * `client.as_tool(...)`: an LLM-ready tool definition for fetching context.\n *\n * Ports Python's `MaximemSynapSDK.as_tool` plus its three module helpers.\n *\n * The point of the helper is that the scope identifiers are CLOSED OVER rather\n * than exposed in the JSON schema. The LLM chooses the query and the limits; it\n * cannot choose whose memory to read. Handing the model a `user_id` parameter\n * is how the per-user privacy filter gets dropped by accident, so the schema\n * deliberately has no such field.\n */\n\nimport { InvalidInputError } from '../errors.js';\nimport type { Json } from '../context/types.js';\n\nexport type ToolScope = 'conversation' | 'user' | 'customer' | 'client' | 'unified';\nexport type ToolStyle = 'openai' | 'anthropic';\n\nconst VALID_SCOPES: readonly ToolScope[] = ['conversation', 'user', 'customer', 'client', 'unified'];\n\nexport interface AsToolOptions {\n scope?: string;\n user_id?: string;\n userId?: string;\n customer_id?: string;\n customerId?: string;\n conversation_id?: string;\n conversationId?: string;\n name?: string;\n description?: string;\n style?: string;\n}\n\nexport type ToolHandler = (callArgs?: Json) => Promise<Json>;\n\nexport interface OpenAiToolDefinition {\n type: 'function';\n function: { name: string; description: string; parameters: Json };\n handler: ToolHandler;\n}\n\nexport interface AnthropicToolDefinition {\n name: string;\n description: string;\n input_schema: Json;\n handler: ToolHandler;\n}\n\nexport type ToolDefinition = OpenAiToolDefinition | AnthropicToolDefinition;\n\nconst BASE_DESCRIPTION =\n 'Retrieve stored context (facts, preferences, recent episodes, ' +\n 'emotions, temporal events) from Synap memory.';\n\n/** Python's `_default_tool_description`. Wording is load-bearing: it primes the model to call the tool. */\nexport function defaultToolDescription(scope: ToolScope): string {\n switch (scope) {\n case 'conversation':\n return (\n BASE_DESCRIPTION +\n ' Scoped to a specific conversation. Call this when you ' +\n 'need facts already established in the current conversation, ' +\n 'or to reload context after a topic shift.'\n );\n case 'user':\n return (\n BASE_DESCRIPTION +\n \" Scoped to the user's long-term memory. Call this when \" +\n 'you need to recall who the user is, their preferences, or ' +\n 'their history across conversations.'\n );\n case 'customer':\n return (\n BASE_DESCRIPTION +\n ' Scoped to the customer/organization. Call this when ' +\n 'you need facts about the customer org rather than an individual.'\n );\n case 'client':\n return (\n BASE_DESCRIPTION +\n ' Scoped to the integrating client/product. Call this for ' +\n 'product-level knowledge (e.g. policies, documentation).'\n );\n default:\n return (\n BASE_DESCRIPTION +\n ' Cross-scope: merges conversation, user, customer, and ' +\n 'client memory in one call. Call this at the start of a turn when ' +\n 'you need general grounding before responding.'\n );\n }\n}\n\n/**\n * Python's `_tool_input_schema`.\n *\n * `conversation_id` appears ONLY for `scope=\"conversation\"` when it was not\n * closed over, in which case it is also the one required field.\n */\nexport function toolInputSchema(scope: ToolScope, options: { hasConversationId: boolean }): Json {\n const props: Json = {\n search_query: {\n type: 'array',\n items: { type: 'string' },\n description:\n 'Optional list of search queries describing what context ' +\n 'you need. Free-form natural language is fine. Omit to ' +\n 'retrieve the most relevant recent context.',\n },\n max_results: {\n type: 'integer',\n minimum: 1,\n maximum: 50,\n description: 'Maximum items to return (default 10).',\n },\n types: {\n type: 'array',\n items: {\n type: 'string',\n enum: ['facts', 'preferences', 'episodes', 'emotions', 'temporal_events'],\n },\n description: 'Filter to specific memory categories. Omit to retrieve all.',\n },\n mode: {\n type: 'string',\n enum: ['fast', 'accurate'],\n description:\n \"Retrieval mode. 'fast' = low-latency (~50ms); 'accurate' = \" +\n \"LLM-decomposed multi-query (~200-500ms). Default 'fast'.\",\n },\n precision_level: {\n type: 'string',\n enum: ['high', 'medium'],\n description:\n \"Result filtering precision. 'high' (default) applies an extra \" +\n \"relevance-refinement pass; 'medium' skips it for faster, less \" +\n 'precisely filtered results (recall unaffected).',\n },\n };\n\n let required: string[] = [];\n if (scope === 'conversation' && !options.hasConversationId) {\n props['conversation_id'] = {\n type: 'string',\n description: 'The conversation id to fetch context for.',\n };\n required = ['conversation_id'];\n }\n\n return { type: 'object', properties: props, required, additionalProperties: false };\n}\n\n/** What `as_tool` needs from the client, so this module stays testable in isolation. */\nexport interface ToolDispatchTarget {\n fetch(options: Json): Promise<{\n formatted_context: string;\n scopes_queried: string[];\n total_items: number;\n }>;\n conversation: { context: { fetch(options: Json): Promise<Json> } };\n user: { context: { fetch(options: Json): Promise<Json> } };\n customer: { context: { fetch(options: Json): Promise<Json> } };\n client: { context: { fetch(options: Json): Promise<Json> } };\n}\n\nconst RESULT_COLLECTIONS = ['facts', 'preferences', 'episodes', 'emotions', 'temporal_events'] as const;\n\n/**\n * Python's `_invoke_scope_fetch`.\n *\n * Returns a plain object so the host runtime can JSON-serialise it straight\n * into the LLM's tool-result message.\n */\nexport async function invokeScopeFetch(params: {\n target: ToolDispatchTarget;\n scope: ToolScope;\n userId: string | undefined;\n customerId: string | undefined;\n conversationId: string | undefined;\n callArgs: Json;\n}): Promise<Json> {\n const { target, scope, userId, customerId, conversationId, callArgs } = params;\n\n const searchQuery = callArgs['search_query'];\n // Python: `int(call_args.get(\"max_results\") or 10)` -- so 0 falls back to 10.\n const rawMax = callArgs['max_results'];\n const maxResults = typeof rawMax === 'number' && rawMax !== 0 ? Math.trunc(rawMax) : 10;\n const types = callArgs['types'];\n const mode = String(callArgs['mode'] ?? '') || 'fast';\n const precisionLevel = String(callArgs['precision_level'] ?? '') || 'high';\n const callConvId =\n (typeof callArgs['conversation_id'] === 'string' && callArgs['conversation_id'] !== ''\n ? (callArgs['conversation_id'] as string)\n : undefined) ?? conversationId;\n\n const shared: Json = {\n search_query: searchQuery,\n max_results: maxResults,\n types,\n mode,\n precision_level: precisionLevel,\n };\n\n if (scope === 'unified') {\n const response = await target.fetch({\n conversation_id: callConvId,\n user_id: userId,\n customer_id: customerId,\n ...shared,\n });\n return {\n formatted_context: response.formatted_context,\n scopes_queried: response.scopes_queried,\n total_items: response.total_items,\n };\n }\n\n let response: Json;\n if (scope === 'conversation') {\n if (callConvId === undefined || callConvId === '') {\n // Python returns this as DATA, not an exception: the LLM sees the error\n // string in its tool result and can retry with an id.\n return { error: 'conversation_id is required', items: [] };\n }\n response = await target.conversation.context.fetch({\n conversation_id: callConvId,\n ...shared,\n user_id: userId,\n });\n } else if (scope === 'user') {\n response = await target.user.context.fetch({\n user_id: userId,\n conversation_id: callConvId,\n ...shared,\n customer_id: customerId,\n });\n } else if (scope === 'customer') {\n response = await target.customer.context.fetch({\n customer_id: customerId,\n conversation_id: callConvId,\n ...shared,\n });\n } else {\n response = await target.client.context.fetch({\n conversation_id: callConvId,\n ...shared,\n });\n }\n\n const out: Json = {};\n for (const collection of RESULT_COLLECTIONS) {\n out[collection] = Array.isArray(response[collection]) ? response[collection] : [];\n }\n return out;\n}\n\n/**\n * Build the tool definition.\n *\n * `onWarning` receives Python's `logger.warning` for the\n * `scope=\"conversation\"` without `user_id` case, which is a soft warning\n * rather than an error.\n */\nexport function buildTool(\n target: ToolDispatchTarget,\n options: AsToolOptions = {},\n onWarning: (message: string) => void = () => {},\n): ToolDefinition {\n const scope = String(options.scope ?? 'user').toLowerCase() as ToolScope;\n if (!VALID_SCOPES.includes(scope)) {\n // Python formats the valid set with sorted() and repr() on the input.\n const sorted = [...VALID_SCOPES].sort();\n throw new InvalidInputError(\n `scope must be one of ['${sorted.join(\"', '\")}'], got '${String(options.scope)}'`,\n );\n }\n\n const userId = options.user_id ?? options.userId;\n const customerId = options.customer_id ?? options.customerId;\n const conversationId = options.conversation_id ?? options.conversationId;\n\n if (scope === 'user' && (userId === undefined || userId === '')) {\n throw new InvalidInputError(\"scope='user' requires user_id\");\n }\n if (scope === 'customer' && (customerId === undefined || customerId === '')) {\n throw new InvalidInputError(\"scope='customer' requires customer_id\");\n }\n if (scope === 'conversation' && (userId === undefined || userId === '')) {\n // Not a hard error: the anticipation cache refuses cross-user matches\n // anyway. But the caller is leaving privacy on the floor, so say so.\n onWarning(\n \"as_tool(scope='conversation') without user_id: the SDK \" +\n 'anticipation cache cannot apply per-user filtering. Pass ' +\n 'user_id to enable the Section 15 privacy guarantee.',\n );\n }\n\n const style = String(options.style ?? 'openai');\n if (style !== 'openai' && style !== 'anthropic') {\n throw new InvalidInputError(`style must be 'openai' or 'anthropic', got '${style}'`);\n }\n\n const toolName = options.name ?? `synap_fetch_${scope}_context`;\n const toolDesc = options.description ?? defaultToolDescription(scope);\n const schema = toolInputSchema(scope, { hasConversationId: conversationId !== undefined });\n\n const handler: ToolHandler = (callArgs: Json = {}) =>\n invokeScopeFetch({ target, scope, userId, customerId, conversationId, callArgs });\n\n if (style === 'openai') {\n return {\n type: 'function',\n function: { name: toolName, description: toolDesc, parameters: schema },\n handler,\n };\n }\n return { name: toolName, description: toolDesc, input_schema: schema, handler };\n}\n","/**\n * SynapClient: the public entry point.\n *\n * Exposes two surfaces, both live in production:\n *\n * - **Namespaced, snake_case** (`client.user.context.fetch(...)`) is the\n * canonical surface and mirrors the Python SDK method for method. It\n * returns raw snake_case.\n * - **Top-level camelCase** (`client.fetchUserContext(...)`) is deprecated but\n * fully supported. It returns the normalised camelCase shape, and it exists\n * because the 0.3.x wrapper had it.\n *\n * They return different shapes on purpose. See gotcha G-B.\n */\n\nimport type { AgentModel } from './agent-model.js';\nimport { InvalidInputError } from './errors.js';\nimport { getEnv } from './util/env.js';\nimport { HttpTransport, type Credentials, type HttpTransportOptions } from './transport/http.js';\nimport { fetchContext, responseShapeOf } from './context/fetch.js';\nimport { normalizeContextResponse } from './context/normalize.js';\nimport {\n AnticipationCache, type AnticipationCacheSnapshot,\n} from './context/anticipation-cache.js';\nimport { flattenContextItems, type FlatMemory } from './context/flatten.js';\nimport {\n createMemoriesInterface, type CreateMemoryOptions, type CreateMemoryResult,\n type MemoriesInterface,\n} from './memories/interface.js';\nimport {\n createConversationNamespace, CompactionSubscribers, type ConversationNamespace,\n} from './conversation/interface.js';\nimport { createUserNamespace, type UserNamespace } from './user/interface.js';\nimport { createCreditsNamespace, type CreditsNamespace } from './credits/interface.js';\nimport { createCacheNamespace, type CacheNamespace } from './cache/interface.js';\nimport { createInstanceNamespace, type InstanceNamespace } from './instance/interface.js';\nimport type { FetchOptions, Json, NormalisedContext, RawContext } from './context/types.js';\nimport { SDK_VERSION } from './version.js';\nimport * as registry from './registry.js';\nimport { ShortTermStore, isComplete, type ShortTermTurn } from './cache/short-term-store.js';\nimport { TurnStore, type TurnInput } from './cache/turn-store.js';\nimport { renderExcerpts, searchTurns, toolResultText, type Encoder } from './cache/history-search.js';\nimport { SEMANTIC_MATCH } from './behavior/contract.js';\nimport { overlayLocalRecentTurns } from './context/overlay.js';\nimport { renderForPrompt, renderCompacted } from './context/local-prompt.js';\nimport { TurnCounter, mergeUserSummary } from './context/user-summary.js';\nimport {\n tryServeFromCache, servedItemIds, distinctQueryCount, type Scope as AnticipationScope,\n} from './context/anticipated.js';\nimport { rungKey } from './context/anticipation-cache.js';\nimport type { FetchHooks } from './context/fetch.js';\nimport { buildAssembledEvent } from './context/assembled.js';\nimport { newCorrelationId } from './util/correlation.js';\nimport { validateConversationId, validateInstanceId } from './util/validators.js';\nimport {\n mergeScopeResults, formatForPrompt, type UnifiedContext,\n} from './context/unified.js';\nimport { buildTool, type AsToolOptions, type ToolDefinition } from './tool/as-tool.js';\nimport { checkCustomerId } from './scoping.js';\n\n/** Levels the SDK emits. Mirrors the subset of Python's logging it uses. */\nexport type SynapLogLevel = 'debug' | 'info' | 'warn' | 'error';\n\n/**\n * Receives every diagnostic the SDK would otherwise write to the console.\n *\n * Python routes these through the stdlib `logging` module, which is why it\n * takes `log_level` and `logger`. There is no global logger in JavaScript, so\n * the SDK takes the sink directly instead of a level plus a framework.\n */\nexport type SynapLogger = (level: SynapLogLevel, message: string) => void;\n\nexport interface SynapClientOptions {\n /**\n * The four identity fields are `| undefined` rather than plain optional so a\n * consumer with `exactOptionalPropertyTypes` can pass `process.env.X`\n * directly, which is what every example does. Undefined already means \"fall\n * back to the environment\" at runtime; this makes the type say so.\n */\n apiKey?: string | undefined;\n clientId?: string | undefined;\n instanceId?: string | undefined;\n baseUrl?: string | undefined;\n /** Python spells this `api_base_url`; both are accepted so one config\n * object works against either SDK. */\n api_base_url?: string | undefined;\n timeouts?: HttpTransportOptions['timeouts'];\n retryPolicy?: HttpTransportOptions['retryPolicy'];\n keepAlive?: HttpTransportOptions['keepAlive'];\n /**\n * Keep the connection warm with a periodic /health ping.\n *\n * Off by default. A clear win for a long-lived process, pure waste in\n * serverless where the container is frozen between requests anyway.\n */\n heartbeat?: boolean;\n fetchImpl?: typeof fetch;\n /**\n * Splice locally-recorded turns into the next conversation-scope fetch, so a\n * just-written turn is visible before the server compacts. Defaults to true,\n * matching Python's `st_verbatim_overlay`. `SYNAP_ST_VERBATIM_OVERLAY`\n * overrides this either way.\n */\n st_verbatim_overlay?: boolean | undefined;\n stVerbatimOverlay?: boolean | undefined;\n /**\n * The model the agent runs on, and optionally its context window. Sent with\n * every recorded turn so the server sizes compaction to the agent's window\n * (it compacts at ~11.7% of it: 15K tokens on a 128K model). A name is\n * enough; the server knows common models' windows. Unset = 128K. Python's\n * `SDKConfig.agent_model` / `agent_context_window`; per-call `model` /\n * `context_window` override them.\n */\n agent_model?: string | undefined;\n agentModel?: string | undefined;\n agent_context_window?: number | undefined;\n agentContextWindow?: number | undefined;\n\n /**\n * Let the SDK answer `get_context_for_prompt` and `get_compacted` from its\n * own short-term store when the conversation is warm, skipping the cloud\n * round trip. Off by default. `SYNAP_SDK_ST_AUTHORITATIVE` overrides this in\n * either direction.\n *\n * Leave it off for an application that runs more than one process against\n * the same conversation. A process learns the server's message count only\n * when it writes. If it reads before it writes (build the prompt, then\n * record the turn), it cannot know that another process recorded a turn in\n * between, and it answers without that turn. Turn it on where one process\n * owns a conversation for its whole life, such as a voice agent.\n *\n * This is the COST switch. `st_verbatim_overlay` is the correctness one and\n * is independent of it.\n */\n sdk_st_authoritative?: boolean | undefined;\n sdkStAuthoritative?: boolean | undefined;\n\n /**\n * Your own embedding model for history search after compaction\n * (cache/history-search.ts), any `{ model, encode }`. It replaces the\n * SDK's model for history search; the anticipation cache uses it only if it\n * is the model its thresholds were tuned on (`history_search_model`).\n */\n history_search_encoder?: Encoder | undefined;\n historySearchEncoder?: Encoder | undefined;\n\n /**\n * The local embedding model, as Python's `history_search_model`: history\n * search after compaction, and matching fetch queries to prefetched items by\n * meaning. \"auto\" (default) runs the SDK's multilingual model in Node when\n * the optional `onnxruntime-node` and `@huggingface/tokenizers` packages are\n * installed: downloaded once (about 135 MB) into ~/.synap/models when the\n * first stream starts or the first history search runs, then loaded in the\n * background. An \"onnx:<repo>@<revision>/<file>\" names another (history\n * search only). null keeps keyword matching. Never loaded on Edge or in a\n * browser.\n */\n history_search_model?: string | null | undefined;\n historySearchModel?: string | null | undefined;\n\n /**\n * Where the SDK's diagnostics go. Defaults to `console.warn` prefixed with\n * `[synap]`. Pass a no-op to silence them, or your own logger to route them.\n */\n logger?: SynapLogger | undefined;\n\n /**\n * Bypass the process registry and build a genuinely separate client.\n *\n * Underscored because it mirrors Python's `_force_new` and is not part of the\n * everyday surface. Needed in tests, and for the rare case of deliberately\n * running two clients on one identity with separate caches.\n */\n _force_new?: boolean;\n}\n\ninterface ScopedContext {\n context: { fetch: (options?: FetchOptions) => Promise<RawContext> };\n}\n\n/**\n * Options for `configure()`.\n *\n * The Python-only keys are declared so a config object can be shared between\n * the two SDKs without a type error. They are accepted and ignored: there is\n * no SQLite cache backend and no global logger to replace here.\n */\nexport interface ConfigureOptions {\n timeouts?: HttpTransportOptions['timeouts'];\n retryPolicy?: HttpTransportOptions['retryPolicy'] | null;\n retry_policy?: HttpTransportOptions['retryPolicy'] | null;\n /** Accepted and ignored. Python-only. */\n storage_path?: string;\n /** Accepted and ignored. Python-only. */\n cache_backend?: string | null;\n /** Accepted and ignored. Python-only. */\n session_timeout_minutes?: number;\n /** Accepted and ignored. Python-only. */\n log_level?: string;\n /** The agent's model; see `SynapClientOptions.agent_model`. */\n agent_model?: string;\n agentModel?: string;\n agent_context_window?: number;\n agentContextWindow?: number;\n /** Replaces the diagnostic sink, same as the constructor option. */\n logger?: SynapLogger | undefined;\n}\n\n/** Options for the cross-scope `fetch()`. Snake_case, matching Python's kwargs.\n *\n * The three identifiers accept `null` as well as `undefined`, because that is\n * what callers actually pass: `conversation_id: convId || null` is the normal\n * way to say \"there is no conversation here\", and `validateConversationId`\n * has always treated `null` as absent. The declared type said otherwise, so\n * the `SynapClient`-to-`SynapSdkLike` assignability test in\n * `synap-claude-agent` (TS) could not typecheck, and the scope guards below\n * did not agree with the validator either. See the guards for that half.\n */\nexport interface UnifiedFetchOptions {\n conversation_id?: string | null;\n user_id?: string | null;\n customer_id?: string | null;\n search_query?: string[];\n max_results?: number;\n types?: string[];\n mode?: string;\n precision_level?: string;\n /** The moment the question is being asked. See FetchOptions.query_date. */\n query_date?: string;\n /** A share of the response per search query. See FetchOptions.max_results_per_query. */\n max_results_per_query?: number;\n include_conversation_context?: boolean;\n scopes?: string[];\n include_scope_labels?: boolean;\n context_mode?: string;\n include_profile?: boolean;\n last_n_conversations?: number;\n /**\n * `\"by_type\"` (default) or `\"ranked\"`. Ranked asks every scope for its\n * ranked list and merges them into `items`, best first across scopes and\n * types, by each item's `rank_score`; `formatted_context` is then one\n * numbered list in that order. The typed lists are filled either way.\n */\n response_shape?: string;\n}\n\n/** `SYNAP_SDK_LOCAL_MODEL=off` keeps the local embedding model from loading. Python: the same. */\nfunction localModelOff(): boolean {\n return ['off', '0', 'false', 'no', 'none'].includes((getEnv('SYNAP_SDK_LOCAL_MODEL') ?? '').trim().toLowerCase());\n}\n\nexport class SynapClient {\n // Native-private (`#`), not TypeScript `private`. TS `private` is erased at\n // compile time, so the field stays enumerable at runtime and a JS consumer\n // can still reach it. These two were public and made `client.transport\n // .credentials.apiKey` and the raw cache's `now`/`fire`/`invalidateEntity`\n // reachable, which is exactly what `client.cache` exists to prevent.\n // Definite-assignment assertions (`!`) because the constructor can RETURN an\n // existing client from the registry before reaching any of these. TypeScript\n // cannot follow a constructor return, so it sees a path where the fields are\n // unassigned. On that path the object is discarded and the incumbent is\n // handed back instead, so nothing observes them.\n readonly #transport!: HttpTransport;\n readonly #anticipationCache!: AnticipationCache;\n\n readonly user!: UserNamespace;\n readonly customer!: ScopedContext;\n readonly client!: ScopedContext;\n readonly conversation!: ConversationNamespace;\n readonly memories!: MemoriesInterface;\n readonly credits!: CreditsNamespace;\n readonly cache!: CacheNamespace;\n readonly instance!: InstanceNamespace;\n\n #closed = false;\n #initialized = false;\n #identity: Promise<void> | undefined;\n readonly #registryKeys!: string[];\n readonly #compactionSubscribers!: CompactionSubscribers;\n readonly #shortTerm!: ShortTermStore;\n /** Every raw turn, past compaction, for local history search. */\n readonly #turnStore!: TurnStore;\n /** A local embedding model for history search; null = keywords only. */\n #localEncoder: Encoder | null = null;\n /** `history_search_model`: \"auto\", an \"onnx:...\" name, or null for none. */\n #localModelName: string | null = 'auto';\n #localModelStarted = false;\n readonly #backfillAfter = new Map<string, number>();\n readonly #stVerbatimOverlay!: boolean | undefined;\n #agentModel: AgentModel = {};\n #logger!: SynapLogger | undefined;\n readonly #stAuthoritative!: boolean | undefined;\n readonly #turns!: TurnCounter;\n readonly #fetchHooks!: FetchHooks;\n\n constructor(options: SynapClientOptions = {}) {\n const credentials = resolveCredentials(options);\n const resolvedBaseUrl = resolveBaseUrl(options);\n // Fail locally on a malformed id rather than sending it upstream.\n validateInstanceId(credentials.instanceId);\n\n // ── Registry: one client per identity, per process ──────────────────────\n // Python has shared state here since 0.2. Without it, a framework\n // integration that builds a client per request or per agent gets a cold\n // anticipation cache every time and pays for a retrieval that Python would\n // have served locally.\n //\n // Returning from a constructor is unusual in JS but legal, and it is the\n // closest equivalent of Python's `self.__dict__ = existing.__dict__`. The\n // two callers then hold the SAME object rather than two objects sharing a\n // state bag, which is stricter than Python.\n const registryKey = registry.buildRegistryKey(credentials.instanceId, credentials.apiKey);\n if (options._force_new !== true) {\n const existing = registry.get(registryKey) as SynapClient | undefined;\n if (existing !== undefined) {\n // Handing back a client built on a DIFFERENT credential than the caller\n // just supplied. Only reachable on the explicit instanceId path, where\n // the id is the identity and the key is not part of it. The caller has\n // no other way to find out: the returned object looks exactly like the\n // one they asked for.\n const incumbent = existing.#transport.currentCredentials().apiKey;\n if (credentials.apiKey && incumbent && credentials.apiKey !== incumbent) {\n existing.#warn(\n `Instance ${registryKey} is already running in this process on a different ` +\n 'API key. The key passed here is ignored; this client will authenticate ' +\n 'as the existing one. Rotating a key this way has no effect. Construct ' +\n 'with apiKey and no instanceId to get a client on your own credential.',\n );\n }\n return existing;\n }\n }\n this.#registryKeys = options._force_new === true ? [] : [registryKey];\n\n this.#transport = new HttpTransport({\n credentials,\n // SYNAP_BASE_URL is honoured, as Python does (sdk.py) and as the 0.3.x\n // wrapper did. Ignoring it was a wrong-DESTINATION bug rather than a\n // missing convenience: anyone pointing at a self-hosted or staging\n // deployment through the environment would silently have started sending\n // their data to production on upgrade.\n ...(resolvedBaseUrl !== undefined ? { baseUrl: resolvedBaseUrl } : {}),\n ...(options.timeouts !== undefined ? { timeouts: options.timeouts } : {}),\n ...(options.retryPolicy !== undefined ? { retryPolicy: options.retryPolicy } : {}),\n ...(options.keepAlive !== undefined ? { keepAlive: options.keepAlive } : {}),\n ...(options.fetchImpl !== undefined ? { fetchImpl: options.fetchImpl } : {}),\n userAgent: `@maximem/synap-js-sdk/${SDK_VERSION}`,\n });\n if (options.heartbeat) this.#transport.startHeartbeat();\n this.#transport.identityResolver = () => this.#resolveIdentity();\n\n this.#anticipationCache = new AnticipationCache();\n\n this.#fetchHooks = {\n beforeFetch: async (scope, options) => {\n const o = options as Record<string, unknown>;\n // A prefetched bundle was assembled with no period restriction, so it\n // cannot answer a request that names one. Same class as the scope_path\n // defect described below: a bundle built for one question served to a\n // different one, entirely inside this process and invisible to every\n // server log. Skipping the cache costs one round trip; serving a bundle\n // that ignores the date costs the answer.\n if (o['query_date'] ?? o['queryDate']) return null;\n // A prefetched bundle was not assembled under a per-query share\n // either, and its items do not say which query found them.\n if (o['max_results_per_query'] ?? o['maxResultsPerQuery']) return null;\n const attempt = await tryServeFromCache(\n this.#anticipationCache,\n scope as AnticipationScope,\n {\n searchQuery: (o['search_query'] ?? o['searchQuery']) as string[] | undefined,\n entityId: (o['user_id'] ?? o['userId']) as string | undefined,\n customerId: (o['customer_id'] ?? o['customerId']) as string | undefined,\n clientId: this.#transport.currentCredentials().clientId || null,\n conversationId: (o['conversation_id'] ?? o['conversationId']) as string | undefined,\n // The rung this read is addressed to, in the same words the server\n // stamped on the bundles it pushed. Without it the cache matched\n // on entity and conversation alone, so a bundle prefetched for one\n // rung answered a caller standing at another, entirely inside this\n // process and invisible to every server log.\n scopeRung: rungKey(\n (o['scope_path'] ?? o['scopePath']) as Record<string, string> | undefined,\n ),\n },\n Number(o['max_results'] ?? o['maxResults'] ?? 10),\n );\n if (attempt.response === null || attempt.hit === null) return null;\n return {\n response: attempt.response,\n servedItemIds: servedItemIds(attempt.hit),\n bundleId: attempt.hit.bundleIds[0] ?? '',\n borderline: attempt.hit.band === 'borderline',\n raceDeadlineMs: SEMANTIC_MATCH.race_deadline_ms,\n };\n },\n onAssembled: (scope, options, response, startedAt) => {\n this.#emitContextAssembled(scope, options as Record<string, unknown>, response, startedAt);\n },\n onServedFromCache: (_scope, options, served) => {\n // Fire-and-forget over the Listen stream. Drives the server's learning\n // loop: per-prefetch outcome scoring, per-pattern hit rates. Carries\n // ids and scope only, never prompt content.\n this.#emitContextUsed(options as Record<string, unknown>, _scope, served);\n },\n };\n\n this.user = createUserNamespace(this.#transport, this.#fetchHooks);\n // `async` arrows, not bare ones. fetchContext is already async so nothing\n // can throw synchronously today, but these are the only two namespace\n // methods that were not async, and the next person to add a validation\n // line above the call would reintroduce a throw that bypasses .catch().\n this.customer = { context: { fetch: async (o = {}) => fetchContext(this.#transport, 'customer', o, this.#fetchHooks) } };\n this.client = { context: { fetch: async (o = {}) => fetchContext(this.#transport, 'client', o, this.#fetchHooks) } };\n this.#compactionSubscribers = new CompactionSubscribers();\n this.#shortTerm = new ShortTermStore();\n // Every raw turn, kept past compaction for local search (turn-store.ts).\n this.#turnStore = new TurnStore();\n this.#shortTerm.attachArchive(this.#turnStore);\n this.#stVerbatimOverlay = options.st_verbatim_overlay ?? options.stVerbatimOverlay;\n this.#agentModel = {\n model: options.agent_model ?? options.agentModel,\n contextWindow: options.agent_context_window ?? options.agentContextWindow,\n };\n this.#logger = options.logger;\n this.#stAuthoritative = options.sdk_st_authoritative ?? options.sdkStAuthoritative;\n this.#localEncoder = options.history_search_encoder ?? options.historySearchEncoder ?? null;\n const modelOption = options.history_search_model !== undefined\n ? options.history_search_model\n : options.historySearchModel;\n this.#localModelName = modelOption === undefined ? 'auto' : modelOption;\n if (this.#localEncoder?.model === SEMANTIC_MATCH.model) this.#anticipationCache.setEncoder(this.#localEncoder);\n this.#turns = new TurnCounter();\n this.conversation = createConversationNamespace(\n this.#transport,\n this.#compactionSubscribers,\n // Mirrors Python's `_invalidate_anticipation_on_write`, including its\n // gate: `invalidateEntity` is a no-op unless\n // SYNAP_SDK_CACHE_INVALIDATE_ON_WRITE is set, so default behaviour is\n // unchanged. Over-invalidating after a failed write is safe; it only\n // costs the next lookup a cold fetch.\n ({ userId, customerId }) => {\n for (const id of new Set([userId, customerId])) {\n if (id) this.#anticipationCache.invalidateEntity(id);\n }\n },\n // Buffer the turn locally so the next fetch sees it, ahead of the server\n // having persisted and compacted it.\n ({ conversationId, role, content, timestamp, messageId, serverMessageCount, inFlight }) => {\n this.#shortTerm.appendTurn(conversationId, role, content, timestamp, {\n messageId,\n serverMessageCount,\n inFlight,\n });\n },\n (response, conversationId, userId, scopePath) => {\n overlayLocalRecentTurns(response, this.#shortTerm, conversationId, this.#stVerbatimOverlay);\n this.#injectUserSummary(response, conversationId, userId, rungKey(scopePath));\n },\n {\n prompt: (conversationId, style) => this.#localPrompt(conversationId, style),\n compacted: (conversationId, format) => this.#localCompacted(conversationId, format),\n reseed: (conversationId, response) => this.#reseedShortTerm(conversationId, response),\n historyExcerpts: (conversationId, response, query) => this.#withHistoryExcerpts(conversationId, response, query),\n },\n () => this.#agentModel,\n );\n // After a forget that deleted something, drop that user's cached bundles so\n // they are not served until the TTL runs out. Unconditional, unlike the\n // invalidate-on-write hook: a forgotten memory must not be served.\n this.memories = createMemoriesInterface(this.#transport, ({ userId, customerId }) => {\n for (const id of new Set([userId, customerId])) {\n if (id) this.#anticipationCache.dropEntity(id);\n }\n });\n this.credits = createCreditsNamespace(this.#transport);\n this.cache = createCacheNamespace(this.#anticipationCache);\n this.instance = createInstanceNamespace(\n () => this.#transport.currentCredentials(),\n this.#anticipationCache,\n ({ conversationId, role, content }) => {\n this.#shortTerm.appendTurn(conversationId, role, content);\n },\n () => this.#agentModel,\n ({ conversationId, result, toolName }) => this.archive_tool_result(conversationId, result, { tool_name: toolName }),\n );\n // A compaction folds turns into the server's summary, so the local copies\n // of those turns must go or they keep being spliced back in.\n // Prefetched bundles start arriving with the stream: load the local model\n // then, not at construction, so a process that never listens or searches\n // never downloads it.\n this.instance._internal.setOnListen(() => this.#startLocalModel());\n this.instance._internal.setCompactionApplier((bundle) => {\n this.#shortTerm.applyCompaction(bundle);\n });\n // The server's message count after each streamed write, which is how the\n // store learns whether it holds the whole conversation.\n this.instance._internal.setConversationStateHandler((conversationId, messageCount) => {\n this.#shortTerm.noteServerCount(conversationId, messageCount);\n });\n\n if (options._force_new !== true) {\n // Claim the slot. A separate get-then-set would race in a threaded\n // runtime; JS is single-threaded between these statements, so this is\n // one operation for clarity rather than for locking.\n const incumbent = registry.registerIfAbsent(registryKey, this) as SynapClient | undefined;\n if (incumbent !== undefined) return incumbent;\n }\n // Route compaction bundles from the one Listen stream into the typed\n // per-conversation subscriptions. Wired here rather than inside either\n // namespace so the two do not have to import each other.\n this.instance._internal.setCompactionDispatcher((conversationId, bundle) => {\n this.#compactionSubscribers.dispatch(conversationId, bundle as Json, (error) => {\n this.#warn(`compaction subscriber threw: ${String(error)}`);\n });\n });\n }\n\n // ── Deprecated camelCase surface, carried over from the 0.3.x wrapper ───────\n // These return the NORMALISED shape, unlike their namespaced equivalents.\n\n /** @deprecated Use `client.user.context.fetch()`. Note it returns raw snake_case. */\n async fetchUserContext(options: FetchOptions = {}): Promise<NormalisedContext> {\n return normalizeContextResponse((await fetchContext(this.#transport, 'user', options)) as Json);\n }\n\n /** @deprecated Use `client.customer.context.fetch()`. */\n async fetchCustomerContext(options: FetchOptions = {}): Promise<NormalisedContext> {\n return normalizeContextResponse((await fetchContext(this.#transport, 'customer', options)) as Json);\n }\n\n /** @deprecated Use `client.client.context.fetch()`. */\n async fetchClientContext(options: FetchOptions = {}): Promise<NormalisedContext> {\n return normalizeContextResponse((await fetchContext(this.#transport, 'client', options)) as Json);\n }\n\n /** @deprecated Use `client.conversation.context.get_context_for_prompt()`. */\n async getContextForPrompt(\n options: { conversationId?: string; conversation_id?: string; style?: string } = {},\n ): Promise<Json> {\n return this.conversation.context.get_context_for_prompt(options);\n }\n\n /**\n * @deprecated Use `client.user.context.fetch()` and read the typed collections.\n *\n * Flattens every context type into one list, exactly as the 0.3.x bridge did.\n */\n async searchMemory(options: {\n userId?: string; user_id?: string;\n customerId?: string; customer_id?: string;\n query: string;\n maxResults?: number; max_results?: number;\n mode?: string;\n conversationId?: string; conversation_id?: string;\n types?: string[];\n }): Promise<{ results: FlatMemory[]; count: number }> {\n const userId = options.user_id ?? options.userId;\n if (!userId) throw new InvalidInputError('user_id is required');\n if (options.query === undefined || options.query === null) {\n throw new InvalidInputError('query is required');\n }\n const context = await fetchContext(this.#transport, 'user', {\n user_id: userId,\n ...(pick(options, 'customer_id', 'customerId') !== undefined\n ? { customer_id: pick(options, 'customer_id', 'customerId') as string }\n : {}),\n ...(pick(options, 'conversation_id', 'conversationId') !== undefined\n ? { conversation_id: pick(options, 'conversation_id', 'conversationId') as string }\n : {}),\n search_query: [options.query],\n max_results: (pick(options, 'max_results', 'maxResults') as number) ?? 10,\n types: options.types ?? ['all'],\n ...(options.mode !== undefined ? { mode: options.mode } : {}),\n });\n const results = flattenContextItems(context);\n return { results, count: results.length };\n }\n\n /**\n * @deprecated Use `client.user.context.fetch()` with an empty `search_query`.\n *\n * Note the default of 100, not 10: it matches the 0.3.x bridge.\n */\n async getMemories(options: {\n userId?: string; user_id?: string;\n customerId?: string; customer_id?: string;\n mode?: string;\n conversationId?: string; conversation_id?: string;\n maxResults?: number; max_results?: number;\n types?: string[];\n }): Promise<{ results: FlatMemory[]; count: number }> {\n const userId = options.user_id ?? options.userId;\n if (!userId) throw new InvalidInputError('user_id is required');\n const context = await fetchContext(this.#transport, 'user', {\n user_id: userId,\n ...(pick(options, 'customer_id', 'customerId') !== undefined\n ? { customer_id: pick(options, 'customer_id', 'customerId') as string }\n : {}),\n ...(pick(options, 'conversation_id', 'conversationId') !== undefined\n ? { conversation_id: pick(options, 'conversation_id', 'conversationId') as string }\n : {}),\n search_query: [],\n max_results: (pick(options, 'max_results', 'maxResults') as number) ?? 100,\n types: options.types ?? ['all'],\n ...(options.mode !== undefined ? { mode: options.mode } : {}),\n });\n const results = flattenContextItems(context);\n return { results, count: results.length };\n }\n\n /** @deprecated Use `client.memories.create()`. */\n async addMemory(options: CreateMemoryOptions): Promise<CreateMemoryResult> {\n return this.memories.create(options);\n }\n\n /**\n * @deprecated Use `client.memories.delete(memoryId)`.\n *\n * The `{ userId }` form is gone: it relied on a process-local id list and so\n * deleted nothing in serverless while reporting success (gotcha G-F).\n */\n async deleteMemory(options: { memoryId?: string; memory_id?: string }): Promise<Json> {\n return this.memories.delete(options.memory_id ?? options.memoryId ?? '');\n }\n\n /**\n * Resolve `client_id` and `instance_id` from the API key.\n *\n * Mirrors Python's `initialize()`. Python raises \"SDK not initialized\" on\n * any call made before it; here the first request runs the same resolution\n * instead (`identityResolver` on the transport), so skipping this call costs\n * nothing and breaks no existing caller. It matters for the anticipation\n * cache: a bundle shared at client scope is filtered on `client_id`, and an\n * empty one fails that filter for every bundle.\n *\n * Calling it explicitly still locks `configure()`, as in Python. Resolving\n * on first use does not, because a caller who never called it could always\n * configure after a request, and that keeps working.\n *\n * Best-effort and idempotent. A whoami failure leaves whatever the\n * constructor resolved and is not fatal.\n */\n async initialize(): Promise<void> {\n this.#initialized = true;\n await this.#resolveIdentity();\n }\n\n /** Runs whoami at most once, however many calls arrive together. */\n #resolveIdentity(): Promise<void> {\n this.#identity ??= this.#whoami();\n return this.#identity;\n }\n\n async #whoami(): Promise<void> {\n const credentials = this.#transport.currentCredentials();\n if (credentials.clientId !== '' && credentials.instanceId !== '') return;\n try {\n const whoami = (await this.#transport.request('whoami', {})) as Json;\n const resolvedClientId = String(whoami['client_id'] ?? '');\n const resolvedInstanceId = String(whoami['instance_id'] ?? '');\n // The instance's scoping mode. Absent against any server older than this\n // field, and absent stays undefined, which every check treats as \"do not\n // enforce\". The server rejects independently either way.\n const iso = whoami['user_context_isolation'];\n // Stored on the transport, not on the client: every interface factory\n // already receives the transport, and surface-parity.test.ts is right\n // that a public member Python does not have is a divergence.\n this.#transport.userContextIsolation = typeof iso === 'string' ? iso : undefined;\n this.#transport.updateCredentials({\n ...(resolvedClientId !== '' && credentials.clientId === ''\n ? { clientId: resolvedClientId }\n : {}),\n ...(resolvedInstanceId !== '' && credentials.instanceId === ''\n ? { instanceId: resolvedInstanceId }\n : {}),\n });\n\n // Now that the real instance id is known, point it at this client too.\n // It was keyed on the credential because the id did not exist yet, so a\n // later `new SynapClient({ instanceId })` for the same instance would\n // otherwise miss and build a second client: two anticipation caches and\n // two Listen streams for one instance. The credential slot is kept, so\n // constructing by API key keeps resolving here as well.\n if (resolvedInstanceId !== '' && this.#registryKeys.length > 0) {\n if (registry.aliasIfAbsent(resolvedInstanceId, this)) {\n this.#registryKeys.push(resolvedInstanceId);\n }\n }\n } catch {\n // Non-fatal, exactly as in Python: identity resolution is an\n // optimisation, and every route still authenticates on the API key.\n }\n }\n\n /**\n * @deprecated Use `initialize()`, which is the Python name. `init()` was a\n * JS-only spelling with no Python counterpart; it now forwards, so a 0.3.x\n * startup script keeps working.\n */\n async init(): Promise<void> {\n return this.initialize();\n }\n\n /** The resolved instance id. Empty until `initialize()` resolves it. */\n get instance_id(): string {\n return this.#transport.currentCredentials().instanceId;\n }\n\n /** The resolved client id. Empty until `initialize()` resolves it. */\n get client_id(): string {\n return this.#transport.currentCredentials().clientId;\n }\n\n /**\n * Update configuration before `initialize()`.\n *\n * Mirrors Python's `configure()`, including its refusal to reconfigure after\n * initialization: the transport and cache are already built by then and a\n * half-applied config is worse than a rejected one.\n *\n * `logger` is honoured here as well as on the constructor. The remaining\n * Python-only keys (`storage_path`, `cache_backend`,\n * `session_timeout_minutes`, `log_level`) are accepted and ignored rather\n * than rejected, because this SDK has no SQLite cache backend and no global\n * logging framework whose level there would be to set. Ignoring them keeps\n * a shared config object portable between the two SDKs.\n */\n configure(options: ConfigureOptions = {}): void {\n if (this.#initialized) {\n throw new InvalidInputError('Cannot reconfigure after initialization');\n }\n if (options.timeouts !== undefined) this.#transport.setTimeouts(options.timeouts);\n if (options.retryPolicy !== undefined || options.retry_policy !== undefined) {\n this.#transport.setRetryPolicy(options.retryPolicy ?? options.retry_policy ?? null);\n }\n if (options.logger !== undefined) this.#logger = options.logger;\n const model = options.agent_model ?? options.agentModel;\n const window = options.agent_context_window ?? options.agentContextWindow;\n if (model !== undefined || window !== undefined) {\n this.#agentModel = {\n model: model ?? this.#agentModel.model,\n contextWindow: window ?? this.#agentModel.contextWindow,\n };\n }\n }\n\n /**\n * Fetch and merge context across every scope with an identifier, in one call.\n *\n * Mirrors Python's `sdk.fetch()`. The three behaviours worth knowing:\n *\n * - Scope fetches run in parallel and a FAILED scope is dropped with a\n * warning rather than failing the call, so partial context still gets\n * returned. The one exception is `InvalidInputError`: a bad request must\n * surface instead of degrading into a silently-empty context.\n * - `user_id` is threaded into the conversation-scope sub-fetch so the\n * anticipation cache can apply its per-user filter.\n * - The summary-mode parameters go ONLY to the user-scope sub-fetch. The\n * other three routes return 422 for `context_mode=\"conversation-summary\"`,\n * and that rejection would be swallowed by the drop-failed-scope rule.\n */\n async fetch(options: UnifiedFetchOptions = {}): Promise<UnifiedContext> {\n const {\n conversation_id: conversationId,\n user_id: userId,\n customer_id: customerId,\n search_query: searchQuery,\n max_results: maxResults = 20,\n types,\n mode = 'fast',\n precision_level: precisionLevel = 'high',\n query_date: queryDate,\n max_results_per_query: maxResultsPerQuery,\n include_conversation_context: includeConversationContext = true,\n scopes,\n include_scope_labels: includeScopeLabels = false,\n context_mode: contextMode = 'in-conversation',\n include_profile: includeProfile = true,\n last_n_conversations: lastNConversations = 1,\n response_shape: responseShape = 'by_type',\n } = options;\n\n validateConversationId(conversationId);\n // Up front, as Python does: a bad value inside a scope fetch would be an\n // InvalidInputError too, but only once a scope is queried.\n responseShapeOf({ response_shape: responseShape });\n\n const wants = (scope: string): boolean => scopes === undefined || scopes.includes(scope);\n const shared = {\n search_query: searchQuery,\n max_results: maxResults,\n types,\n mode,\n precision_level: precisionLevel,\n // In `shared` so it reaches all four branches at once. The Python edit\n // threaded three call sites by hand and missed the client branch, which\n // sits one indent deeper; spreading removes that whole class of miss.\n query_date: queryDate,\n max_results_per_query: maxResultsPerQuery,\n response_shape: responseShape,\n };\n\n const labels: string[] = [];\n const tasks: Array<Promise<RawContext>> = [];\n\n // `null` means absent, exactly as `validateConversationId` already reads\n // it. The guards below used to test only `!== undefined && !== ''`, so a\n // caller writing `conversation_id: convId || null` -- which is what the\n // Claude Agent (TS) integration writes, and the obvious way to spell \"no\n // conversation\" -- sailed past them and fired a conversation-scope fetch\n // carrying `conversation_id: null`. The validator waved it through and the\n // scope was queried for a conversation that does not exist.\n const present = (v: string | null | undefined): v is string =>\n v !== undefined && v !== null && v !== '';\n\n if (present(conversationId) && wants('conversation')) {\n labels.push('conversation');\n tasks.push(this.conversation.context.fetch({\n conversation_id: conversationId, ...shared,\n user_id: userId, customer_id: customerId,\n } as FetchOptions));\n }\n if (present(userId) && wants('user')) {\n labels.push('user');\n tasks.push(this.user.context.fetch({\n user_id: userId, conversation_id: conversationId, ...shared,\n customer_id: customerId,\n context_mode: contextMode,\n include_profile: includeProfile,\n last_n_conversations: lastNConversations,\n } as FetchOptions));\n }\n if (present(customerId) && wants('customer')) {\n labels.push('customer');\n tasks.push(this.customer.context.fetch({\n customer_id: customerId, conversation_id: conversationId, ...shared,\n } as FetchOptions));\n }\n // Client scope needs no identifier (it is inferred from auth), but Python\n // only queries it when EXPLICITLY named in `scopes`. Preserved: including\n // it by default would add a billed fetch to every unified call.\n if (scopes !== undefined && scopes.includes('client')) {\n labels.push('client');\n tasks.push(this.client.context.fetch({\n conversation_id: conversationId, ...shared,\n } as FetchOptions));\n }\n\n if (tasks.length === 0) {\n const empty = mergeScopeResults([]);\n return empty;\n }\n\n const settled = await Promise.allSettled(tasks);\n const successful: Array<readonly [string, RawContext]> = [];\n for (const [i, result] of settled.entries()) {\n const label = labels[i] as string;\n if (result.status === 'fulfilled') {\n successful.push([label, result.value]);\n continue;\n }\n if (result.reason instanceof InvalidInputError) throw result.reason;\n this.#warn(`Scope '${label}' fetch failed in unified fetch (non-fatal): ${String(result.reason)}`);\n }\n\n const merged = mergeScopeResults(successful);\n\n // Read-your-writes: splice the locally buffered tail over whatever the\n // server returned for this conversation.\n for (const [, response] of successful) {\n overlayLocalRecentTurns(response, this.#shortTerm, conversationId, this.#stVerbatimOverlay);\n }\n\n if (includeConversationContext && present(conversationId)) {\n try {\n merged.conversation_context = (await this.conversation.context.get_context_for_prompt({\n conversation_id: conversationId,\n })) as Json;\n } catch {\n // Non-fatal in Python too: the merged items are still worth returning.\n }\n }\n\n merged.formatted_context = formatForPrompt(merged, {\n includeScope: includeScopeLabels,\n includeConversationContext,\n });\n return merged;\n }\n\n /**\n * An LLM-ready tool definition that fetches Synap context.\n *\n * Mirrors Python's `as_tool()`. The scope identifiers are closed over, so\n * the model cannot choose whose memory to read.\n */\n as_tool(options: AsToolOptions = {}): ToolDefinition {\n return buildTool(this as never, options, (message) => this.#warn(message));\n }\n\n /**\n * A read-only diagnostic view of the in-process anticipation cache.\n *\n * Mirrors Python's `anticipation_cache_snapshot()`. Returns the same\n * snake_case shape, since this output gets logged and compared across SDKs.\n */\n anticipation_cache_snapshot(): AnticipationCacheSnapshot {\n return this.#anticipationCache.snapshot();\n }\n\n /**\n * Report a locally served fetch back to the server.\n *\n * Only while listening: the event rides the Listen stream, and there is\n * nowhere to put it otherwise. Never throws, because a telemetry failure\n * must not fail a retrieval that already succeeded.\n */\n #emitContextUsed(\n options: Record<string, unknown>,\n scope: string,\n served: { servedItemIds: string[]; bundleId: string },\n ): void {\n try {\n if (!this.instance.is_listening) return;\n this.instance._internal.sendContextUsed({\n bundle_id: served.bundleId,\n conversation_id: String(options['conversation_id'] ?? options['conversationId'] ?? ''),\n user_id: String(options['user_id'] ?? options['userId'] ?? ''),\n customer_id: String(options['customer_id'] ?? options['customerId'] ?? ''),\n served_item_ids: served.servedItemIds,\n scope,\n source_bundle_ids: served.bundleId ? [served.bundleId] : [],\n timestamp_ms: Date.now(),\n // How many distinct search queries the fetch carried. A hit never\n // reaches the fetch route, so this event is how the server learns it\n // happened and what it is charged by. The count only: this event must\n // not carry the caller's words.\n query_count: distinctQueryCount(options['search_query'] ?? options['searchQuery']),\n });\n } catch {\n // Non-fatal by design.\n }\n }\n\n /**\n * Report what was actually composed for this turn.\n *\n * Fires whether the fetch was served from the cache or the network, because\n * the audit needs to distinguish them. Like the other stream events it is\n * fire-and-forget and only goes anywhere while listening.\n */\n #emitContextAssembled(\n scope: string,\n options: Record<string, unknown>,\n response: RawContext,\n startedAt: number,\n ): void {\n try {\n if (!this.instance.is_listening) return;\n this.instance._internal.sendContextAssembled({\n ...buildAssembledEvent({\n correlationId: newCorrelationId(),\n response,\n conversationId: (options['conversation_id'] ?? options['conversationId']) as string | undefined,\n userId: (options['user_id'] ?? options['userId']) as string | undefined,\n customerId: (options['customer_id'] ?? options['customerId']) as string | undefined,\n startedAt,\n sdkVersion: SDK_VERSION,\n }),\n scope,\n });\n } catch {\n // Non-fatal by design.\n }\n }\n\n /**\n * Fold a cached user summary in, every Nth turn.\n *\n * Only when the caller supplied a user_id. Without one the summary lookup\n * cannot be scoped, and an unscoped lookup is how user A's summary ends up in\n * user B's response.\n */\n #injectUserSummary(\n response: RawContext,\n conversationId: string | undefined,\n userId: string | undefined,\n scopeRung: string | null,\n ): void {\n this.#turns.increment(conversationId);\n if (userId === undefined || userId === '') return;\n if (!this.#turns.shouldInject(conversationId)) return;\n // The rung, for the same reason the user id is here: a summary prefetched\n // for one rung is not this caller's summary if they are standing at\n // another, and splicing it in is the same failure one level down.\n const summary = this.#anticipationCache.lookupUserSummary(userId, scopeRung);\n if (summary === null) return;\n mergeUserSummary(response, summary as Json);\n }\n\n /**\n * Whether the SDK is authoritative for short-term context.\n *\n * Resolution order matches Python's `_is_st_authoritative`:\n * `SYNAP_SDK_ST_AUTHORITATIVE` first, as an explicit override in either\n * direction, then the option. Off by default. Being enabled does not mean\n * the local store is used: that also takes an entry that holds the whole\n * conversation, see `isComplete`.\n */\n #isStAuthoritative(): boolean {\n const raw = (getEnv('SYNAP_SDK_ST_AUTHORITATIVE') ?? '').trim().toLowerCase();\n if (['0', 'false', 'no', 'off'].includes(raw)) return false;\n if (['1', 'true', 'yes', 'on'].includes(raw)) return true;\n return this.#stAuthoritative === true;\n }\n\n /**\n * Render `get_context_for_prompt` from the local store, or null to fall\n * through to the network.\n *\n * The store answers only when it holds the whole conversation: the server\n * has confirmed a count the entry accounts for. An entry that is merely\n * present is not enough. After a restart or an eviction it holds one turn,\n * and rendering that as the conversation drops everything said before it.\n */\n /**\n * `response` with the earlier turns that best match `query` (by default the\n * latest user message) appended, when a compaction exists. The search runs\n * here, over the turn archive. Python: `_with_history_excerpts`.\n */\n async #withHistoryExcerpts(conversationId: string, response: Json, query: string | undefined): Promise<Json> {\n const r = (response ?? {}) as Record<string, unknown>;\n const store = this.#turnStore;\n const recent = (Array.isArray(r['recent_messages']) ? r['recent_messages'] : []) as Array<Record<string, unknown>>;\n store.addMany(\n conversationId,\n recent.map((m) => ({\n role: String(m['role'] ?? 'user'),\n content: String(m['content'] ?? ''),\n timestamp: String(m['timestamp'] ?? ''),\n message_id: typeof m['message_id'] === 'string' ? m['message_id'] : null,\n })),\n );\n const entry = this.#shortTerm.get(conversationId);\n const total = Math.max(Number(r['total_message_count'] ?? 0) || 0, Number(entry?.serverMessageCount ?? 0) || 0);\n this.#maybeBackfillTurns(conversationId, total);\n if (!r['context_block']) return response;\n const turns = store.turns(conversationId);\n let q = query;\n if (!q) {\n const latest = [...recent].reverse().find((m) => m['role'] === 'user');\n q = latest ? String(latest['content'] ?? '') : ([...turns].reverse().find((t) => t.role === 'user')?.content ?? '');\n }\n if (!q) return response;\n const exclude = new Set<string>();\n for (const m of recent) {\n if (typeof m['message_id'] === 'string' && m['message_id']) exclude.add(m['message_id']);\n exclude.add(String(m['content'] ?? ''));\n }\n this.#startLocalModel();\n const hits = await searchTurns(turns, q, { exclude, encoder: this.#localEncoder, store });\n if (hits.length === 0) return response;\n const block = renderExcerpts(hits, turns.length ? turns[turns.length - 1]!.timestamp : '');\n return {\n ...r,\n history_excerpts: hits.map((h) => ({ position: h.position, role: h.role, text: h.text })),\n history_block: block,\n formatted_context: (String(r['formatted_context'] ?? '').trimEnd() + '\\n\\n' + block).trim(),\n } as Json;\n }\n\n /**\n * Keep what a tool returned in this process's turn archive, so history search\n * can find it after a compaction. Never sent anywhere: the server's\n * conversation holds what people said, and a compaction is made from that.\n * `instance.record_tool_result` calls this too. Python: `archive_tool_result`.\n */\n archive_tool_result(\n conversationId: string,\n result: unknown,\n options: { tool_name?: string | null; toolName?: string | null; timestamp?: string } = {},\n ): void {\n if (!conversationId || result === null || result === undefined) return;\n try {\n this.#turnStore.add(conversationId, 'tool', toolResultText(result, options.tool_name ?? options.toolName),\n options.timestamp ?? new Date().toISOString());\n } catch {\n // The archive is a convenience: never fail the agent's call.\n }\n }\n\n static readonly BACKFILL_COOLDOWN_MS = 60_000;\n static readonly BACKFILL_PAGE = 500;\n\n /** Fetch, in the background, turns the server holds and the archive does not. */\n /**\n * Load the local embedding model in the background, once. Searches and\n * lookups before it is ready match by words. Python: `_start_history_encoder`.\n */\n #startLocalModel(): void {\n if (this.#localModelStarted) return;\n this.#localModelStarted = true;\n if (localModelOff()) return;\n const name = this.#localModelName === 'auto' ? SEMANTIC_MATCH.model : this.#localModelName;\n if (!name) return;\n const callerEncoder = this.#localEncoder;\n void import('./cache/onnx-encoder.js')\n .then(({ canRunLocalModel, loadOnnxEncoder }) => (canRunLocalModel() ? loadOnnxEncoder(name) : null))\n .then((encoder) => {\n if (encoder === null) return;\n // A caller's own encoder keeps history search.\n if (callerEncoder === null) this.#localEncoder = encoder;\n // The anticipation thresholds hold for one model only.\n if (encoder.model === SEMANTIC_MATCH.model) this.#anticipationCache.setEncoder(encoder);\n })\n .catch(() => {\n /* words only, as without a model */\n });\n }\n\n #maybeBackfillTurns(conversationId: string, total: number): void {\n if (total <= this.#turnStore.count(conversationId)) return;\n const now = Date.now();\n if (now < (this.#backfillAfter.get(conversationId) ?? 0)) return;\n this.#backfillAfter.set(conversationId, now + SynapClient.BACKFILL_COOLDOWN_MS);\n void this.#backfillTurns(conversationId, total);\n }\n\n async #backfillTurns(conversationId: string, total: number): Promise<void> {\n try {\n let start = 1;\n while (start <= total) {\n const result = await this.#transport.request<Json>('conversation_turns', {\n pathParams: { conversation_id: conversationId },\n query: { start, limit: SynapClient.BACKFILL_PAGE },\n });\n const page = (((result ?? {}) as Record<string, unknown>)['turns'] ?? []) as TurnInput[];\n this.#turnStore.addMany(conversationId, page);\n if (page.length < SynapClient.BACKFILL_PAGE) break;\n start += SynapClient.BACKFILL_PAGE;\n }\n } catch (error) {\n this.#warn(`Turn backfill for ${conversationId} failed: ${String(error)}`);\n }\n }\n\n #localPrompt(conversationId: string, style: string): Json | null {\n if (!this.#isStAuthoritative()) return null;\n const entry = this.#shortTerm.get(conversationId);\n if (entry === null || !isComplete(entry)) return null;\n if (entry.compactionId === null && entry.recentTurns.length === 0) return null;\n return renderForPrompt(entry, style) as unknown as Json;\n }\n\n /** Same, for `get_compacted`. Requires an actual compaction, not just turns. */\n #localCompacted(conversationId: string, format: string): Json | null {\n if (!this.#isStAuthoritative()) return null;\n const entry = this.#shortTerm.get(conversationId);\n if (entry === null || entry.compactionId === null || !isComplete(entry)) return null;\n return renderCompacted(entry, format);\n }\n\n /**\n * Bring the local store back in step from the server's answer.\n *\n * Only for a conversation this process writes to. The server's answer lists\n * every message its summary does not cover, which is exactly what the entry\n * was missing.\n */\n #reseedShortTerm(conversationId: string, response: Json): void {\n if (!this.#isStAuthoritative()) return;\n const entry = this.#shortTerm.get(conversationId);\n if (entry === null || !entry.participant) return;\n const r = (response ?? {}) as Record<string, unknown>;\n const recent = Array.isArray(r['recent_messages']) ? r['recent_messages'] : [];\n this.#shortTerm.reseed(conversationId, {\n compactionId: typeof r['compaction_id'] === 'string' ? r['compaction_id'] : null,\n recentMessages: recent as ShortTermTurn[],\n compactedMessageCount: Number(r['compacted_message_count'] ?? 0) || 0,\n totalMessageCount: Number(r['total_message_count'] ?? 0) || 0,\n });\n }\n\n /** Every diagnostic the SDK emits passes through here. */\n #warn(message: string): void {\n if (this.#logger !== undefined) {\n this.#logger('warn', message);\n return;\n }\n // eslint-disable-next-line no-console\n console.warn(`[synap] ${message}`);\n }\n\n /** Release timers and connections. Safe to call more than once. */\n async shutdown(): Promise<void> {\n if (this.#closed) return;\n this.#closed = true;\n this.#shortTerm.clear();\n this.#turns.clear();\n // Release every registry slot pointing here, so a later construction\n // builds a fresh client rather than adopting this shut-down one. Only\n // slots still owned by this client are dropped.\n for (const key of this.#registryKeys) registry.unregisterIfOwner(key, this);\n\n // Stop the stream first: it holds a socket and a heartbeat timer, and\n // leaving either alive is what keeps a finished process from exiting.\n await this.instance.stop_listening();\n this.#compactionSubscribers.removeAll();\n this.#transport.close();\n this.#anticipationCache.clear();\n }\n}\n\nfunction pick(o: Record<string, unknown>, snake: string, camel: string): unknown {\n const a = o[snake];\n return a !== undefined ? a : o[camel];\n}\n\n/** Explicit option wins, then the environment, then the SDK default. */\nfunction resolveBaseUrl(options: SynapClientOptions): string | undefined {\n const explicit = options.baseUrl ?? options.api_base_url;\n if (explicit !== undefined) return explicit;\n const fromEnv = getEnv('SYNAP_BASE_URL')?.trim();\n return fromEnv !== undefined && fromEnv !== '' ? fromEnv : undefined;\n}\n\nfunction resolveCredentials(options: SynapClientOptions): Credentials {\n // Read at call time, not module load, so a late process.env mutation works.\n const apiKey = options.apiKey ?? getEnv('SYNAP_API_KEY');\n if (!apiKey) {\n throw new InvalidInputError(\n 'No Synap API key found. Set SYNAP_API_KEY in your environment, or pass ' +\n 'apiKey to the SynapClient constructor.',\n );\n }\n return {\n apiKey,\n clientId: options.clientId ?? getEnv('SYNAP_CLIENT_ID') ?? '',\n instanceId: options.instanceId ?? getEnv('SYNAP_INSTANCE_ID') ?? '',\n };\n}\n"]}