@maximem/synap-js-sdk 0.4.6 → 0.4.7

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/util/correlation.ts","../src/transport/endpoints.ts","../src/transport/retry.ts","../src/transport/keepalive.ts","../src/scoping.ts","../src/transport/http.ts","../src/context/fetch.ts","../src/context/normalize.ts","../src/context/flatten.ts","../src/memories/interface.ts","../src/util/validators.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/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":["o","dt","sleep","either","iso","pick","COLLECTIONS","itemId","response"],"mappings":"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","file":"/Users/anish/maximemai/maximem_synap/synap/sdk/js/dist/index.cjs","sourcesContent":[null,"/**\n * Correlation ID generation.\n *\n * `globalThis.crypto` is unflagged from Node 19 onward and this package\n * declares `engines.node >= 20`, so `randomUUID` is normally present. It is\n * still feature-detected rather than assumed, because `engines` is advisory:\n * npm warns and installs anyway, and the SDK also runs on Workers, Edge and\n * Bun, where the available crypto surface varies.\n *\n * Deliberately does NOT import `node:crypto`. That import is what breaks Edge\n * and Workers builds at bundle time, and a correlation ID is not a security\n * value, so there is nothing to gain from it.\n */\nexport function newCorrelationId(): string {\n const c = globalThis.crypto;\n if (typeof c?.randomUUID === 'function') return c.randomUUID();\n\n if (typeof c?.getRandomValues === 'function') {\n const b = c.getRandomValues(new Uint8Array(16));\n // RFC 4122 version (4) and variant bits. The `?? 0` guards satisfy\n // `noUncheckedIndexedAccess`; a 16-byte array always has these slots.\n b[6] = ((b[6] ?? 0) & 0x0f) | 0x40;\n b[8] = ((b[8] ?? 0) & 0x3f) | 0x80;\n const h = Array.from(b, (x) => x.toString(16).padStart(2, '0')).join('');\n return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;\n }\n\n // Last resort. Correlation IDs are for tracing only, never for security, so\n // Math.random is acceptable here and only here.\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (ch) => {\n const r = (Math.random() * 16) | 0;\n return (ch === 'x' ? r : (r & 0x3) | 0x8).toString(16);\n });\n}\n","/**\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 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 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\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 return new InsufficientCreditsError(message, {\n ...opts,\n balanceCredits: numberOrNull(parsed['balance_credits']),\n requiredCredits:\n numberOrNull(parsed['minimum_required_credits']) ??\n numberOrNull(parsed['required_credits']),\n recoveryUrl: stringOrNull(parsed['recovery_url']),\n redeemUrl: stringOrNull(parsed['redeem_url']),\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 const header = response.headers.get('retry-after');\n const retryAfter = header !== null && header.trim() !== '' ? Number(header) : null;\n return new RateLimitError(message, {\n ...opts,\n retryAfterSeconds: retryAfter !== null && Number.isFinite(retryAfter) ? retryAfter : null,\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 * 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 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 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 // - 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 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 FetchHooks {\n /** Returns a response to short-circuit the network, or null to continue. */\n beforeFetch?: (\n scope: Scope,\n options: FetchOptions,\n ) => { response: RawContext; servedItemIds: string[]; bundleId: string } | 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/** 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 const anticipated = hooks.beforeFetch?.(scope, options) ?? null;\n if (anticipated !== null) {\n hooks.onServedFromCache?.(scope, options, anticipated);\n hooks.onAssembled?.(scope, options, anticipated.response, startedAt);\n return anticipated.response;\n }\n\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 const response = (result?.['context'] as RawContext) ?? {};\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, 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\nfunction normalizeTemporalFields(item: RawContextItem = {}): TemporalFields {\n return {\n eventDate: pick(item['eventDate'] as string, item.event_date, 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 };\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 };\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 };\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 };\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 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 };\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\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 rawResponse: pick(context['rawResponse'] as Json, context['raw_response'] as Json, {}),\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 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 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\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 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\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 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\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(transport: HttpTransport): 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 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 * 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 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\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\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 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 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?: { conversation_id?: string; conversationId?: string; style?: string }): 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 * 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}) => void;\n\nexport function createConversationNamespace(\n transport: HttpTransport,\n subscribers: CompactionSubscribers = new CompactionSubscribers(),\n onWrite: WriteInvalidator = () => {},\n onTurn: TurnRecorder = () => {},\n /** Applied to a conversation-scoped response before it is returned. */\n onFetched: (\n response: RawContext,\n conversationId: string | undefined,\n userId: 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 } = { prompt: () => null, compacted: () => null },\n): ConversationNamespace {\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 = buildRecordMessageBody(options);\n const result = await transport.request<Json>('conversations_messages', { body });\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: String(body['conversation_id'] ?? ''),\n role: String(body['role'] ?? 'user'),\n content: String(body['content'] ?? ''),\n timestamp: body['timestamp'] as string | undefined,\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(buildRecordMessageBody);\n const result = await transport.request<Json>('conversations_messages_batch', {\n body: { messages: bodies },\n });\n for (const body of bodies) {\n onTurn({\n conversationId: String(body['conversation_id'] ?? ''),\n role: String(body['role'] ?? 'user'),\n content: String(body['content'] ?? ''),\n timestamp: body['timestamp'] as string | undefined,\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 );\n return response;\n },\n\n get_context_for_prompt: async (options = {}) => {\n const id = convId(options);\n const local = localShortTerm.prompt(id, options.style ?? 'structured');\n if (local !== null) return local;\n const result = await transport.request<Json>('context_for_prompt', {\n pathParams: { conversation_id: id },\n query: { style: options.style ?? 'structured' },\n });\n return (result?.['context_for_prompt'] as Json) ?? result ?? {};\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 {\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';\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 /**\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 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 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 /** 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 /** 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\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): InstanceNamespace {\n let client: StreamClient | null = null;\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\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 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 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 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 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 onStateChange: (state: StreamState) => {\n if (state === 'reconnecting') {\n reconnectAttempts += 1;\n onReconnect?.(reconnectAttempts);\n } else if (state === 'disconnected') {\n onDisconnect?.('stream disconnected');\n }\n },\n });\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 const active = client;\n client = null;\n await active.disconnect();\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 conversationId = options.conversation_id ?? options.conversationId ?? '';\n onTurn?.({\n conversationId,\n role: options.role ?? 'user',\n content: options.content,\n });\n client.sendConversationEvent({\n event_type: options.event_type ?? options.eventType ?? 'user_message',\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 metadata: options.metadata ?? {},\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 ? '' : JSON.stringify(a);\n })(),\n search_queries: options.search_queries ?? options.searchQueries ?? [],\n context_types: options.context_types ?? options.contextTypes ?? [],\n });\n },\n\n async record_thinking(options: RecordThinkingOptions) {\n if (client === null) throw new ListeningNotActiveError('Listening is not active');\n client.sendConversationEvent({\n // Python emits reasoning as an `agent_thinking` conversation event.\n event_type: 'agent_thinking',\n content: options.content,\n role: 'assistant',\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","/** Kept in sync with package.json by the release workflow. */\nexport const SDK_VERSION = '1.0.0-beta.0';\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\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}\n\nexport interface ShortTermEntry {\n conversationId: string;\n summary: 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}\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 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 /** 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 appendTurn(conversationId: string, role: string, content: string, timestamp?: string): void {\n if (!conversationId) return;\n const stamp = timestamp ?? new Date(this.#now()).toISOString();\n const entry = this.#entries.get(conversationId) ?? this.#blank(conversationId);\n entry.recentTurns.push({ role, content, timestamp: stamp });\n entry.lastActivityAt = parseIso(stamp) ?? this.#now();\n this.#touch(conversationId, entry);\n this.#evict();\n }\n\n /**\n * Apply a `compaction_update` bundle.\n *\n * Replaces the summary fields from the bundle's `conversation_context` and\n * prunes locally buffered turns at or before its `end_timestamp`. Turns that\n * arrived DURING the compaction window are kept by design: the server has not\n * seen them yet, which is the whole reason the local tail exists.\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 entry = this.#entries.get(conversationId) ?? this.#blank(conversationId);\n\n // `x || existing` throughout, matching Python: a blank field from the\n // server must not erase what is already held.\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 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 (endTs !== null && entry.recentTurns.length > 0) {\n entry.recentTurns = entry.recentTurns.filter((t) => isAfter(t.timestamp, endTs));\n }\n entry.lastActivityAt = this.#now();\n this.#touch(conversationId, entry);\n this.#evict();\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 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 };\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 * 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 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 conversationId: string | undefined,\n /** `st_verbatim_overlay` from the client options. The env var still wins. */\n configured?: boolean,\n): void {\n if (conversationId === undefined || 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 const localTurns = budgetRecentTurns(entry.recentTurns);\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 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 [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: `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\nfunction formatStructured(entry: ShortTermEntry, recent: LocalRecentMessage[]): string {\n const parts: string[] = [];\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 if (entry.summary) parts.push(entry.summary.trim());\n if (entry.conversationalParagraph) parts.push(entry.conversationalParagraph.trim());\n else if (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 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 };\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 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\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 } = params;\n switch (scope) {\n case 'conversation':\n return { searchQuery, entityId, customerId, clientId, conversationId };\n case 'user':\n // No conversationId: a user-scope fetch is not bound to one thread.\n return { searchQuery, entityId, customerId, clientId };\n case 'customer':\n return { searchQuery, entityId: null, customerId, clientId };\n case 'client':\n return { searchQuery, entityId: null, customerId: null, clientId };\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 function tryServeFromCache(\n cache: AnticipationCache,\n scope: Scope,\n params: AnticipationLookupParams,\n maxItems: number,\n): AnticipationAttempt {\n try {\n const scoped = scopeLookupParams(scope, params);\n const hit = cache.lookup({\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 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 * `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 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 /** 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 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 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\n out.total_items = ITEM_COLLECTIONS.reduce((n, c) => n + out[c].length, 0);\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 if (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 (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 (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 (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 (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 { InvalidInputError } from './errors.js';\nimport { getEnv } from './util/env.js';\nimport { HttpTransport, type Credentials, type HttpTransportOptions } from './transport/http.js';\nimport { fetchContext } 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 } from './cache/short-term-store.js';\nimport { overlayLocalRecentTurns } from './context/overlay.js';\nimport { renderForPrompt, renderCompacted } from './context/local-prompt.js';\nimport { TurnCounter, mergeUserSummary } from './context/user-summary.js';\nimport { tryServeFromCache, servedItemIds, type Scope as AnticipationScope } from './context/anticipated.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 /**\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` also enables it.\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 * 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 /** 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. */\nexport interface UnifiedFetchOptions {\n conversation_id?: string;\n user_id?: string;\n customer_id?: string;\n search_query?: string[];\n max_results?: number;\n types?: string[];\n mode?: string;\n precision_level?: string;\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\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 readonly #stVerbatimOverlay!: boolean | undefined;\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: (scope, options) => {\n const o = options as Record<string, unknown>;\n const attempt = 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 },\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 };\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 this.#stVerbatimOverlay = options.st_verbatim_overlay ?? options.stVerbatimOverlay;\n this.#logger = options.logger;\n this.#stAuthoritative = options.sdk_st_authoritative ?? options.sdkStAuthoritative;\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 }) => {\n this.#shortTerm.appendTurn(conversationId, role, content, timestamp);\n },\n (response, conversationId, userId) => {\n overlayLocalRecentTurns(response, this.#shortTerm, conversationId, this.#stVerbatimOverlay);\n this.#injectUserSummary(response, conversationId, userId);\n },\n {\n prompt: (conversationId, style) => this.#localPrompt(conversationId, style),\n compacted: (conversationId, format) => this.#localCompacted(conversationId, format),\n },\n );\n this.memories = createMemoriesInterface(this.#transport);\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 );\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 this.instance._internal.setCompactionApplier((bundle) => {\n this.#shortTerm.applyCompaction(bundle);\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 }\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 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 } = options;\n\n validateConversationId(conversationId);\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 };\n\n const labels: string[] = [];\n const tasks: Array<Promise<RawContext>> = [];\n\n if (conversationId !== undefined && 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 (userId !== undefined && 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 (customerId !== undefined && 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 && conversationId !== undefined && 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 });\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 ): void {\n this.#turns.increment(conversationId);\n if (userId === undefined || userId === '') return;\n if (!this.#turns.shouldInject(conversationId)) return;\n const summary = this.#anticipationCache.lookupUserSummary(userId);\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`: the explicit\n * option first, then `SYNAP_SDK_ST_AUTHORITATIVE` (truthy strings only).\n * Defaults to false, so nothing changes for a caller who never sets it.\n */\n #isStAuthoritative(): boolean {\n if (this.#stAuthoritative === true) return true;\n const raw = (getEnv('SYNAP_SDK_ST_AUTHORITATIVE') ?? '').trim().toLowerCase();\n return ['1', 'true', 'yes', 'on'].includes(raw);\n }\n\n /**\n * Render `get_context_for_prompt` from the local store, or null to fall\n * through to the network. Warm means a compaction landed OR raw turns are\n * buffered: with neither there is nothing to render and the server's answer\n * is the only real one.\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) 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) return null;\n return renderCompacted(entry, format);\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":["/private/tmp/claude-501/-Users-scaler-maximem-synap/941df347-d7e5-4dc7-a714-e7bd41f7ddd2/scratchpad/wt-scopefix/synap/sdk/js/dist/index.cjs","../src/util/correlation.ts","../src/transport/endpoints.ts","../src/transport/retry.ts","../src/transport/keepalive.ts","../src/scoping.ts","../src/transport/http.ts","../src/context/fetch.ts","../src/context/normalize.ts","../src/context/flatten.ts","../src/memories/interface.ts","../src/util/validators.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/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":["o","dt","sleep","either","iso","pick","COLLECTIONS","itemId","response"],"mappings":"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","file":"/private/tmp/claude-501/-Users-scaler-maximem-synap/941df347-d7e5-4dc7-a714-e7bd41f7ddd2/scratchpad/wt-scopefix/synap/sdk/js/dist/index.cjs","sourcesContent":[null,"/**\n * Correlation ID generation.\n *\n * `globalThis.crypto` is unflagged from Node 19 onward and this package\n * declares `engines.node >= 20`, so `randomUUID` is normally present. It is\n * still feature-detected rather than assumed, because `engines` is advisory:\n * npm warns and installs anyway, and the SDK also runs on Workers, Edge and\n * Bun, where the available crypto surface varies.\n *\n * Deliberately does NOT import `node:crypto`. That import is what breaks Edge\n * and Workers builds at bundle time, and a correlation ID is not a security\n * value, so there is nothing to gain from it.\n */\nexport function newCorrelationId(): string {\n const c = globalThis.crypto;\n if (typeof c?.randomUUID === 'function') return c.randomUUID();\n\n if (typeof c?.getRandomValues === 'function') {\n const b = c.getRandomValues(new Uint8Array(16));\n // RFC 4122 version (4) and variant bits. The `?? 0` guards satisfy\n // `noUncheckedIndexedAccess`; a 16-byte array always has these slots.\n b[6] = ((b[6] ?? 0) & 0x0f) | 0x40;\n b[8] = ((b[8] ?? 0) & 0x3f) | 0x80;\n const h = Array.from(b, (x) => x.toString(16).padStart(2, '0')).join('');\n return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;\n }\n\n // Last resort. Correlation IDs are for tracing only, never for security, so\n // Math.random is acceptable here and only here.\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (ch) => {\n const r = (Math.random() * 16) | 0;\n return (ch === 'x' ? r : (r & 0x3) | 0x8).toString(16);\n });\n}\n","/**\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 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 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\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 return new InsufficientCreditsError(message, {\n ...opts,\n balanceCredits: numberOrNull(parsed['balance_credits']),\n requiredCredits:\n numberOrNull(parsed['minimum_required_credits']) ??\n numberOrNull(parsed['required_credits']),\n recoveryUrl: stringOrNull(parsed['recovery_url']),\n redeemUrl: stringOrNull(parsed['redeem_url']),\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 const header = response.headers.get('retry-after');\n const retryAfter = header !== null && header.trim() !== '' ? Number(header) : null;\n return new RateLimitError(message, {\n ...opts,\n retryAfterSeconds: retryAfter !== null && Number.isFinite(retryAfter) ? retryAfter : null,\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 * 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 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 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 // - 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 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 FetchHooks {\n /** Returns a response to short-circuit the network, or null to continue. */\n beforeFetch?: (\n scope: Scope,\n options: FetchOptions,\n ) => { response: RawContext; servedItemIds: string[]; bundleId: string } | 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/** 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 const anticipated = hooks.beforeFetch?.(scope, options) ?? null;\n if (anticipated !== null) {\n hooks.onServedFromCache?.(scope, options, anticipated);\n hooks.onAssembled?.(scope, options, anticipated.response, startedAt);\n return anticipated.response;\n }\n\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 const response = (result?.['context'] as RawContext) ?? {};\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, 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\nfunction normalizeTemporalFields(item: RawContextItem = {}): TemporalFields {\n return {\n eventDate: pick(item['eventDate'] as string, item.event_date, 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 };\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 };\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 };\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 };\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 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 };\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\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 rawResponse: pick(context['rawResponse'] as Json, context['raw_response'] as Json, {}),\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 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 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\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 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(transport: HttpTransport): 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 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 * 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 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\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\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 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 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?: { conversation_id?: string; conversationId?: string; style?: string }): 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 * 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}) => void;\n\nexport function createConversationNamespace(\n transport: HttpTransport,\n subscribers: CompactionSubscribers = new CompactionSubscribers(),\n onWrite: WriteInvalidator = () => {},\n onTurn: TurnRecorder = () => {},\n /** Applied to a conversation-scoped response before it is returned. */\n onFetched: (\n response: RawContext,\n conversationId: string | undefined,\n userId: 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 } = { prompt: () => null, compacted: () => null },\n): ConversationNamespace {\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 = buildRecordMessageBody(options);\n const result = await transport.request<Json>('conversations_messages', { body });\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: String(body['conversation_id'] ?? ''),\n role: String(body['role'] ?? 'user'),\n content: String(body['content'] ?? ''),\n timestamp: body['timestamp'] as string | undefined,\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(buildRecordMessageBody);\n const result = await transport.request<Json>('conversations_messages_batch', {\n body: { messages: bodies },\n });\n for (const body of bodies) {\n onTurn({\n conversationId: String(body['conversation_id'] ?? ''),\n role: String(body['role'] ?? 'user'),\n content: String(body['content'] ?? ''),\n timestamp: body['timestamp'] as string | undefined,\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 );\n return response;\n },\n\n get_context_for_prompt: async (options = {}) => {\n const id = convId(options);\n const local = localShortTerm.prompt(id, options.style ?? 'structured');\n if (local !== null) return local;\n const result = await transport.request<Json>('context_for_prompt', {\n pathParams: { conversation_id: id },\n query: { style: options.style ?? 'structured' },\n });\n return (result?.['context_for_prompt'] as Json) ?? result ?? {};\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 {\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';\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 /**\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 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 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 /** 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 /** 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\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): InstanceNamespace {\n let client: StreamClient | null = null;\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\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 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 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 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 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 onStateChange: (state: StreamState) => {\n if (state === 'reconnecting') {\n reconnectAttempts += 1;\n onReconnect?.(reconnectAttempts);\n } else if (state === 'disconnected') {\n onDisconnect?.('stream disconnected');\n }\n },\n });\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 const active = client;\n client = null;\n await active.disconnect();\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 conversationId = options.conversation_id ?? options.conversationId ?? '';\n onTurn?.({\n conversationId,\n role: options.role ?? 'user',\n content: options.content,\n });\n client.sendConversationEvent({\n event_type: options.event_type ?? options.eventType ?? 'user_message',\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 metadata: options.metadata ?? {},\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 ? '' : JSON.stringify(a);\n })(),\n search_queries: options.search_queries ?? options.searchQueries ?? [],\n context_types: options.context_types ?? options.contextTypes ?? [],\n });\n },\n\n async record_thinking(options: RecordThinkingOptions) {\n if (client === null) throw new ListeningNotActiveError('Listening is not active');\n client.sendConversationEvent({\n // Python emits reasoning as an `agent_thinking` conversation event.\n event_type: 'agent_thinking',\n content: options.content,\n role: 'assistant',\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","/** Kept in sync with package.json by the release workflow. */\nexport const SDK_VERSION = '1.0.0-beta.0';\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\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}\n\nexport interface ShortTermEntry {\n conversationId: string;\n summary: 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}\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 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 /** 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 appendTurn(conversationId: string, role: string, content: string, timestamp?: string): void {\n if (!conversationId) return;\n const stamp = timestamp ?? new Date(this.#now()).toISOString();\n const entry = this.#entries.get(conversationId) ?? this.#blank(conversationId);\n entry.recentTurns.push({ role, content, timestamp: stamp });\n entry.lastActivityAt = parseIso(stamp) ?? this.#now();\n this.#touch(conversationId, entry);\n this.#evict();\n }\n\n /**\n * Apply a `compaction_update` bundle.\n *\n * Replaces the summary fields from the bundle's `conversation_context` and\n * prunes locally buffered turns at or before its `end_timestamp`. Turns that\n * arrived DURING the compaction window are kept by design: the server has not\n * seen them yet, which is the whole reason the local tail exists.\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 entry = this.#entries.get(conversationId) ?? this.#blank(conversationId);\n\n // `x || existing` throughout, matching Python: a blank field from the\n // server must not erase what is already held.\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 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 (endTs !== null && entry.recentTurns.length > 0) {\n entry.recentTurns = entry.recentTurns.filter((t) => isAfter(t.timestamp, endTs));\n }\n entry.lastActivityAt = this.#now();\n this.#touch(conversationId, entry);\n this.#evict();\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 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 };\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 * 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 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 conversationId: string | undefined,\n /** `st_verbatim_overlay` from the client options. The env var still wins. */\n configured?: boolean,\n): void {\n if (conversationId === undefined || 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 const localTurns = budgetRecentTurns(entry.recentTurns);\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 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 [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: `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\nfunction formatStructured(entry: ShortTermEntry, recent: LocalRecentMessage[]): string {\n const parts: string[] = [];\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 if (entry.summary) parts.push(entry.summary.trim());\n if (entry.conversationalParagraph) parts.push(entry.conversationalParagraph.trim());\n else if (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 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 };\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 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\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 } = params;\n switch (scope) {\n case 'conversation':\n return { searchQuery, entityId, customerId, clientId, conversationId };\n case 'user':\n // No conversationId: a user-scope fetch is not bound to one thread.\n return { searchQuery, entityId, customerId, clientId };\n case 'customer':\n return { searchQuery, entityId: null, customerId, clientId };\n case 'client':\n return { searchQuery, entityId: null, customerId: null, clientId };\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 function tryServeFromCache(\n cache: AnticipationCache,\n scope: Scope,\n params: AnticipationLookupParams,\n maxItems: number,\n): AnticipationAttempt {\n try {\n const scoped = scopeLookupParams(scope, params);\n const hit = cache.lookup({\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 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 * `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 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 /** 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 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 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\n out.total_items = ITEM_COLLECTIONS.reduce((n, c) => n + out[c].length, 0);\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 if (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 (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 (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 (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 (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 { InvalidInputError } from './errors.js';\nimport { getEnv } from './util/env.js';\nimport { HttpTransport, type Credentials, type HttpTransportOptions } from './transport/http.js';\nimport { fetchContext } 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 } from './cache/short-term-store.js';\nimport { overlayLocalRecentTurns } from './context/overlay.js';\nimport { renderForPrompt, renderCompacted } from './context/local-prompt.js';\nimport { TurnCounter, mergeUserSummary } from './context/user-summary.js';\nimport { tryServeFromCache, servedItemIds, type Scope as AnticipationScope } from './context/anticipated.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 /**\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` also enables it.\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 * 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 /** 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. */\nexport interface UnifiedFetchOptions {\n conversation_id?: string;\n user_id?: string;\n customer_id?: string;\n search_query?: string[];\n max_results?: number;\n types?: string[];\n mode?: string;\n precision_level?: string;\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\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 readonly #stVerbatimOverlay!: boolean | undefined;\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: (scope, options) => {\n const o = options as Record<string, unknown>;\n const attempt = 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 },\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 };\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 this.#stVerbatimOverlay = options.st_verbatim_overlay ?? options.stVerbatimOverlay;\n this.#logger = options.logger;\n this.#stAuthoritative = options.sdk_st_authoritative ?? options.sdkStAuthoritative;\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 }) => {\n this.#shortTerm.appendTurn(conversationId, role, content, timestamp);\n },\n (response, conversationId, userId) => {\n overlayLocalRecentTurns(response, this.#shortTerm, conversationId, this.#stVerbatimOverlay);\n this.#injectUserSummary(response, conversationId, userId);\n },\n {\n prompt: (conversationId, style) => this.#localPrompt(conversationId, style),\n compacted: (conversationId, format) => this.#localCompacted(conversationId, format),\n },\n );\n this.memories = createMemoriesInterface(this.#transport);\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 );\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 this.instance._internal.setCompactionApplier((bundle) => {\n this.#shortTerm.applyCompaction(bundle);\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 }\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 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 } = options;\n\n validateConversationId(conversationId);\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 };\n\n const labels: string[] = [];\n const tasks: Array<Promise<RawContext>> = [];\n\n if (conversationId !== undefined && 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 (userId !== undefined && 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 (customerId !== undefined && 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 && conversationId !== undefined && 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 });\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 ): void {\n this.#turns.increment(conversationId);\n if (userId === undefined || userId === '') return;\n if (!this.#turns.shouldInject(conversationId)) return;\n const summary = this.#anticipationCache.lookupUserSummary(userId);\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`: the explicit\n * option first, then `SYNAP_SDK_ST_AUTHORITATIVE` (truthy strings only).\n * Defaults to false, so nothing changes for a caller who never sets it.\n */\n #isStAuthoritative(): boolean {\n if (this.#stAuthoritative === true) return true;\n const raw = (getEnv('SYNAP_SDK_ST_AUTHORITATIVE') ?? '').trim().toLowerCase();\n return ['1', 'true', 'yes', 'on'].includes(raw);\n }\n\n /**\n * Render `get_context_for_prompt` from the local store, or null to fall\n * through to the network. Warm means a compaction landed OR raw turns are\n * buffered: with neither there is nothing to render and the server's answer\n * is the only real one.\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) 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) return null;\n return renderCompacted(entry, format);\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"]}