@maximem/synap-js-sdk 0.2.4 → 1.0.0-beta.0
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.
- package/MIGRATING.md +146 -0
- package/README.md +96 -83
- package/bin/synap-js-sdk.js +45 -122
- package/dist/anticipation-cache-BgkqcTPJ.d.cts +180 -0
- package/dist/anticipation-cache-BgkqcTPJ.d.ts +180 -0
- package/dist/anticipation-cache-D5YHEPV3.js +10 -0
- package/dist/anticipation-cache-D5YHEPV3.js.map +1 -0
- package/dist/anticipation-cache-JBZHTJ3O.cjs +10 -0
- package/dist/anticipation-cache-JBZHTJ3O.cjs.map +1 -0
- package/dist/chunk-5SC3ELNK.js +856 -0
- package/dist/chunk-5SC3ELNK.js.map +1 -0
- package/dist/chunk-HR5Y3KXS.cjs +172 -0
- package/dist/chunk-HR5Y3KXS.cjs.map +1 -0
- package/dist/chunk-NIDPTBNC.cjs +800 -0
- package/dist/chunk-NIDPTBNC.cjs.map +1 -0
- package/dist/chunk-QBCVMPGK.js +28 -0
- package/dist/chunk-QBCVMPGK.js.map +1 -0
- package/dist/chunk-T3NZBIV2.js +172 -0
- package/dist/chunk-T3NZBIV2.js.map +1 -0
- package/dist/chunk-TCEP5OVR.js +800 -0
- package/dist/chunk-TCEP5OVR.js.map +1 -0
- package/dist/chunk-VXRCBMCY.cjs +856 -0
- package/dist/chunk-VXRCBMCY.cjs.map +1 -0
- package/dist/chunk-ZKOJDUN4.cjs +28 -0
- package/dist/chunk-ZKOJDUN4.cjs.map +1 -0
- package/dist/env-55ULHNJZ.cjs +11 -0
- package/dist/env-55ULHNJZ.cjs.map +1 -0
- package/dist/env-QCUKKW36.js +11 -0
- package/dist/env-QCUKKW36.js.map +1 -0
- package/dist/grpc/index.cjs +87 -0
- package/dist/grpc/index.cjs.map +1 -0
- package/dist/grpc/index.d.cts +162 -0
- package/dist/grpc/index.d.ts +162 -0
- package/dist/grpc/index.js +87 -0
- package/dist/grpc/index.js.map +1 -0
- package/dist/index.cjs +2111 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1477 -0
- package/dist/index.d.ts +1477 -0
- package/dist/index.js +2111 -0
- package/dist/index.js.map +1 -0
- package/dist/stream-client-ME5N5NK4.js +7 -0
- package/dist/stream-client-ME5N5NK4.js.map +1 -0
- package/dist/stream-client-ZN6ADZBR.cjs +7 -0
- package/dist/stream-client-ZN6ADZBR.cjs.map +1 -0
- package/package.json +68 -19
- package/.env.example +0 -39
- package/bridge/synap_bridge.py +0 -708
- package/src/bridge-manager.js +0 -262
- package/src/errors.js +0 -151
- package/src/index.js +0 -13
- package/src/runtime.js +0 -161
- package/src/setup-typescript.js +0 -161
- package/src/synap-client.js +0 -363
- package/types/index.d.ts +0 -384
|
@@ -0,0 +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/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/context/unified.ts","../src/tool/as-tool.ts","../src/client.ts"],"names":["o","dt","sleep","either","response","pick"],"mappings":"AAAA;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,wDAA6B;AAC7B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,wDAA6B;AAC7B;AACE;AACF,wDAA6B;AAC7B;AACA;AC1BO,SAAS,gBAAA,CAAA,EAA2B;AACzC,EAAA,MAAM,EAAA,EAAI,UAAA,CAAW,MAAA;AACrB,EAAA,GAAA,CAAI,uBAAO,CAAA,2BAAG,aAAA,IAAe,UAAA,EAAY,OAAO,CAAA,CAAE,UAAA,CAAW,CAAA;AAE7D,EAAA,GAAA,CAAI,uBAAO,CAAA,6BAAG,kBAAA,IAAoB,UAAA,EAAY;AAC5C,IAAA,MAAM,EAAA,EAAI,CAAA,CAAE,eAAA,CAAgB,IAAI,UAAA,CAAW,EAAE,CAAC,CAAA;AAG9C,IAAA,CAAA,CAAE,CAAC,EAAA,EAAA,kBAAM,CAAA,CAAE,CAAC,CAAA,UAAK,GAAA,EAAA,EAAK,GAAA,EAAQ,EAAA;AAC9B,IAAA,CAAA,CAAE,CAAC,EAAA,EAAA,kBAAM,CAAA,CAAE,CAAC,CAAA,UAAK,GAAA,EAAA,EAAK,GAAA,EAAQ,GAAA;AAC9B,IAAA,MAAM,EAAA,EAAI,KAAA,CAAM,IAAA,CAAK,CAAA,EAAG,CAAC,CAAA,EAAA,GAAM,CAAA,CAAE,QAAA,CAAS,EAAE,CAAA,CAAE,QAAA,CAAS,CAAA,EAAG,GAAG,CAAC,CAAA,CAAE,IAAA,CAAK,EAAE,CAAA;AACvE,IAAA,OAAO,CAAA,EAAA;AACT,EAAA;AAIO,EAAA;AACC,IAAA;AACE,IAAA;AACT,EAAA;AACH;ADsBY;AACA;AEpCC;AACD,EAAA;AACV,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACF;AASgB;AAIP,EAAA;AACC,IAAA;AACF,IAAA;AACI,MAAA;AACJ,QAAA;AACF,MAAA;AACF,IAAA;AACO,IAAA;AACR,EAAA;AACH;AF2BY;AACA;AGhEN;AACJ,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACD;AAYY;AACX,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAyBgB;AACN,EAAA;AAEJ,EAAA;AACK,IAAA;AACT,EAAA;AAEK,EAAA;AACI,IAAA;AACT,EAAA;AAEM,EAAA;AACF,EAAA;AAGK,IAAA;AACL,MAAA;AACA,MAAA;AACA,MAAA;AACF,IAAA;AACF,EAAA;AAEO,EAAA;AACE,IAAA;AACP,IAAA;AACQ,IAAA;AACV,EAAA;AACF;AAGgB;AACV,EAAA;AAGK,IAAA;AACT,EAAA;AACM,EAAA;AACA,EAAA;AACD,EAAA;AAEE,EAAA;AACT;AHoBY;AACA;AIpHC;AAEA;AACA;AAUT;AAGY;AAEZ,EAAA;AAGQ,EAAA;AAEZ;AAQA;AAGM,EAAA;AACC,EAAA;AACH,IAAA;AACO,IAAA;AACT,EAAA;AACI,EAAA;AAEI,IAAA;AAAgB;AAA0B,MAAA;AAAQ,IAAA;AAGxD,IAAA;AACE,MAAA;AAA+C;AAE/C,MAAA;AACA,MAAA;AACD,IAAA;AACK,EAAA;AAGN,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAea;AAGX,EAAA;AACmB,IAAA;AACA,IAAA;AAChB,EAAA;AAFgB,EAAA;AACA,EAAA;AAJoC,iBAAA;AAOzC,EAAA;AACR,IAAA;AAEA,IAAA;AACA,IAAA;AAEC,IAAA;AAGE,MAAA;AAA2C,MAAA;AAC1C,IAAA;AAEF,oBAAA;AACR,EAAA;AAEa,EAAA;AACP,IAAA;AACF,MAAA;AACK,MAAA;AACP,IAAA;AACF,EAAA;AAEI,EAAA;AACK,IAAA;AACT,EAAA;AACF;AJuEY;AACA;AKvKC;AASA;AA0CA;AACF,EAAA;AAAA;AAAA;AAGT,EAAA;AACA,EAAA;AAEA,EAAA;AACiB,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,kBAAA;AAEjB,EAAA;AACO,IAAA;AAEA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACG,MAAA;AACN,uCAAA;AACF,IAAA;AACF,EAAA;AAAA;AAGA,EAAA;AACO,IAAA;AACP,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAgBA,EAAA;AACS,IAAA;AACT,EAAA;AAAA;AAGA,EAAA;AACO,IAAA;AACP,EAAA;AAEA,EAAA;AACO,IAAA;AACP,EAAA;AAAA;AAGA,EAAA;AACO,IAAA;AAGP,EAAA;AAEc,EAAA;AACP,IAAA;AACA,IAAA;AACP,EAAA;AAEM,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AAEF,IAAA;AACA,IAAA;AAEG,IAAA;AACL,MAAA;AACI,MAAA;AACF,QAAA;AACF,MAAA;AACE,QAAA;AACA,QAAA;AACE,UAAA;AACA,UAAA;AACA,UAAA;AACA,UAAA;AACA,UAAA;AACA,UAAA;AACD,QAAA;AACG,QAAA;AACJ,QAAA;AACF,MAAA;AACF,IAAA;AACM,IAAA;AACR,EAAA;AAEc,EAAA;AAKN,IAAA;AACA,IAAA;AAEA,IAAA;AACA,IAAA;AAEA,IAAA;AACJ,MAAA;AACA,MAAA;AACA,MAAA;AACF,IAAA;AACI,IAAA;AAGG,MAAA;AAGP,IAAA;AAKI,IAAA;AAEA,IAAA;AACA,IAAA;AACF,MAAA;AACF,IAAA;AACQ,MAAA;AACR,IAAA;AACE,MAAA;AACF,IAAA;AAEK,IAAA;AACG,MAAA;AACR,IAAA;AAEI,IAAA;AACE,IAAA;AACD,IAAA;AACD,IAAA;AACF,MAAA;AACM,IAAA;AACA,MAAA;AACR,IAAA;AACF,EAAA;AAEiB,EAAA;AACT,IAAA;AACN,IAAA;AACM,MAAA;AACN,IAAA;AACO,IAAA;AACT,EAAA;AAEQ,EAAA;AAIC,IAAA;AACL,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACI,MAAA;AACJ,MAAA;AACF,IAAA;AACF,EAAA;AAAA;AAGc,EAAA;AACN,IAAA;AACF,IAAA;AACI,MAAA;AACA,MAAA;AACF,MAAA;AACE,MAAA;AACR,IAAA;AACE,MAAA;AACF,IAAA;AACF,EAAA;AACF;AAUgB;AAEV,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AAIA,EAAA;AAGA,EAAA;AAEG,EAAA;AACT;AAES;AACD,EAAA;AAGJ,EAAA;AAMJ;AAES;AACH,EAAA;AACE,EAAA;AACF,EAAA;AACK,IAAA;AACT,EAAA;AACO,EAAA;AACL,IAAA;AACE,IAAA;AACJ,EAAA;AACF;AAEA;AACQ,EAAA;AACF,EAAA;AACA,EAAA;AACF,IAAA;AACM,EAAA;AAER,EAAA;AAIM,EAAA;AACA,EAAA;AAIA,EAAA;AAIA,EAAA;AACA,EAAA;AAEE,EAAA;AACD,IAAA;AACA,IAAA;AACH,MAAA;AACG,IAAA;AACA,IAAA;AACH,MAAA;AACG,IAAA;AACH,MAAA;AACG,IAAA;AACH,MAAA;AACK,QAAA;AACH,QAAA;AACA,QAAA;AACD,MAAA;AACE,IAAA;AAOC,MAAA;AACF,QAAA;AACF,MAAA;AACA,MAAA;AACG,IAAA;AACG,MAAA;AACA,MAAA;AACN,MAAA;AACK,QAAA;AACH,QAAA;AACD,MAAA;AACH,IAAA;AACK,IAAA;AACH,MAAA;AACG,IAAA;AACH,MAAA;AACF,IAAA;AACM,MAAA;AACJ,MAAA;AACJ,EAAA;AACF;AAES;AACA,EAAA;AACT;AAiBS;AACD,EAAA;AACA,EAAA;AACA,EAAA;AACJ,IAAA;AACG,EAAA;AAC6B,kBAAA;AAE9B,EAAA;AACE,IAAA;AACC,IAAA;AACP,EAAA;AAEO,EAAA;AACG,IAAA;AACR,IAAA;AACE,MAAA;AACA,sBAAA;AACF,IAAA;AACF,EAAA;AACF;AAaS;AACG,EAAA;AACH,EAAA;AACC,IAAA;AACJ,sBAAA;AACA,MAAA;AACG,IAAA;AACL,IAAA;AACE,MAAA;AACA,MAAA;AACF,IAAA;AACQ,oBAAA;AACT,EAAA;AACH;ALQY;AACA;AMtcN;AACE,EAAA;AACI,EAAA;AACF,EAAA;AACR,EAAA;AACF;AAGS;AACG,EAAA;AACA,EAAA;AACA,EAAA;AACH,EAAA;AACT;AAOgB;AACR,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AAEF,EAAA;AACI,IAAA;AACR,EAAA;AACI,EAAA;AACI,IAAA;AACR,EAAA;AAWM,EAAA;AACJ,IAAA;AACA,IAAA;AACA,IAAA;AACO,IAAA;AACP,IAAA;AACF,EAAA;AACI,EAAA;AACA,EAAA;AACG,IAAA;AACP,EAAA;AAEQ,EAAA;AACD,IAAA;AACE,MAAA;AACA,MAAA;AAID,MAAA;AACJ,MAAA;AACG,IAAA;AACE,MAAA;AACA,MAAA;AACL,MAAA;AACG,IAAA;AACE,MAAA;AACD,MAAA;AACA,MAAA;AACJ,MAAA;AACG,IAAA;AACH,MAAA;AACJ,EAAA;AAES,EAAA;AACX;AAGA;AAKU,EAAA;AACF,EAAA;AAEE,EAAA;AACV;ANyaY;AACA;AO3fI;AACd,EAAA;AACO,EAAA;AACT;AAES;AACA,EAAA;AACT;AAEa;AAEJ;AACA,EAAA;AACL,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACF;AAEgB;AACP,EAAA;AACD,IAAA;AACJ,IAAA;AACA,IAAA;AACQ,IAAA;AACR,IAAA;AACA,IAAA;AACG,IAAA;AACL,EAAA;AACF;AAEgB;AACP,EAAA;AACD,IAAA;AACJ,IAAA;AACA,IAAA;AACA,IAAA;AACQ,IAAA;AACR,IAAA;AACA,IAAA;AACG,IAAA;AACL,EAAA;AACF;AAEgB;AACP,EAAA;AACD,IAAA;AACJ,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACG,IAAA;AACL,EAAA;AACF;AAEgB;AACP,EAAA;AACD,IAAA;AACJ,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACG,IAAA;AACL,EAAA;AACF;AAEgB;AACP,EAAA;AACD,IAAA;AACJ,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACQ,IAAA;AACR,IAAA;AACA,IAAA;AACF,EAAA;AACF;AAEgB;AACP,EAAA;AACL,IAAA;AACA,IAAA;AACS,IAAA;AACT,IAAA;AACA,IAAA;AACE,MAAA;AACA,MAAA;AACA,MAAA;AACF,IAAA;AACF,EAAA;AACF;AAEgB;AAGT,EAAA;AACE,EAAA;AACL,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACF;AAGgB;AACR,EAAA;AACC,EAAA;AACG,IAAA;AACR,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AAGA,IAAA;AACE,MAAA;AACE,QAAA;AACA,QAAA;AACA,QAAA;AACF,MAAA;AACF,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACF;AP6eY;AACA;AQ5mBD;AACY;AACZ;AAEC;AAEH;AACA,EAAA;AACL,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACF;AAEgB;AACR,EAAA;AAEN,EAAA;AACQ,IAAA;AACE,MAAA;AAAU,MAAA;AAAyB,MAAA;AACzC,MAAA;AAAwB,MAAA;AACxB,MAAA;AAAyB,MAAA;AAC1B,IAAA;AACH,EAAA;AACA,EAAA;AACQ,IAAA;AACE,MAAA;AAAU,MAAA;AAAyB,MAAA;AACzC,MAAA;AAAwB,MAAA;AACxB,MAAA;AAA+B,MAAA;AAChC,IAAA;AACH,EAAA;AACA,EAAA;AAEQ,IAAA;AACE,MAAA;AAAU,MAAA;AAAyB,MAAA;AACzC,MAAA;AAA4B,MAAA;AAA4B,MAAA;AACzD,IAAA;AACH,EAAA;AACA,EAAA;AAEQ,IAAA;AACE,MAAA;AAAU,MAAA;AAAyB,MAAA;AACzC,MAAA;AAA4B,MAAA;AAA4B,MAAA;AACzD,IAAA;AACH,EAAA;AACA,EAAA;AACQ,IAAA;AACE,MAAA;AAAU,MAAA;AAAyB,MAAA;AACzC,MAAA;AAAwB,MAAA;AACxB,MAAA;AAAmC,MAAA;AACpC,IAAA;AACH,EAAA;AAEO,EAAA;AACT;AR2nBY;AACA;ASnrBC;AACX,EAAA;AAAwB,EAAA;AAAY,EAAA;AAAS,EAAA;AAAO,EAAA;AAAS,EAAA;AAAS,EAAA;AACxE;AAIa;AAGA;AACA;AA0BJ;AACGA,EAAAA;AACA,EAAA;AACAA,EAAAA;AACH,EAAA;AACT;AAES;AACG,EAAA;AACH,EAAA;AACT;AAGgB;AACT,EAAA;AAECA,EAAAA;AACA,EAAA;AACA,EAAA;AAKA,EAAA;AACE,IAAA;AACJ,MAAA;AACF,IAAA;AACF,EAAA;AACM,EAAA;AACE,IAAA;AACJ,MAAA;AACF,IAAA;AACF,EAAA;AAIO,EAAA;AACL,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACE,MAAA;AACF,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACF;AAqEM;AAEU;AACP,EAAA;AACC,IAAA;AACJ,MAAA;AACF,IAAA;AAEM,IAAA;AACE,MAAA;AACD,MAAA;AACH,QAAA;AACF,MAAA;AACM,MAAA;AACN,MAAA;AACE,QAAA;AACD,MAAA;AACH,IAAA;AAEM,IAAA;AACE,MAAA;AACA,MAAA;AACD,MAAA;AACA,MAAA;AAEC,MAAA;AACA,MAAA;AACF,MAAA;AAIF,QAAA;AACF,MAAA;AACI,MAAA;AACF,QAAA;AACE,UAAA;AACF,QAAA;AACF,MAAA;AAEM,MAAA;AACD,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AAIC,MAAA;AACF,MAAA;AACA,MAAA;AACF,QAAA;AACF,MAAA;AAEI,MAAA;AAGF,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACF,MAAA;AACE,QAAA;AAIA,QAAA;AACF,MAAA;AACE,QAAA;AACF,MAAA;AAEA,MAAA;AACE,QAAA;AACA,QAAA;AACD,MAAA;AACH,IAAA;AAEM,IAAA;AACC,MAAA;AACL,MAAA;AACF,IAAA;AAEM,IAAA;AACE,MAAA;AACD,MAAA;AACA,MAAA;AACC,MAAA;AACJ,QAAA;AACA,QAAA;AACF,MAAA;AACMC,MAAAA;AACFA,MAAAA;AACA,MAAA;AAEJ,MAAA;AACE,QAAA;AACA,QAAA;AACD,MAAA;AACH,IAAA;AAEM,IAAA;AAIC,MAAA;AACH,QAAA;AACE,UAAA;AAIF,QAAA;AACF,MAAA;AACA,MAAA;AACF,IAAA;AAEM,IAAA;AACC,MAAA;AACL,MAAA;AACE,QAAA;AACD,MAAA;AACH,IAAA;AAEM,IAAA;AACC,MAAA;AACC,MAAA;AAEA,MAAA;AAGA,MAAA;AACF,MAAA;AACJ,MAAA;AACE,QAAA;AACA,QAAA;AACI,QAAA;AACA,QAAA;AAGF,UAAA;AACF,QAAA;AACA,QAAA;AACF,MAAA;AACF,IAAA;AACF,EAAA;AACF;AAESC;AACG,EAAA;AACH,EAAA;AACC,IAAA;AACJ,sBAAA;AACA,MAAA;AACG,IAAA;AAOL,IAAA;AACE,MAAA;AACA,MAAA;AACF,IAAA;AACQ,oBAAA;AACT,EAAA;AACH;AToiBY;AACA;AUn3BN;AAKA;AAUU;AACV,EAAA;AACA,EAAA;AAEI,IAAA;AACR,EAAA;AACF;AAUgB;AACV,EAAA;AACA,EAAA;AACI,IAAA;AACR,EAAA;AACF;AV81BY;AACA;AWx1BHC;AACGH,EAAAA;AACA,EAAA;AACAA,EAAAA;AACH,EAAA;AACT;AAES;AACG,EAAA;AACH,EAAA;AACT;AAEgB;AACRA,EAAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AAED,EAAA;AAIL,EAAA;AACK,EAAA;AACD,EAAA;AACI,IAAA;AACR,EAAA;AACK,EAAA;AACA,EAAA;AAEC,EAAA;AACJ,IAAA;AACM,IAAA;AACN,IAAA;AACA,IAAA;AACA,IAAA;AAAa;AAEb,IAAA;AACF,EAAA;AACM,EAAA;AACF,EAAA;AACG,EAAA;AACT;AAEgB;AACRA,EAAAA;AACA,EAAA;AACA,EAAA;AACD,EAAA;AACA,EAAA;AACD,EAAA;AACI,IAAA;AACR,EAAA;AAEM,EAAA;AACJ,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AAGM,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACG,EAAA;AACT;AAEgB;AACRA,EAAAA;AACA,EAAA;AACD,EAAA;AACL,EAAA;AAEM,EAAA;AACA,EAAA;AACA,EAAA;AACF,EAAA;AACA,EAAA;AACG,EAAA;AACT;AA4Ba;AACF,EAAA;AAEL,EAAA;AACI,IAAA;AACF,IAAA;AACC,IAAA;AAED,IAAA;AACG,IAAA;AAED,MAAA;AACJ,MAAA;AACM,MAAA;AACF,MAAA;AACE,MAAA;AACF,MAAA;AACA,MAAA;AACN,IAAA;AACF,EAAA;AAEU,EAAA;AACJ,IAAA;AACE,MAAA;AACJ,MAAA;AACK,MAAA;AACL,MAAA;AACF,IAAA;AACM,IAAA;AACF,IAAA;AACC,IAAA;AACE,IAAA;AACT,EAAA;AAAA;AAGS,EAAA;AAGP,IAAA;AACM,MAAA;AACF,QAAA;AAGI,QAAA;AACN,MAAA;AACE,QAAA;AACF,MAAA;AACF,IAAA;AACF,EAAA;AACF;AAegB;AAIS;AACjB,EAAA;AACE,IAAA;AACG,IAAA;AAIT,IAAA;AACO,IAAA;AACT,EAAA;AAEO,EAAA;AACC,IAAA;AACE,MAAA;AACA,MAAA;AACN,MAAA;AACE,QAAA;AACA,QAAA;AACD,MAAA;AACD,MAAA;AACF,IAAA;AAEM,IAAA;AACC,MAAA;AACH,QAAA;AACF,MAAA;AACM,MAAA;AACA,MAAA;AACJ,QAAA;AACD,MAAA;AAED,MAAA;AACE,QAAA;AACE,UAAA;AACA,UAAA;AACD,QAAA;AACH,MAAA;AACA,MAAA;AACF,IAAA;AAEM,IAAA;AACE,MAAA;AACA,MAAA;AACN,MAAA;AACE,QAAA;AACA,QAAA;AACD,MAAA;AACD,MAAA;AACF,IAAA;AAEA,IAAA;AACQ,MAAA;AAIJ,QAAA;AACG,2BAAA;AAEH,QAAA;AACA,QAAA;AACF,MAAA;AAEA,MAAA;AACE,QAAA;AACA,QAAA;AACE,UAAA;AACA,UAAA;AACD,QAAA;AACD,QAAA;AACF,MAAA;AAEM,MAAA;AACJ,QAAA;AACF,MAAA;AAEA,MAAA;AACE,QAAA;AACA,QAAA;AACI,QAAA;AACA,QAAA;AACF,UAAA;AACE,YAAA;AACA,YAAA;AACD,UAAA;AACH,QAAA;AAGE,UAAA;AACA,UAAA;AACF,QAAA;AACF,MAAA;AAEM,MAAA;AACJ,QAAA;AACE,UAAA;AACD,QAAA;AACH,MAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAYA,MAAA;AACM,QAAA;AACA,QAAA;AACJ,QAAA;AACF,MAAA;AAAA;AAGA,MAAA;AACE,QAAA;AACF,MAAA;AACF,IAAA;AACF,EAAA;AACF;AXowBY;AACA;AYnmCI;AACP,EAAA;AACC,IAAA;AACE,MAAA;AACD,MAAA;AACC,MAAA;AAEN,MAAA;AACE,QAAA;AAA8B;AAAA;AAG1B,QAAA;AACL,MAAA;AACH,IAAA;AACA,IAAA;AAAuC,MAAA;AAAmD,IAAA;AAC5F,EAAA;AACF;AZsmCY;AACA;Aa5mCI;AACP,EAAA;AACC,IAAA;AAAgB,MAAA;AAAmD,IAAA;AAEnE,IAAA;AACJ,MAAA;AACE,QAAA;AACD,MAAA;AACH,IAAA;AAEM,IAAA;AACJ,MAAA;AACE,QAAA;AACE,UAAA;AACA,UAAA;AACA,UAAA;AACA,UAAA;AACA,UAAA;AACF,QAAA;AACD,MAAA;AACH,IAAA;AAEM,IAAA;AACC,MAAA;AAEL,MAAA;AACF,IAAA;AACF,EAAA;AACF;Ab4mCY;AACA;ActoCI;AACP,EAAA;AACE,IAAA;AAEP,IAAA;AACO,MAAA;AACC,MAAA;AACR,IAAA;AAEA,IAAA;AACO,MAAA;AACC,MAAA;AACR,IAAA;AAEO,IAAA;AACT,EAAA;AACF;AdqoCY;AACA;AellCN;AACA;AAEG;AACD,EAAA;AACF,EAAA;AACE,EAAA;AACC,EAAA;AACT;AAES;AACD,EAAA;AACF,EAAA;AACK,EAAA;AACX;AAEgB;AAIV,EAAA;AACA,EAAA;AAIE,EAAA;AACJ,IAAA;AACE,sBAAA;AACF,IAAA;AACA,IAAA;AACE,MAAA;AACF,IAAA;AACA,IAAA;AACE,MAAA;AACF,IAAA;AACF,EAAA;AAEM,EAAA;AACE,IAAA;AACA,MAAA;AAGF,QAAA;AACF,MAAA;AAEM,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACF,MAAA;AAEE,MAAA;AACJ,QAAA;AACA,QAAA;AACA,QAAA;AACI,QAAA;AACJ,QAAA;AACE,UAAA;AACF,QAAA;AACA,QAAA;AACE,UAAA;AACE,YAAA;AACA,4BAAA;AACF,UAAA;AACE,4BAAA;AACF,UAAA;AACF,QAAA;AACD,MAAA;AAEG,MAAA;AACF,QAAA;AACF,MAAA;AAGM,QAAA;AACJ,QAAA;AACE,UAAA;AACE,UAAA;AACJ,QAAA;AACF,MAAA;AACA,MAAA;AACF,IAAA;AAEM,IAAA;AAIA,MAAA;AACE,MAAA;AACN,MAAA;AACM,MAAA;AACR,IAAA;AAEM,IAAA;AACA,MAAA;AACA,MAAA;AACF,QAAA;AACF,MAAA;AACA,MAAA;AACE,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACD,MAAA;AACH,IAAA;AAEM,IAAA;AACA,MAAA;AACJ,MAAA;AAA6B;AAE3B,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACD,MAAA;AACH,IAAA;AAEI,IAAA;AACF,MAAA;AACF,IAAA;AAEA,IAAA;AACF,EAAA;AAEO,EAAA;AACT;Af0jCY;AACA;AgBjyCC;AhBmyCD;AACA;AiBvxCC;AACX,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAqBS;AACD,EAAA;AACC,EAAA;AACT;AAWgB;AAGR,EAAA;AACA,EAAA;AACI,IAAA;AAAG,IAAA;AAAiB,IAAA;AAAc,IAAA;AAAc,IAAA;AACxD,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AAEA,EAAA;AACM,IAAA;AACI,IAAA;AACF,MAAA;AACN,IAAA;AAGQ,IAAA;AACF,MAAA;AACN,IAAA;AAEM,IAAA;AAIA,MAAA;AACN,IAAA;AAEA,IAAA;AACQ,MAAA;AACD,MAAA;AACL,MAAA;AACE,QAAA;AACI,QAAA;AACF,UAAA;AACA,UAAA;AACA,UAAA;AACF,QAAA;AACI,QAAA;AACN,MAAA;AACF,IAAA;AACF,EAAA;AAEI,EAAA;AACG,EAAA;AACT;AAGS;AACA,EAAA;AACT;AAGS;AACH,EAAA;AACE,EAAA;AACF,EAAA;AACG,EAAA;AACT;AAES;AACA,EAAA;AACT;AAES;AACD,EAAA;AACE,EAAA;AACV;AAGS;AACA,EAAA;AACT;AAegB;AACR,EAAA;AACA,EAAA;AACA,EAAA;AAEE,EAAA;AACA,IAAA;AACA,MAAA;AACA,MAAA;AACJ,MAAA;AACD,IAAA;AACD,IAAA;AAAkC;AACpC,EAAA;AAEQ,EAAA;AACA,IAAA;AACE,MAAA;AACF,MAAA;AACE,MAAA;AAGF,MAAA;AACF,QAAA;AACF,MAAA;AACA,MAAA;AACD,IAAA;AACD,IAAA;AAAwC;AAC1C,EAAA;AAEQ,EAAA;AACA,IAAA;AAEA,MAAA;AACA,MAAA;AACJ,MAAA;AACD,IAAA;AACD,IAAA;AAAqC;AACvC,EAAA;AAEQ,EAAA;AACA,IAAA;AACE,MAAA;AACF,MAAA;AACE,MAAA;AACF,MAAA;AACF,QAAA;AACF,MAAA;AAEM,MAAA;AACN,MAAA;AACD,IAAA;AACD,IAAA;AAAqC;AACvC,EAAA;AAEQ,EAAA;AACA,IAAA;AACE,MAAA;AACF,MAAA;AACE,MAAA;AACF,MAAA;AACJ,MAAA;AACD,IAAA;AACD,IAAA;AAA4C;AAC9C,EAAA;AAEI,EAAA;AACI,IAAA;AACF,IAAA;AACF,MAAA;AAAoD;AACtD,IAAA;AACF,EAAA;AAEM,EAAA;AACF,EAAA;AAA8D;AAE5D,EAAA;AACF,EAAA;AAEE,EAAA;AACF,EAAA;AAEG,EAAA;AACT;AAGS;AACH,EAAA;AACE,EAAA;AACA,EAAA;AACF,EAAA;AACF,IAAA;AACQ,MAAA;AAIF,MAAA;AACA,MAAA;AAAsE,MAAA;AAErE,MAAA;AAGC,MAAA;AACR,IAAA;AACF,EAAA;AACM,EAAA;AACI,EAAA;AACJ,EAAA;AACF,EAAA;AACA,EAAA;AACG,EAAA;AAAgC;AACzC;AAGS;AACH,EAAA;AACE,EAAA;AACE,IAAA;AAEF,IAAA;AACE,IAAA;AACF,IAAA;AACE,IAAA;AACA,IAAA;AACF,IAAA;AACE,IAAA;AACF,IAAA;AACA,IAAA;AAKF,MAAA;AACF,IAAA;AACO,IAAA;AACR,EAAA;AACM,EAAA;AAA8B;AACvC;AAQS;AACD,EAAA;AACF,EAAA;AACJ,EAAA;AACQ,IAAA;AACF,IAAA;AACN,EAAA;AACO,EAAA;AACT;AAQS;AACD,EAAA;AACF,EAAA;AACK,IAAA;AACT,EAAA;AACU,EAAA;AACJ,EAAA;AAEC,EAAA;AACT;AjB4rCY;AACA;AkBx+CN;AAgCA;AAKU;AACN,EAAA;AACD,IAAA;AACH,MAAA;AAMG,IAAA;AACH,MAAA;AAMG,IAAA;AACH,MAAA;AAKG,IAAA;AACH,MAAA;AAKF,IAAA;AACE,MAAA;AAMJ,EAAA;AACF;AAQgB;AACR,EAAA;AACJ,IAAA;AACQ,MAAA;AACN,MAAA;AACA,MAAA;AAIF,IAAA;AACA,IAAA;AACQ,MAAA;AACN,MAAA;AACA,MAAA;AACA,MAAA;AACF,IAAA;AACO,IAAA;AACC,MAAA;AACN,MAAA;AACE,QAAA;AACA,QAAA;AACF,MAAA;AACA,MAAA;AACF,IAAA;AACM,IAAA;AACE,MAAA;AACA,MAAA;AACN,MAAA;AAGF,IAAA;AACA,IAAA;AACQ,MAAA;AACA,MAAA;AACN,MAAA;AAIF,IAAA;AACF,EAAA;AAEI,EAAA;AACA,EAAA;AACI,IAAA;AACE,MAAA;AACN,MAAA;AACF,IAAA;AACA,IAAA;AACF,EAAA;AAES,EAAA;AACX;AAeM;AAQN;AAQU,EAAA;AAEF,EAAA;AAEA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AAKA,EAAA;AACJ,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AAEI,EAAA;AACII,IAAAA;AACJ,MAAA;AACA,MAAA;AACA,MAAA;AACG,MAAA;AACJ,IAAA;AACM,IAAA;AACL,MAAA;AACA,MAAA;AACA,MAAA;AACF,IAAA;AACF,EAAA;AAEI,EAAA;AACA,EAAA;AACE,IAAA;AAGF,MAAA;AACF,IAAA;AACA,IAAA;AACE,MAAA;AACG,MAAA;AACH,MAAA;AACD,IAAA;AACH,EAAA;AACE,IAAA;AACE,MAAA;AACA,MAAA;AACG,MAAA;AACH,MAAA;AACD,IAAA;AACH,EAAA;AACE,IAAA;AACE,MAAA;AACA,MAAA;AACG,MAAA;AACJ,IAAA;AACI,EAAA;AACL,IAAA;AACE,MAAA;AACG,MAAA;AACJ,IAAA;AACH,EAAA;AAEM,EAAA;AACN,EAAA;AACM,IAAA;AACN,EAAA;AACO,EAAA;AACT;AASgB;AAIE;AACV,EAAA;AACD,EAAA;AAEG,IAAA;AACA,IAAA;AACJ,MAAA;AACF,IAAA;AACF,EAAA;AAEM,EAAA;AACA,EAAA;AACA,EAAA;AAEF,EAAA;AACI,IAAA;AACR,EAAA;AACI,EAAA;AACI,IAAA;AACR,EAAA;AACI,EAAA;AAGF,IAAA;AACE,MAAA;AAGF,IAAA;AACF,EAAA;AAEM,EAAA;AACF,EAAA;AACI,IAAA;AACR,EAAA;AAEM,EAAA;AACA,EAAA;AACA,EAAA;AAEA,EAAA;AAGF,EAAA;AACK,IAAA;AACC,MAAA;AACN,MAAA;AACA,MAAA;AACF,IAAA;AACF,EAAA;AACS,EAAA;AACX;AlBi2CY;AACA;AmBrjDC;AAAY;AAAA;AAAA;AAAA;AAAA;AAMd,EAAA;AACA,EAAA;AAEA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AAEC,EAAA;AACV,EAAA;AACS,EAAA;AAET,EAAA;AACQ,IAAA;AAEN,IAAA;AAEK,IAAA;AACH,MAAA;AACI,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACJ,MAAA;AACD,IAAA;AACG,IAAA;AAEC,IAAA;AAEA,IAAA;AAKA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACE,MAAA;AACA,MAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAMF,MAAA;AACD,QAAA;AACE,UAAA;AACF,QAAA;AACF,MAAA;AACF,IAAA;AACK,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACG,MAAA;AACD,MAAA;AACP,IAAA;AAIK,IAAA;AACE,MAAA;AACH,QAAA;AACD,MAAA;AACF,IAAA;AACH,EAAA;AAAA;AAAA;AAAA;AAMM,EAAA;AACG,IAAA;AACT,EAAA;AAAA;AAGM,EAAA;AACG,IAAA;AACT,EAAA;AAAA;AAGM,EAAA;AACG,IAAA;AACT,EAAA;AAAA;AAGM,EAAA;AAGG,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOM,EAAA;AASE,IAAA;AACD,IAAA;AACD,IAAA;AACI,MAAA;AACR,IAAA;AACM,IAAA;AACJ,MAAA;AACIC,MAAAA;AAGAA,MAAAA;AAGJ,MAAA;AACA,MAAA;AACA,MAAA;AACI,MAAA;AACL,IAAA;AACK,IAAA;AACC,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOM,EAAA;AAQE,IAAA;AACD,IAAA;AACC,IAAA;AACJ,MAAA;AACIA,MAAAA;AAGAA,MAAAA;AAGJ,MAAA;AACA,MAAA;AACA,MAAA;AACI,MAAA;AACL,IAAA;AACK,IAAA;AACC,IAAA;AACT,EAAA;AAAA;AAGM,EAAA;AACG,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQM,EAAA;AACG,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAeM,EAAA;AACA,IAAA;AACC,IAAA;AACC,IAAA;AACF,IAAA;AACA,IAAA;AACI,MAAA;AACA,MAAA;AACA,MAAA;AACD,MAAA;AACC,QAAA;AAGA,QAAA;AAGL,MAAA;AACK,IAAA;AAGR,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOM,EAAA;AACG,IAAA;AACT,EAAA;AAAA;AAGI,EAAA;AACK,IAAA;AACT,EAAA;AAAA;AAGI,EAAA;AACK,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAeU,EAAA;AACJ,IAAA;AACI,MAAA;AACR,IAAA;AACI,IAAA;AACA,IAAA;AACG,MAAA;AACP,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAiBM,EAAA;AACE,IAAA;AACJ,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACE,IAAA;AAEJ,IAAA;AAEM,IAAA;AACA,IAAA;AACJ,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACF,IAAA;AAEM,IAAA;AACA,IAAA;AAEF,IAAA;AACF,MAAA;AACM,MAAA;AACJ,QAAA;AAAoC,QAAA;AACpC,QAAA;AAAiB,QAAA;AACD,MAAA;AACpB,IAAA;AACI,IAAA;AACF,MAAA;AACM,MAAA;AACJ,QAAA;AAAiB,QAAA;AAAoC,QAAA;AACrD,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACgB,MAAA;AACpB,IAAA;AACI,IAAA;AACF,MAAA;AACM,MAAA;AACJ,QAAA;AAAyB,QAAA;AAAoC,QAAA;AAC7C,MAAA;AACpB,IAAA;AAII,IAAA;AACF,MAAA;AACM,MAAA;AACJ,QAAA;AAAoC,QAAA;AACpB,MAAA;AACpB,IAAA;AAEI,IAAA;AACI,MAAA;AACN,MAAA;AACF,IAAA;AAEM,IAAA;AACA,IAAA;AACN,IAAA;AACQ,MAAA;AACF,MAAA;AACF,QAAA;AACA,QAAA;AACF,MAAA;AACI,MAAA;AACC,MAAA;AACP,IAAA;AAEM,IAAA;AAEF,IAAA;AACE,MAAA;AACF,QAAA;AACE,UAAA;AACD,QAAA;AACH,MAAA;AAEA,MAAA;AACF,IAAA;AAEO,IAAA;AACL,MAAA;AACA,MAAA;AACD,IAAA;AACM,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQQ,EAAA;AACC,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQA,EAAA;AACS,IAAA;AACT,EAAA;AAAA;AAGM,EAAA;AAEI,IAAA;AACV,EAAA;AAAA;AAGM,EAAA;AACA,IAAA;AACC,IAAA;AAGC,IAAA;AACD,IAAA;AACA,IAAA;AACA,IAAA;AACP,EAAA;AACF;AAESA;AACGL,EAAAA;AACH,EAAA;AACT;AAES;AAED,EAAA;AACD,EAAA;AACG,IAAA;AACJ,MAAA;AAEF,IAAA;AACF,EAAA;AACO,EAAA;AACL,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACF;AnB0+CY;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/Users/anish/maximemai/maximem_synap/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 * 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';\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\nexport const DEFAULT_TIMEOUTS: TimeoutConfig = { connect: 5.0, read: 30.0, write: 10.0 };\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 retryPolicy?: Partial<RetryPolicy>;\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 = { ...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 setTimeouts(timeouts: Partial<TimeoutConfig>): void {\n this.timeouts = { ...this.timeouts, ...timeouts };\n }\n\n /** `null` restores the default policy, matching Python's `retry_policy=None`. */\n setRetryPolicy(policy: Partial<RetryPolicy> | null): void {\n this.retryPolicy = policy === null\n ? { ...DEFAULT_RETRY_POLICY }\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 return new InsufficientCreditsError(message, {\n ...opts,\n balanceCredits: numberOrNull(parsed['balance_credits']),\n requiredCredits: numberOrNull(parsed['required_credits']),\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\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 // 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 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/** 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): Promise<RawContext> {\n const { endpoint, body } = buildFetchRequest(scope, options);\n const result = await transport.request<Json>(endpoint, { body });\n // The wrapper returned `result.context || {}`; preserve that exactly.\n return (result?.['context'] as RawContext) ?? {};\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\nexport interface CreateMemoryResult {\n ingestion_id?: string;\n status?: string;\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\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<Json>;\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 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<Json>('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 if (!customerId) throw new InvalidInputError('customer_id is required');\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\nexport interface CompactOptions {\n conversation_id?: string;\n conversationId?: string;\n strategy?: string;\n compaction_level?: string;\n 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 if (!customerId) throw new InvalidInputError('customer_id is required');\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\nexport interface ConversationNamespace {\n record_message(options: RecordMessageOptions): Promise<Json>;\n record_messages_batch(messages: RecordMessageOptions[]): Promise<Json>;\n ingest_transcript(options: IngestTranscriptOptions): Promise<Json>;\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\nexport function createConversationNamespace(\n transport: HttpTransport,\n subscribers: CompactionSubscribers = new CompactionSubscribers(),\n onWrite: WriteInvalidator = () => {},\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 const body = buildRecordMessageBody(options);\n const result = await transport.request<Json>('conversations_messages', { 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 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 const bodies = messages.map(buildRecordMessageBody);\n const result = await transport.request<Json>('conversations_messages_batch', {\n body: { messages: bodies },\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<Json>('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 validateConversationId(\n (options as { conversation_id?: string; conversationId?: string }).conversation_id ??\n (options as { conversationId?: string }).conversationId,\n );\n return fetchContext(transport, 'conversation', options);\n },\n\n get_context_for_prompt: async (options = {}) => {\n const id = convId(options);\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 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 } 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(transport: HttpTransport): 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); } },\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<Json>;\n get_ledger(options?: { limit?: number; offset?: number }): Promise<Json>;\n estimate(options: EstimateOptions): Promise<Json>;\n redeem(code: string): Promise<Json>;\n}\n\nexport function createCreditsNamespace(transport: HttpTransport): CreditsNamespace {\n return {\n async get_balance() { return transport.request<Json>('credits_balance'); },\n\n async get_ledger(options = {}) {\n return transport.request<Json>('credits_ledger', {\n query: { limit: options.limit ?? 50, offset: options.offset ?? 0 },\n });\n },\n\n async estimate(options) {\n return transport.request<Json>('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<Json>('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 search_queries?: string[];\n searchQueries?: string[];\n context_types?: string[];\n contextTypes?: string[];\n}\n\nexport interface RecordThinkingOptions {\n content: 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 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 const raw = getEnv('SYNAP_GRPC_USE_TLS');\n if (raw === undefined || raw === '') return undefined;\n return !['0', 'false', 'no', 'off'].includes(raw.trim().toLowerCase());\n}\n\nexport function createInstanceNamespace(\n credentials: () => StreamCredentials,\n cache: AnticipationCache,\n): InstanceNamespace {\n let client: StreamClient | null = null;\n let dispatchCompaction:\n | ((conversationId: string, bundle: Record<string, unknown>) => void)\n | null = null;\n\n const internals: InstanceInternals = {\n onCompactionUpdate(conversationId, bundle) {\n dispatchCompaction?.(conversationId, bundle);\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 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 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: options.metadata ?? {},\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 * 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 { 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';\n\nexport interface SynapClientOptions {\n apiKey?: string;\n clientId?: string;\n instanceId?: string;\n baseUrl?: string;\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\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 /** Accepted and ignored. Python-only. */\n logger?: unknown;\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 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 #compactionSubscribers: CompactionSubscribers;\n\n constructor(options: SynapClientOptions = {}) {\n const credentials = resolveCredentials(options);\n // Fail locally on a malformed id rather than sending it upstream.\n validateInstanceId(credentials.instanceId);\n\n this.#transport = new HttpTransport({\n credentials,\n ...(options.baseUrl !== undefined ? { baseUrl: options.baseUrl } : {}),\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.user = createUserNamespace(this.#transport);\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) } };\n this.client = { context: { fetch: async (o = {}) => fetchContext(this.#transport, 'client', o) } };\n this.#compactionSubscribers = new CompactionSubscribers();\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 );\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 );\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 this.#transport.updateCredentials({\n ...(resolvedClientId !== '' && credentials.clientId === ''\n ? { clientId: resolvedClientId }\n : {}),\n ...(resolvedInstanceId !== '' && credentials.instanceId === ''\n ? { instanceId: 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 * The Python-only keys (`storage_path`, `cache_backend`,\n * `session_timeout_minutes`, `logger`) are accepted and ignored rather than\n * rejected, because this SDK has no SQLite cache backend and no global\n * logger to swap. Ignoring them keeps a shared config object portable\n * 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 }\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 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 /** Python logs via `logger.warning`; here it is one overridable seam. */\n #warn(message: string): void {\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 // 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\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"]}
|