@rubytech/create-maxy-code 0.1.427 → 0.1.429

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.
Files changed (192) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/embed-client/dist/index.d.ts +5 -1
  3. package/payload/platform/lib/embed-client/dist/index.d.ts.map +1 -1
  4. package/payload/platform/lib/embed-client/dist/index.js +114 -36
  5. package/payload/platform/lib/embed-client/dist/index.js.map +1 -1
  6. package/payload/platform/lib/embed-client/src/index.ts +141 -34
  7. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +4 -2
  8. package/payload/platform/plugins/docs/references/admin-ui.md +2 -0
  9. package/payload/platform/plugins/docs/references/internals.md +1 -1
  10. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/embed-throughput.test.d.ts +2 -0
  11. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/embed-throughput.test.d.ts.map +1 -0
  12. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/embed-throughput.test.js +92 -0
  13. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/embed-throughput.test.js.map +1 -0
  14. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/embed-timeout.test.d.ts +2 -0
  15. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/embed-timeout.test.d.ts.map +1 -0
  16. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/embed-timeout.test.js +56 -0
  17. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/embed-timeout.test.js.map +1 -0
  18. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/embeddings-cap.test.js +56 -0
  19. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/embeddings-cap.test.js.map +1 -1
  20. package/payload/platform/plugins/work/mcp/dist/lib/embeddings.d.ts +9 -3
  21. package/payload/platform/plugins/work/mcp/dist/lib/embeddings.d.ts.map +1 -1
  22. package/payload/platform/plugins/work/mcp/dist/lib/embeddings.js +19 -15
  23. package/payload/platform/plugins/work/mcp/dist/lib/embeddings.js.map +1 -1
  24. package/payload/platform/plugins/workflows/mcp/dist/lib/embeddings.d.ts +1 -1
  25. package/payload/platform/plugins/workflows/mcp/dist/lib/embeddings.d.ts.map +1 -1
  26. package/payload/platform/plugins/workflows/mcp/dist/lib/embeddings.js +7 -18
  27. package/payload/platform/plugins/workflows/mcp/dist/lib/embeddings.js.map +1 -1
  28. package/payload/server/{chunk-3Z5WE53X.js → chunk-UUG6ZSO5.js} +65 -9
  29. package/payload/server/maxy-edge.js +1 -1
  30. package/payload/server/public/assets/AdminLoginScreens-C4AZT7cQ.js +1 -0
  31. package/payload/server/public/assets/AdminShell-BfLQQ27Z.js +2 -0
  32. package/payload/server/public/assets/{Checkbox-B_lfjsqd.js → Checkbox-BC0c_gbn.js} +1 -1
  33. package/payload/server/public/assets/admin-SaF1F4e-.js +1 -0
  34. package/payload/server/public/assets/{arc-dVLpR-ec.js → arc-qKfXIt8v.js} +1 -1
  35. package/payload/server/public/assets/architecture-YZFGNWBL-FPHT7s-C.js +1 -0
  36. package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-C6u_CVzb.js → architectureDiagram-Q4EWVU46-DkVrGn7G.js} +1 -1
  37. package/payload/server/public/assets/audio-attachment-mime-BBW07eyG.js +2 -0
  38. package/payload/server/public/assets/{blockDiagram-DXYQGD6D-Cy8F5b8O.js → blockDiagram-DXYQGD6D-DqavswGH.js} +1 -1
  39. package/payload/server/public/assets/{browser-CCWufeXr.js → browser-CvZxNdKw.js} +1 -1
  40. package/payload/server/public/assets/{c4Diagram-AHTNJAMY-DRXrlepj.js → c4Diagram-AHTNJAMY-B7fyKiXe.js} +1 -1
  41. package/payload/server/public/assets/calendar-DXxLMF2J.js +1 -0
  42. package/payload/server/public/assets/channel-j70eMvcL.js +1 -0
  43. package/payload/server/public/assets/chat-B9Qjt7C9.js +1 -0
  44. package/payload/server/public/assets/chevron-left-CLPxfjDV.js +1 -0
  45. package/payload/server/public/assets/{chunk-2KRD3SAO-fEyfhRLz.js → chunk-2KRD3SAO-DsgMtGkv.js} +1 -1
  46. package/payload/server/public/assets/{chunk-336JU56O-NUWO4y7q.js → chunk-336JU56O-CkdISfNt.js} +2 -2
  47. package/payload/server/public/assets/chunk-426QAEUC-D-NcAlTW.js +1 -0
  48. package/payload/server/public/assets/{chunk-4BX2VUAB-BnfiNR3l.js → chunk-4BX2VUAB-CYpiF2bl.js} +1 -1
  49. package/payload/server/public/assets/{chunk-4TB4RGXK-Bj_aJOz8.js → chunk-4TB4RGXK-CWyc_F7v.js} +1 -1
  50. package/payload/server/public/assets/{chunk-55IACEB6-aK0DNSKb.js → chunk-55IACEB6-BbzKMw2Z.js} +1 -1
  51. package/payload/server/public/assets/{chunk-5FUZZQ4R-DeqAkq5C.js → chunk-5FUZZQ4R-u19Nai9e.js} +1 -1
  52. package/payload/server/public/assets/{chunk-5PVQY5BW-SShlujAQ.js → chunk-5PVQY5BW-Ba8by6Uy.js} +1 -1
  53. package/payload/server/public/assets/{chunk-67CJDMHE-CZpzQ0DL.js → chunk-67CJDMHE-BwUuxtyb.js} +1 -1
  54. package/payload/server/public/assets/{chunk-7N4EOEYR-COnjO6jy.js → chunk-7N4EOEYR-BRH9iM9L.js} +1 -1
  55. package/payload/server/public/assets/{chunk-AA7GKIK3-C9V31L2z.js → chunk-AA7GKIK3-BGoztgLL.js} +1 -1
  56. package/payload/server/public/assets/{chunk-BSJP7CBP-DSwnSGe3.js → chunk-BSJP7CBP-C5GDAZTl.js} +1 -1
  57. package/payload/server/public/assets/{chunk-CIAEETIT-CnRUTuta.js → chunk-CIAEETIT-ChY32VPS.js} +1 -1
  58. package/payload/server/public/assets/{chunk-EDXVE4YY-BexiL2LH.js → chunk-EDXVE4YY-CPSD3p6p.js} +1 -1
  59. package/payload/server/public/assets/{chunk-ENJZ2VHE-aTklWdJQ.js → chunk-ENJZ2VHE-D1oL-iUr.js} +1 -1
  60. package/payload/server/public/assets/{chunk-FMBD7UC4-svcEBQzs.js → chunk-FMBD7UC4-CPvYEr54.js} +1 -1
  61. package/payload/server/public/assets/{chunk-FOC6F5B3-CG7R7Xri.js → chunk-FOC6F5B3-9Yyj7Vbz.js} +1 -1
  62. package/payload/server/public/assets/{chunk-ICPOFSXX-CGN7Z4x9.js → chunk-ICPOFSXX-BaYgh8FR.js} +2 -2
  63. package/payload/server/public/assets/{chunk-K5T4RW27-CWBh78jw.js → chunk-K5T4RW27-5ucpeRvm.js} +1 -1
  64. package/payload/server/public/assets/{chunk-KGLVRYIC-Ctiq4q-5.js → chunk-KGLVRYIC-DW-FHk7T.js} +1 -1
  65. package/payload/server/public/assets/{chunk-LIHQZDEY-BII77Q7K.js → chunk-LIHQZDEY-CGGVfAyG.js} +1 -1
  66. package/payload/server/public/assets/{chunk-ORNJ4GCN-DT7R01H8.js → chunk-ORNJ4GCN-Bv8EUa5d.js} +1 -1
  67. package/payload/server/public/assets/{chunk-OYMX7WX6-y-Jkbd4C.js → chunk-OYMX7WX6-C1mXJnyw.js} +1 -1
  68. package/payload/server/public/assets/chunk-QZHKN3VN-CAwbQBtf.js +1 -0
  69. package/payload/server/public/assets/{chunk-U2HBQHQK-DzFo6KbQ.js → chunk-U2HBQHQK-PwSQz6EP.js} +1 -1
  70. package/payload/server/public/assets/{chunk-X2U36JSP-CYSwgtLV.js → chunk-X2U36JSP--NPHTBlU.js} +1 -1
  71. package/payload/server/public/assets/{chunk-XPW4576I-DHlM4tOn.js → chunk-XPW4576I-Bbc7Da32.js} +1 -1
  72. package/payload/server/public/assets/{chunk-YZCP3GAM-B20rTmLl.js → chunk-YZCP3GAM-D-9v8e--.js} +1 -1
  73. package/payload/server/public/assets/{chunk-ZZ45TVLE-C7hP4J8m.js → chunk-ZZ45TVLE-CMPg670Q.js} +1 -1
  74. package/payload/server/public/assets/classDiagram-6PBFFD2Q-Ct6Rn0vt.js +1 -0
  75. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-B_2XCIFv.js +1 -0
  76. package/payload/server/public/assets/clone-DjPWA_NH.js +1 -0
  77. package/payload/server/public/assets/{cose-bilkent-S5V4N54A-nKjTv9sR.js → cose-bilkent-S5V4N54A-NVSb1Bhy.js} +1 -1
  78. package/payload/server/public/assets/{dagre-KV5264BT-BNJYTnls.js → dagre-KV5264BT-V2Ai1DFl.js} +1 -1
  79. package/payload/server/public/assets/{dagre-Cx8LiiUZ.js → dagre-QfLcO-1j.js} +1 -1
  80. package/payload/server/public/assets/data-BKE6Sa-g.js +1 -0
  81. package/payload/server/public/assets/{diagram-5BDNPKRD-CoAN5_f9.js → diagram-5BDNPKRD-Caw21693.js} +1 -1
  82. package/payload/server/public/assets/{diagram-G4DWMVQ6-pRmsN4_x.js → diagram-G4DWMVQ6-CgwVkT2q.js} +1 -1
  83. package/payload/server/public/assets/{diagram-MMDJMWI5-CRoylyp4.js → diagram-MMDJMWI5-DFUrLZhs.js} +1 -1
  84. package/payload/server/public/assets/{diagram-TYMM5635-BT3ZtG5b.js → diagram-TYMM5635-CEEiEuZJ.js} +1 -1
  85. package/payload/server/public/assets/{erDiagram-SMLLAGMA-DSKws1ZV.js → erDiagram-SMLLAGMA-CG10NrT3.js} +1 -1
  86. package/payload/server/public/assets/file-download-B2B2DTar.js +1 -0
  87. package/payload/server/public/assets/{flatten-jn8bZgcI.js → flatten-DAO0sfCM.js} +1 -1
  88. package/payload/server/public/assets/{flowDiagram-DWJPFMVM-C2L38ZE5.js → flowDiagram-DWJPFMVM-BqkV9Okv.js} +1 -1
  89. package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-Bm2h-vjm.js → ganttDiagram-T4ZO3ILL-DRiCDQF6.js} +1 -1
  90. package/payload/server/public/assets/gitGraph-7Q5UKJZL-B8Hx7f-D.js +1 -0
  91. package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-BoowbF7u.js → gitGraphDiagram-UUTBAWPF-BZV2voyy.js} +1 -1
  92. package/payload/server/public/assets/graph-DRNzDBTV.js +51 -0
  93. package/payload/server/public/assets/{graph-labels-BLx-0vyT.js → graph-labels-DxqDdO7G.js} +1 -1
  94. package/payload/server/public/assets/{graphlib-BvT2s_o1.js → graphlib-DGPThX8M.js} +1 -1
  95. package/payload/server/public/assets/info-OMHHGYJF-BicCJTqU.js +1 -0
  96. package/payload/server/public/assets/infoDiagram-42DDH7IO-tf-8rOez.js +2 -0
  97. package/payload/server/public/assets/{isEmpty-Dkk6_L-B.js → isEmpty-Cb_U81C6.js} +1 -1
  98. package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-nxcmj-IX.js → ishikawaDiagram-UXIWVN3A-BKsAxSvp.js} +1 -1
  99. package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-q5SNIvmS.js → journeyDiagram-VCZTEJTY-CmliuOd-.js} +1 -1
  100. package/payload/server/public/assets/{kanban-definition-6JOO6SKY-BugG9Q1o.js → kanban-definition-6JOO6SKY-DAlElMb-.js} +1 -1
  101. package/payload/server/public/assets/{line-MZCQLR-q.js → line-DKZqLKAC.js} +1 -1
  102. package/payload/server/public/assets/{linear-DmQGNk-g.js → linear-CbSGdh1x.js} +1 -1
  103. package/payload/server/public/assets/{maximize-2-CnNQnj_v.js → maximize-2-DKx6OBKv.js} +1 -1
  104. package/payload/server/public/assets/{mermaid-parser.core-DvoeqLxw.js → mermaid-parser.core-DFvbao9C.js} +2 -2
  105. package/payload/server/public/assets/{mermaid.core-BLaw7iG4.js → mermaid.core-C0vEBxDm.js} +3 -3
  106. package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-DMkt10Vc.js → mindmap-definition-QFDTVHPH-CShASUKd.js} +1 -1
  107. package/payload/server/public/assets/operator-CTJjI8te.js +1 -0
  108. package/payload/server/public/assets/{ordinal-DuJYM7p1.js → ordinal-H0tVmBDP.js} +1 -1
  109. package/payload/server/public/assets/packet-4T2RLAQJ-Ckn_5hq0.js +1 -0
  110. package/payload/server/public/assets/page-CbI1wtef.js +1 -0
  111. package/payload/server/public/assets/page-JzxyFGle.js +32 -0
  112. package/payload/server/public/assets/{pdf-render-riHufgrn.js → pdf-render-jAQU0nXt.js} +1 -1
  113. package/payload/server/public/assets/pie-ZZUOXDRM-DzlGQW3J.js +1 -0
  114. package/payload/server/public/assets/{pieDiagram-DEJITSTG-DRp4lD2B.js → pieDiagram-DEJITSTG-CBZHpDAJ.js} +1 -1
  115. package/payload/server/public/assets/{play-yEkd7ouW.js → play-DHttF4fl.js} +1 -1
  116. package/payload/server/public/assets/public-DzeW3_eM.js +1 -0
  117. package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-DkT63Dn4.js → quadrantDiagram-34T5L4WZ-n3bB3K91.js} +1 -1
  118. package/payload/server/public/assets/radar-PYXPWWZC-BHo2YD8b.js +1 -0
  119. package/payload/server/public/assets/{reduce-COyotL2V.js → reduce-DAvwat-a.js} +1 -1
  120. package/payload/server/public/assets/{requirementDiagram-MS252O5E-65GGtDkc.js → requirementDiagram-MS252O5E-DiDvQK3c.js} +1 -1
  121. package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-D1vRwLWd.js → sankeyDiagram-XADWPNL6-DWDQQZmd.js} +1 -1
  122. package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-Bp47ZSN2.js → sequenceDiagram-FGHM5R23-DmAqRlBT.js} +1 -1
  123. package/payload/server/public/assets/square-CHcQHtK8.js +1 -0
  124. package/payload/server/public/assets/{stateDiagram-FHFEXIEX-BZHHcP_e.js → stateDiagram-FHFEXIEX-D4R-n6bZ.js} +1 -1
  125. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-BcFRbGRu.js +1 -0
  126. package/payload/server/public/assets/tasks-ClfJU1CK.js +1 -0
  127. package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-DZ1wUyj-.js → timeline-definition-GMOUNBTQ-CjiHGP4l.js} +1 -1
  128. package/payload/server/public/assets/treeView-SZITEDCU-BtRr5vXp.js +1 -0
  129. package/payload/server/public/assets/treemap-W4RFUUIX-D8p0sbyo.js +1 -0
  130. package/payload/server/public/assets/useCopyFeedback-CPEOUSGU.js +1 -0
  131. package/payload/server/public/assets/{useSubAccountSwitcher-ayajZz8T.css → useSubAccountSwitcher-CdS1CorH.css} +1 -1
  132. package/payload/server/public/assets/{vennDiagram-DHZGUBPP-CttBahsn.js → vennDiagram-DHZGUBPP-CBdDQ7c-.js} +1 -1
  133. package/payload/server/public/assets/wardley-RL74JXVD-DgMn-9ZJ.js +1 -0
  134. package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-Y5tWieZg.js → wardleyDiagram-NUSXRM2D-BlIQPKLb.js} +1 -1
  135. package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-C78A9qbe.js → xychartDiagram-5P7HB3ND-CslKgh9B.js} +1 -1
  136. package/payload/server/public/browser.html +6 -6
  137. package/payload/server/public/calendar.html +7 -7
  138. package/payload/server/public/chat.html +13 -14
  139. package/payload/server/public/data.html +10 -10
  140. package/payload/server/public/graph.html +11 -11
  141. package/payload/server/public/index.html +14 -15
  142. package/payload/server/public/operator.html +15 -16
  143. package/payload/server/public/public.html +13 -14
  144. package/payload/server/public/tasks.html +5 -5
  145. package/payload/server/server.js +228 -9
  146. package/payload/server/public/assets/AdminLoginScreens-yAIhWcGc.js +0 -1
  147. package/payload/server/public/assets/AdminShell-Bk1AFQ4D.js +0 -2
  148. package/payload/server/public/assets/Transcript-D67OZnVM.js +0 -2
  149. package/payload/server/public/assets/admin-DuOhkvrZ.js +0 -1
  150. package/payload/server/public/assets/admin-types-ClHBm0ni.js +0 -1
  151. package/payload/server/public/assets/architecture-YZFGNWBL-CgeA3HPD.js +0 -1
  152. package/payload/server/public/assets/calendar-CNk1iPrA.js +0 -1
  153. package/payload/server/public/assets/channel-B04VBWf7.js +0 -1
  154. package/payload/server/public/assets/chat-7qB-dwBq.js +0 -1
  155. package/payload/server/public/assets/chevron-left-CpcDbHIz.js +0 -1
  156. package/payload/server/public/assets/chunk-426QAEUC-DZkHcXRA.js +0 -1
  157. package/payload/server/public/assets/chunk-QZHKN3VN-D9Mwgzti.js +0 -1
  158. package/payload/server/public/assets/classDiagram-6PBFFD2Q-CIo_TzfF.js +0 -1
  159. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-SzQYvAUZ.js +0 -1
  160. package/payload/server/public/assets/clone-BVjcjsBA.js +0 -1
  161. package/payload/server/public/assets/data-BnHcS27X.js +0 -1
  162. package/payload/server/public/assets/file-download-Hf_UILaA.js +0 -1
  163. package/payload/server/public/assets/gitGraph-7Q5UKJZL-DzQ18i0T.js +0 -1
  164. package/payload/server/public/assets/graph-klLugHEK.js +0 -51
  165. package/payload/server/public/assets/info-OMHHGYJF-CwsoaVzV.js +0 -1
  166. package/payload/server/public/assets/infoDiagram-42DDH7IO-ieNsH2bV.js +0 -2
  167. package/payload/server/public/assets/operator-CRcySYv9.js +0 -1
  168. package/payload/server/public/assets/packet-4T2RLAQJ-DxB-bO4L.js +0 -1
  169. package/payload/server/public/assets/page-CVo3CWIY.js +0 -1
  170. package/payload/server/public/assets/page-DPTbZRwl.js +0 -32
  171. package/payload/server/public/assets/pie-ZZUOXDRM-PSdKvB8-.js +0 -1
  172. package/payload/server/public/assets/public-Fi6ccYaE.js +0 -1
  173. package/payload/server/public/assets/radar-PYXPWWZC-D8FqRiWq.js +0 -1
  174. package/payload/server/public/assets/rotate-ccw-xHksz9mr.js +0 -1
  175. package/payload/server/public/assets/square-BZqaxg7D.js +0 -1
  176. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-qVtoGx56.js +0 -1
  177. package/payload/server/public/assets/tasks-COWmfts8.js +0 -1
  178. package/payload/server/public/assets/treeView-SZITEDCU-i-w4rJmQ.js +0 -1
  179. package/payload/server/public/assets/treemap-W4RFUUIX-Dij6AlAj.js +0 -1
  180. package/payload/server/public/assets/wardley-RL74JXVD-BJhOQVqW.js +0 -1
  181. /package/payload/server/public/assets/{_baseFor-Bjj9It3Q.js → _baseFor-D3BqAOBr.js} +0 -0
  182. /package/payload/server/public/assets/{array-BrrxEo12.js → array-iYP9fzJp.js} +0 -0
  183. /package/payload/server/public/assets/{cytoscape.esm-CTWJxUxo.js → cytoscape.esm-BQSBzPsB.js} +0 -0
  184. /package/payload/server/public/assets/{defaultLocale-CFDbRV4k.js → defaultLocale-BrdA1gDi.js} +0 -0
  185. /package/payload/server/public/assets/{dist-CRpLAnCP.js → dist-Cvgxm7qv.js} +0 -0
  186. /package/payload/server/public/assets/{init-dXCzGgLP.js → init-Bx0S6pkp.js} +0 -0
  187. /package/payload/server/public/assets/{katex-CHA1JFYJ.js → katex-BsiH_a6d.js} +0 -0
  188. /package/payload/server/public/assets/{path-CCRpdXhK.js → path-BLSgTy3w.js} +0 -0
  189. /package/payload/server/public/assets/{preload-helper-CH6UZRzu.js → preload-helper-ClBMtM1g.js} +0 -0
  190. /package/payload/server/public/assets/{rough.esm-Bud4g4fh.js → rough.esm-BMOtDaaS.js} +0 -0
  191. /package/payload/server/public/assets/{src-xN1oULc8.js → src-DV4LKv2E.js} +0 -0
  192. /package/payload/server/public/assets/{useSubAccountSwitcher-DADvGnr1.js → useSubAccountSwitcher-DyjTbzQR.js} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy-code",
3
- "version": "0.1.427",
3
+ "version": "0.1.429",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy-code": "./dist/index.js"
@@ -1,6 +1,10 @@
1
- export declare const EMBED_INPUT_MAX_CHARS = 8000;
1
+ export declare const EMBED_INPUT_MAX_CHARS: number;
2
2
  export declare const EMBED_SUBBATCH_SIZE: number;
3
3
  export declare const EMBED_SUBBATCH_COOLDOWN_MS: number;
4
+ export declare const EMBED_MS_PER_INPUT: number;
5
+ export declare const EMBED_REQUEST_TIMEOUT_MS: number;
6
+ export declare const EMBED_KEEP_ALIVE: number | string;
4
7
  export declare function embed(text: string): Promise<number[]>;
8
+ export declare function effectiveSubBatchSize(): number;
5
9
  export declare function embedBatch(texts: string[]): Promise<number[][]>;
6
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAcA,eAAO,MAAM,qBAAqB,OAAO,CAAC;AAmB1C,eAAO,MAAM,mBAAmB,QAA6D,CAAC;AAC9F,eAAO,MAAM,0BAA0B,QAGtC,CAAC;AA4BF,wBAAsB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAa3D;AAED,wBAAsB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAoBrE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA4BA,eAAO,MAAM,qBAAqB,QASjC,CAAC;AAaF,eAAO,MAAM,mBAAmB,QAA6D,CAAC;AAC9F,eAAO,MAAM,0BAA0B,QAGtC,CAAC;AAWF,eAAO,MAAM,kBAAkB,QAA8D,CAAC;AAQ9F,eAAO,MAAM,wBAAwB,QAGpC,CAAC;AAcF,eAAO,MAAM,gBAAgB,EAAE,MAAM,GAAG,MAEhB,CAAC;AA8EzB,wBAAsB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAG3D;AAKD,wBAAgB,qBAAqB,IAAI,MAAM,CAM9C;AAED,wBAAsB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAqBrE"}
@@ -6,16 +6,32 @@
6
6
  // and one POST shape across the platform (Task 636). fetch-only — no node:fs —
7
7
  // so it is safe to bundle into the ESM payload.
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.EMBED_SUBBATCH_COOLDOWN_MS = exports.EMBED_SUBBATCH_SIZE = exports.EMBED_INPUT_MAX_CHARS = void 0;
9
+ exports.EMBED_KEEP_ALIVE = exports.EMBED_REQUEST_TIMEOUT_MS = exports.EMBED_MS_PER_INPUT = exports.EMBED_SUBBATCH_COOLDOWN_MS = exports.EMBED_SUBBATCH_SIZE = exports.EMBED_INPUT_MAX_CHARS = void 0;
10
10
  exports.embed = embed;
11
+ exports.effectiveSubBatchSize = effectiveSubBatchSize;
11
12
  exports.embedBatch = embedBatch;
12
13
  const OLLAMA_URL = process.env.OLLAMA_URL ?? "http://localhost:11434";
13
14
  const EMBED_MODEL = process.env.EMBED_MODEL ?? "nomic-embed-text";
14
- // nomic-embed-text has a 2048-token context (~8k chars at ~4 chars/token).
15
- // An over-limit input makes the Ollama embed endpoint error; cap defensively so
16
- // an oversized body embeds (truncated) instead of throwing and aborting the
17
- // caller.
18
- exports.EMBED_INPUT_MAX_CHARS = 8000;
15
+ function envInt(name, def) {
16
+ const raw = process.env[name];
17
+ if (raw === undefined || raw === "")
18
+ return def;
19
+ const n = Number(raw);
20
+ return Number.isFinite(n) ? n : def;
21
+ }
22
+ // nomic-embed-text's usable embed context is 2048 tokens. The old 8000-char cap
23
+ // assumed ~4 chars/token, but dense emoji/markup content runs richer, so a
24
+ // section that passed the char cap still errored with "input length exceeds the
25
+ // context length". Size the default against a conservative 3 chars/token worst
26
+ // case, reserving a small token margin for the tokenizer's specials, so a capped
27
+ // input is provably under context: (2048 - 48) * 3 = 6000. Context is
28
+ // env-overridable so raising it raises the cap; EMBED_INPUT_MAX_CHARS is also
29
+ // directly overridable.
30
+ const EMBED_MODEL_CONTEXT_TOKENS = Math.max(1, Math.floor(envInt("EMBED_MODEL_CONTEXT_TOKENS", 2048)));
31
+ const EMBED_CONTEXT_MARGIN_TOKENS = 48;
32
+ const EMBED_WORST_CASE_CHARS_PER_TOKEN = 3;
33
+ exports.EMBED_INPUT_MAX_CHARS = Math.max(1, Math.floor(envInt("EMBED_INPUT_MAX_CHARS", (EMBED_MODEL_CONTEXT_TOKENS - EMBED_CONTEXT_MARGIN_TOKENS) *
34
+ EMBED_WORST_CASE_CHARS_PER_TOKEN)));
19
35
  // Batch pacing (Task 960). On a passively-cooled Pi, embedding a whole
20
36
  // document's sections in one `/api/embed` POST pins ollama's llama-server
21
37
  // across every core for the entire batch with no idle gaps — the SoC climbs
@@ -27,15 +43,39 @@ exports.EMBED_INPUT_MAX_CHARS = 8000;
27
43
  // this pacing is defence-in-depth and faster recovery. Both knobs are
28
44
  // env-overridable so an operator can tune them from on-device thermal
29
45
  // measurement without a code change.
30
- function envInt(name, def) {
31
- const raw = process.env[name];
32
- if (raw === undefined || raw === "")
33
- return def;
34
- const n = Number(raw);
35
- return Number.isFinite(n) ? n : def;
36
- }
37
46
  exports.EMBED_SUBBATCH_SIZE = Math.max(1, Math.floor(envInt("EMBED_SUBBATCH_SIZE", 32)));
38
47
  exports.EMBED_SUBBATCH_COOLDOWN_MS = Math.max(0, envInt("EMBED_SUBBATCH_COOLDOWN_MS", 1000));
48
+ // Throughput-aware sub-batch sizing (Task 1533). On a CPU-only embedder each
49
+ // input takes ~EMBED_MS_PER_INPUT ms; a fixed 32-input POST at the char cap is
50
+ // ~128 s, past the 120 s request timeout, so a full-document batch aborts and
51
+ // the atomic ingest write lands nothing. Bound each POST to 80% of the timeout
52
+ // so it provably completes: derive a duration ceiling and take the smaller of
53
+ // it and the count ceiling (EMBED_SUBBATCH_SIZE, Task 960's thermal cap).
54
+ // EMBED_MS_PER_INPUT is the budgeted worst-case wall-time for one input at the
55
+ // EMBED_INPUT_MAX_CHARS cap on the measured device; env-tunable for other
56
+ // hardware.
57
+ exports.EMBED_MS_PER_INPUT = Math.max(1, Math.floor(envInt("EMBED_MS_PER_INPUT", 6000)));
58
+ const EMBED_TIMEOUT_UTILISATION = 0.8;
59
+ // Explicit per-request timeout, well under undici's 300000ms headers default.
60
+ // Without it a slow or wedged ollama returns no headers and the caller hangs a
61
+ // silent 5 minutes before a bare `fetch failed`. With it the request aborts in
62
+ // seconds and throws a cause-naming error instead. Env-overridable for
63
+ // on-device tuning.
64
+ exports.EMBED_REQUEST_TIMEOUT_MS = Math.max(1, Math.floor(envInt("EMBED_REQUEST_TIMEOUT_MS", 120000)));
65
+ // Model warmth (Task 1533). Keep the embed model resident so an interactive
66
+ // graph query-embed never cold-loads (~10 s) and blows its 5 s timeout, and so
67
+ // a query issued while an ingest runs embeds warm instead of degrading to BM25.
68
+ // Ollama's keep_alive accepts a number (seconds; -1 = keep forever) or a
69
+ // duration string ("30m"). Pass a numeric value through as a number so "-1" is
70
+ // the keep-forever sentinel rather than an unparseable duration string.
71
+ // Treat an empty-string env as unset (matches envInt above), so
72
+ // `EMBED_KEEP_ALIVE=` does not send an empty keep_alive that Ollama rejects.
73
+ const EMBED_KEEP_ALIVE_RAW = process.env.EMBED_KEEP_ALIVE === undefined || process.env.EMBED_KEEP_ALIVE === ""
74
+ ? "-1"
75
+ : process.env.EMBED_KEEP_ALIVE;
76
+ exports.EMBED_KEEP_ALIVE = /^-?\d+$/.test(EMBED_KEEP_ALIVE_RAW)
77
+ ? Number(EMBED_KEEP_ALIVE_RAW)
78
+ : EMBED_KEEP_ALIVE_RAW;
39
79
  function capInput(text) {
40
80
  if (text.length <= exports.EMBED_INPUT_MAX_CHARS)
41
81
  return text;
@@ -43,49 +83,87 @@ function capInput(text) {
43
83
  return text.slice(0, exports.EMBED_INPUT_MAX_CHARS);
44
84
  }
45
85
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
46
- // POST one already-capped sub-batch of inputs to the embed endpoint.
47
- async function postBatch(inputs) {
48
- const res = await fetch(`${OLLAMA_URL}/api/embed`, {
49
- method: "POST",
50
- headers: { "Content-Type": "application/json" },
51
- body: JSON.stringify({ model: EMBED_MODEL, input: inputs }),
52
- });
86
+ // One POST to the embed endpoint for an already-capped input (a single string
87
+ // for embed(), an array for a sub-batch). Both paths route through here so the
88
+ // timeout and the cause-naming error apply on every embed call. The wire shape
89
+ // of `input` is preserved (string vs array) so the ollama request is unchanged.
90
+ // On timeout or failure the error's first line names the cause (timeout vs HTTP
91
+ // status), the input count, and the largest input size, so a slow or failing
92
+ // ollama is legible immediately rather than after a 5-minute silent hang.
93
+ async function embedRequest(input) {
94
+ const inputs = Array.isArray(input) ? input : [input];
95
+ const maxChars = inputs.reduce((m, s) => Math.max(m, s.length), 0);
96
+ const target = `${OLLAMA_URL}/api/embed`;
97
+ const startedAt = Date.now();
98
+ let res;
99
+ try {
100
+ res = await fetch(target, {
101
+ method: "POST",
102
+ headers: { "Content-Type": "application/json" },
103
+ body: JSON.stringify({ model: EMBED_MODEL, input, keep_alive: exports.EMBED_KEEP_ALIVE }),
104
+ signal: AbortSignal.timeout(exports.EMBED_REQUEST_TIMEOUT_MS),
105
+ });
106
+ }
107
+ catch (err) {
108
+ const name = err instanceof Error ? err.name : "";
109
+ if (name === "TimeoutError" || name === "AbortError") {
110
+ process.stderr.write(`[embed] op=timeout ms=${exports.EMBED_REQUEST_TIMEOUT_MS} inputs=${inputs.length} maxChars=${maxChars}\n`);
111
+ throw new Error(`Ollama embed request timed out after ${exports.EMBED_REQUEST_TIMEOUT_MS}ms (inputs=${inputs.length}, largestInputChars=${maxChars}) at ${target}; ollama did not respond in time (slow, cold-loading, or CPU-saturated). Raise EMBED_REQUEST_TIMEOUT_MS or check ollama.`);
112
+ }
113
+ // undici throws TypeError("fetch failed", { cause }) on a connection error
114
+ // (ollama down, refused, DNS). The legible cause is on err.cause, not the
115
+ // "fetch failed" message, so name it so "network is down" reads distinctly
116
+ // from "ollama is slow".
117
+ let detail = err instanceof Error ? err.message : String(err);
118
+ const cause = err instanceof Error ? err.cause : undefined;
119
+ if (cause !== undefined) {
120
+ detail += `: ${cause instanceof Error ? cause.message : String(cause)}`;
121
+ }
122
+ throw new Error(`Ollama embed request failed (inputs=${inputs.length}, largestInputChars=${maxChars}) at ${target}: ${detail}`);
123
+ }
53
124
  if (!res.ok) {
54
125
  const body = await res.text();
55
- throw new Error(`Ollama batch embedding failed (${res.status}): ${body}`);
126
+ throw new Error(`Ollama embed request failed (HTTP ${res.status}, inputs=${inputs.length}, largestInputChars=${maxChars}): ${body}`);
56
127
  }
57
128
  const data = (await res.json());
129
+ // Per-POST breadcrumb (Task 1533). One line per successful POST so an
130
+ // oversized or slow sub-batch is visible before it times out and the
131
+ // throughput-aware sizing is verifiable from the log. inputs is the POST's
132
+ // sub-batch size; maxChars its largest input.
133
+ process.stderr.write(`[embed] op=post inputs=${inputs.length} maxChars=${maxChars} ms=${Date.now() - startedAt}\n`);
58
134
  return data.embeddings;
59
135
  }
136
+ // POST one already-capped sub-batch of inputs to the embed endpoint.
137
+ async function postBatch(inputs) {
138
+ return embedRequest(inputs);
139
+ }
60
140
  async function embed(text) {
61
- const input = capInput(text);
62
- const res = await fetch(`${OLLAMA_URL}/api/embed`, {
63
- method: "POST",
64
- headers: { "Content-Type": "application/json" },
65
- body: JSON.stringify({ model: EMBED_MODEL, input }),
66
- });
67
- if (!res.ok) {
68
- const body = await res.text();
69
- throw new Error(`Ollama embedding failed (${res.status}): ${body}`);
70
- }
71
- const data = (await res.json());
72
- return data.embeddings[0];
141
+ const data = await embedRequest(capInput(text));
142
+ return data[0];
143
+ }
144
+ // The POST size actually used by embedBatch: the smaller of the count ceiling
145
+ // (thermal, Task 960) and the duration ceiling (throughput, Task 1533). At
146
+ // defaults: min(32, floor(120000*0.8/6000)) = min(32, 16) = 16.
147
+ function effectiveSubBatchSize() {
148
+ const durationCap = Math.max(1, Math.floor((exports.EMBED_REQUEST_TIMEOUT_MS * EMBED_TIMEOUT_UTILISATION) / exports.EMBED_MS_PER_INPUT));
149
+ return Math.min(exports.EMBED_SUBBATCH_SIZE, durationCap);
73
150
  }
74
151
  async function embedBatch(texts) {
75
152
  const inputs = texts.map(capInput);
153
+ const subBatch = effectiveSubBatchSize();
76
154
  // Common case — an array within the sub-batch size is one POST, unchanged.
77
- if (inputs.length <= exports.EMBED_SUBBATCH_SIZE) {
155
+ if (inputs.length <= subBatch) {
78
156
  return postBatch(inputs);
79
157
  }
80
158
  // Oversized: split into sub-batches, cooling between each (not after the
81
159
  // last). Vectors are concatenated in input order so callers can keep their
82
160
  // index-aligned `embeddings[i]` ↔ `texts[i]` contract.
83
161
  const out = [];
84
- for (let i = 0; i < inputs.length; i += exports.EMBED_SUBBATCH_SIZE) {
162
+ for (let i = 0; i < inputs.length; i += subBatch) {
85
163
  if (i > 0 && exports.EMBED_SUBBATCH_COOLDOWN_MS > 0) {
86
164
  await sleep(exports.EMBED_SUBBATCH_COOLDOWN_MS);
87
165
  }
88
- const chunk = inputs.slice(i, i + exports.EMBED_SUBBATCH_SIZE);
166
+ const chunk = inputs.slice(i, i + subBatch);
89
167
  out.push(...(await postBatch(chunk)));
90
168
  }
91
169
  return out;
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -8,11 +8,34 @@
8
8
  const OLLAMA_URL = process.env.OLLAMA_URL ?? "http://localhost:11434";
9
9
  const EMBED_MODEL = process.env.EMBED_MODEL ?? "nomic-embed-text";
10
10
 
11
- // nomic-embed-text has a 2048-token context (~8k chars at ~4 chars/token).
12
- // An over-limit input makes the Ollama embed endpoint error; cap defensively so
13
- // an oversized body embeds (truncated) instead of throwing and aborting the
14
- // caller.
15
- export const EMBED_INPUT_MAX_CHARS = 8000;
11
+ function envInt(name: string, def: number): number {
12
+ const raw = process.env[name];
13
+ if (raw === undefined || raw === "") return def;
14
+ const n = Number(raw);
15
+ return Number.isFinite(n) ? n : def;
16
+ }
17
+
18
+ // nomic-embed-text's usable embed context is 2048 tokens. The old 8000-char cap
19
+ // assumed ~4 chars/token, but dense emoji/markup content runs richer, so a
20
+ // section that passed the char cap still errored with "input length exceeds the
21
+ // context length". Size the default against a conservative 3 chars/token worst
22
+ // case, reserving a small token margin for the tokenizer's specials, so a capped
23
+ // input is provably under context: (2048 - 48) * 3 = 6000. Context is
24
+ // env-overridable so raising it raises the cap; EMBED_INPUT_MAX_CHARS is also
25
+ // directly overridable.
26
+ const EMBED_MODEL_CONTEXT_TOKENS = Math.max(1, Math.floor(envInt("EMBED_MODEL_CONTEXT_TOKENS", 2048)));
27
+ const EMBED_CONTEXT_MARGIN_TOKENS = 48;
28
+ const EMBED_WORST_CASE_CHARS_PER_TOKEN = 3;
29
+ export const EMBED_INPUT_MAX_CHARS = Math.max(
30
+ 1,
31
+ Math.floor(
32
+ envInt(
33
+ "EMBED_INPUT_MAX_CHARS",
34
+ (EMBED_MODEL_CONTEXT_TOKENS - EMBED_CONTEXT_MARGIN_TOKENS) *
35
+ EMBED_WORST_CASE_CHARS_PER_TOKEN,
36
+ ),
37
+ ),
38
+ );
16
39
 
17
40
  // Batch pacing (Task 960). On a passively-cooled Pi, embedding a whole
18
41
  // document's sections in one `/api/embed` POST pins ollama's llama-server
@@ -25,18 +48,50 @@ export const EMBED_INPUT_MAX_CHARS = 8000;
25
48
  // this pacing is defence-in-depth and faster recovery. Both knobs are
26
49
  // env-overridable so an operator can tune them from on-device thermal
27
50
  // measurement without a code change.
28
- function envInt(name: string, def: number): number {
29
- const raw = process.env[name];
30
- if (raw === undefined || raw === "") return def;
31
- const n = Number(raw);
32
- return Number.isFinite(n) ? n : def;
33
- }
34
51
  export const EMBED_SUBBATCH_SIZE = Math.max(1, Math.floor(envInt("EMBED_SUBBATCH_SIZE", 32)));
35
52
  export const EMBED_SUBBATCH_COOLDOWN_MS = Math.max(
36
53
  0,
37
54
  envInt("EMBED_SUBBATCH_COOLDOWN_MS", 1000),
38
55
  );
39
56
 
57
+ // Throughput-aware sub-batch sizing (Task 1533). On a CPU-only embedder each
58
+ // input takes ~EMBED_MS_PER_INPUT ms; a fixed 32-input POST at the char cap is
59
+ // ~128 s, past the 120 s request timeout, so a full-document batch aborts and
60
+ // the atomic ingest write lands nothing. Bound each POST to 80% of the timeout
61
+ // so it provably completes: derive a duration ceiling and take the smaller of
62
+ // it and the count ceiling (EMBED_SUBBATCH_SIZE, Task 960's thermal cap).
63
+ // EMBED_MS_PER_INPUT is the budgeted worst-case wall-time for one input at the
64
+ // EMBED_INPUT_MAX_CHARS cap on the measured device; env-tunable for other
65
+ // hardware.
66
+ export const EMBED_MS_PER_INPUT = Math.max(1, Math.floor(envInt("EMBED_MS_PER_INPUT", 6000)));
67
+ const EMBED_TIMEOUT_UTILISATION = 0.8;
68
+
69
+ // Explicit per-request timeout, well under undici's 300000ms headers default.
70
+ // Without it a slow or wedged ollama returns no headers and the caller hangs a
71
+ // silent 5 minutes before a bare `fetch failed`. With it the request aborts in
72
+ // seconds and throws a cause-naming error instead. Env-overridable for
73
+ // on-device tuning.
74
+ export const EMBED_REQUEST_TIMEOUT_MS = Math.max(
75
+ 1,
76
+ Math.floor(envInt("EMBED_REQUEST_TIMEOUT_MS", 120000)),
77
+ );
78
+
79
+ // Model warmth (Task 1533). Keep the embed model resident so an interactive
80
+ // graph query-embed never cold-loads (~10 s) and blows its 5 s timeout, and so
81
+ // a query issued while an ingest runs embeds warm instead of degrading to BM25.
82
+ // Ollama's keep_alive accepts a number (seconds; -1 = keep forever) or a
83
+ // duration string ("30m"). Pass a numeric value through as a number so "-1" is
84
+ // the keep-forever sentinel rather than an unparseable duration string.
85
+ // Treat an empty-string env as unset (matches envInt above), so
86
+ // `EMBED_KEEP_ALIVE=` does not send an empty keep_alive that Ollama rejects.
87
+ const EMBED_KEEP_ALIVE_RAW =
88
+ process.env.EMBED_KEEP_ALIVE === undefined || process.env.EMBED_KEEP_ALIVE === ""
89
+ ? "-1"
90
+ : process.env.EMBED_KEEP_ALIVE;
91
+ export const EMBED_KEEP_ALIVE: number | string = /^-?\d+$/.test(EMBED_KEEP_ALIVE_RAW)
92
+ ? Number(EMBED_KEEP_ALIVE_RAW)
93
+ : EMBED_KEEP_ALIVE_RAW;
94
+
40
95
  function capInput(text: string): string {
41
96
  if (text.length <= EMBED_INPUT_MAX_CHARS) return text;
42
97
  process.stderr.write(
@@ -48,41 +103,93 @@ function capInput(text: string): string {
48
103
  const sleep = (ms: number): Promise<void> =>
49
104
  new Promise((resolve) => setTimeout(resolve, ms));
50
105
 
51
- // POST one already-capped sub-batch of inputs to the embed endpoint.
52
- async function postBatch(inputs: string[]): Promise<number[][]> {
53
- const res = await fetch(`${OLLAMA_URL}/api/embed`, {
54
- method: "POST",
55
- headers: { "Content-Type": "application/json" },
56
- body: JSON.stringify({ model: EMBED_MODEL, input: inputs }),
57
- });
106
+ // One POST to the embed endpoint for an already-capped input (a single string
107
+ // for embed(), an array for a sub-batch). Both paths route through here so the
108
+ // timeout and the cause-naming error apply on every embed call. The wire shape
109
+ // of `input` is preserved (string vs array) so the ollama request is unchanged.
110
+ // On timeout or failure the error's first line names the cause (timeout vs HTTP
111
+ // status), the input count, and the largest input size, so a slow or failing
112
+ // ollama is legible immediately rather than after a 5-minute silent hang.
113
+ async function embedRequest(input: string | string[]): Promise<number[][]> {
114
+ const inputs = Array.isArray(input) ? input : [input];
115
+ const maxChars = inputs.reduce((m, s) => Math.max(m, s.length), 0);
116
+ const target = `${OLLAMA_URL}/api/embed`;
117
+ const startedAt = Date.now();
118
+ let res: Response;
119
+ try {
120
+ res = await fetch(target, {
121
+ method: "POST",
122
+ headers: { "Content-Type": "application/json" },
123
+ body: JSON.stringify({ model: EMBED_MODEL, input, keep_alive: EMBED_KEEP_ALIVE }),
124
+ signal: AbortSignal.timeout(EMBED_REQUEST_TIMEOUT_MS),
125
+ });
126
+ } catch (err) {
127
+ const name = err instanceof Error ? err.name : "";
128
+ if (name === "TimeoutError" || name === "AbortError") {
129
+ process.stderr.write(
130
+ `[embed] op=timeout ms=${EMBED_REQUEST_TIMEOUT_MS} inputs=${inputs.length} maxChars=${maxChars}\n`,
131
+ );
132
+ throw new Error(
133
+ `Ollama embed request timed out after ${EMBED_REQUEST_TIMEOUT_MS}ms (inputs=${inputs.length}, largestInputChars=${maxChars}) at ${target}; ollama did not respond in time (slow, cold-loading, or CPU-saturated). Raise EMBED_REQUEST_TIMEOUT_MS or check ollama.`,
134
+ );
135
+ }
136
+ // undici throws TypeError("fetch failed", { cause }) on a connection error
137
+ // (ollama down, refused, DNS). The legible cause is on err.cause, not the
138
+ // "fetch failed" message, so name it so "network is down" reads distinctly
139
+ // from "ollama is slow".
140
+ let detail = err instanceof Error ? err.message : String(err);
141
+ const cause = err instanceof Error ? (err as { cause?: unknown }).cause : undefined;
142
+ if (cause !== undefined) {
143
+ detail += `: ${cause instanceof Error ? cause.message : String(cause)}`;
144
+ }
145
+ throw new Error(
146
+ `Ollama embed request failed (inputs=${inputs.length}, largestInputChars=${maxChars}) at ${target}: ${detail}`,
147
+ );
148
+ }
58
149
  if (!res.ok) {
59
150
  const body = await res.text();
60
- throw new Error(`Ollama batch embedding failed (${res.status}): ${body}`);
151
+ throw new Error(
152
+ `Ollama embed request failed (HTTP ${res.status}, inputs=${inputs.length}, largestInputChars=${maxChars}): ${body}`,
153
+ );
61
154
  }
62
155
  const data = (await res.json()) as { embeddings: number[][] };
156
+ // Per-POST breadcrumb (Task 1533). One line per successful POST so an
157
+ // oversized or slow sub-batch is visible before it times out and the
158
+ // throughput-aware sizing is verifiable from the log. inputs is the POST's
159
+ // sub-batch size; maxChars its largest input.
160
+ process.stderr.write(
161
+ `[embed] op=post inputs=${inputs.length} maxChars=${maxChars} ms=${Date.now() - startedAt}\n`,
162
+ );
63
163
  return data.embeddings;
64
164
  }
65
165
 
166
+ // POST one already-capped sub-batch of inputs to the embed endpoint.
167
+ async function postBatch(inputs: string[]): Promise<number[][]> {
168
+ return embedRequest(inputs);
169
+ }
170
+
66
171
  export async function embed(text: string): Promise<number[]> {
67
- const input = capInput(text);
68
- const res = await fetch(`${OLLAMA_URL}/api/embed`, {
69
- method: "POST",
70
- headers: { "Content-Type": "application/json" },
71
- body: JSON.stringify({ model: EMBED_MODEL, input }),
72
- });
73
- if (!res.ok) {
74
- const body = await res.text();
75
- throw new Error(`Ollama embedding failed (${res.status}): ${body}`);
76
- }
77
- const data = (await res.json()) as { embeddings: number[][] };
78
- return data.embeddings[0];
172
+ const data = await embedRequest(capInput(text));
173
+ return data[0];
174
+ }
175
+
176
+ // The POST size actually used by embedBatch: the smaller of the count ceiling
177
+ // (thermal, Task 960) and the duration ceiling (throughput, Task 1533). At
178
+ // defaults: min(32, floor(120000*0.8/6000)) = min(32, 16) = 16.
179
+ export function effectiveSubBatchSize(): number {
180
+ const durationCap = Math.max(
181
+ 1,
182
+ Math.floor((EMBED_REQUEST_TIMEOUT_MS * EMBED_TIMEOUT_UTILISATION) / EMBED_MS_PER_INPUT),
183
+ );
184
+ return Math.min(EMBED_SUBBATCH_SIZE, durationCap);
79
185
  }
80
186
 
81
187
  export async function embedBatch(texts: string[]): Promise<number[][]> {
82
188
  const inputs = texts.map(capInput);
189
+ const subBatch = effectiveSubBatchSize();
83
190
 
84
191
  // Common case — an array within the sub-batch size is one POST, unchanged.
85
- if (inputs.length <= EMBED_SUBBATCH_SIZE) {
192
+ if (inputs.length <= subBatch) {
86
193
  return postBatch(inputs);
87
194
  }
88
195
 
@@ -90,11 +197,11 @@ export async function embedBatch(texts: string[]): Promise<number[][]> {
90
197
  // last). Vectors are concatenated in input order so callers can keep their
91
198
  // index-aligned `embeddings[i]` ↔ `texts[i]` contract.
92
199
  const out: number[][] = [];
93
- for (let i = 0; i < inputs.length; i += EMBED_SUBBATCH_SIZE) {
200
+ for (let i = 0; i < inputs.length; i += subBatch) {
94
201
  if (i > 0 && EMBED_SUBBATCH_COOLDOWN_MS > 0) {
95
202
  await sleep(EMBED_SUBBATCH_COOLDOWN_MS);
96
203
  }
97
- const chunk = inputs.slice(i, i + EMBED_SUBBATCH_SIZE);
204
+ const chunk = inputs.slice(i, i + subBatch);
98
205
  out.push(...(await postBatch(chunk)));
99
206
  }
100
207
  return out;
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: platform-architecture
3
3
  description: Use when grounding any documented-surface claim about what Maxy ships — plugins, skills, specialists, install/deploy flows, internals. This is the install catalogue, not evidence of what is enabled on the current account. For install state on this account, call `capabilities-here`; for documented surface, cite the `Source:` URL inline.
4
- content-hash: sha256:a557577a5292537ca408682f87fd5ab625194fc595602a4f2e886088d1ae40c1
4
+ content-hash: sha256:1dcb3b43e4fbb1e5689cd8087e9f58e4a0e5e330fd7bd02df320f59a9bbc035b
5
5
  brand: maxy-code
6
6
  product-name: Maxy
7
7
  ---
@@ -2690,6 +2690,8 @@ either is a regression.
2690
2690
 
2691
2691
  **`/chat` Claude-desktop transcript presentation.** The admin webchat keeps the shared `Transcript` shell (stream, follow-tail) but injects a /chat-only item renderer via the component's optional `renderItems` prop: `renderChatTimeline` in `app/chat/transcript-render.tsx`. Presentation: operator turns render as a right-aligned grey bubble showing the message text and, beneath it, a subtle always-visible time-of-day stamp (no label); delivered agent replies render as plain prose with the same time-of-day stamp beneath (the reply-document filename line stays, as prose); the stamp is HH:MM from the turn's `ts` (locale-formatted, right-aligned inside the operator bubble, left-aligned under agent prose), and a turn whose `ts` is null/unparseable shows no stamp, never an empty line — so only delivered operator and agent-reply turns are stamped, while tool runs, the collapsed "Thinking" block, and the agent-error banner stay time-free; a maximal consecutive run of `tool-call`/`tool-result` turns renders as one collapsed grey one-liner ("Used N tools ›") whose expansion shows every call and result payload — a lone call still gets the one-liner, and prose or a directive row ends a run. The WhatsApp/operator reader (`/whatsapp`, `OperatorConversations`) omits the prop and keeps the default `renderTimeline`, so its bubble/DM chrome is unchanged — except that since the attachment-render extraction the default renderer draws inbound `operator-inbound` uploads and `agent-file` deliveries inline (image thumbnail / pdf.js preview / audio player / download chip) via the attachment renderer shared with `/chat`, extracted to `app/chat/attachment-render.tsx` to break the cycle with `transcript-render.tsx`; `renderChatTimeline` is untouched. Both presentations are test-pinned (`app/whatsapp/__tests__/Transcript-*.test.tsx`, `app/chat/__tests__/transcript-render.test.tsx`, `app/chat/__tests__/pdf-preview.test.tsx`). **Day-divider:** both renderers insert a centered `.day-divider` row between two consecutive timeline items on different local calendar days (label `Today`/`Yesterday`, else `Sat 14 Jun 2026`), so a thread spanning midnight is never an ambiguous run of HH:MM; the first dated item gets a leading divider, a null/unparseable `ts` marks no boundary, dividers are computed over the filtered `visibleItems`, and in `/chat` a day crossover flushes any open tool/think run first so a collapsed run never spans it. Per-bubble HH:MM stamps are unchanged. Shared helpers `dayKey`/`dayLabel`/`itemTs`/`DayDivider` live in `app/whatsapp/Transcript.tsx`.
2692
2692
 
2693
+ **WhatsApp reader is read/write (WhatsApp-Web surface).** A WhatsApp store conversation in the operator Conversations reader (`OperatorConversations`) and the admin root channel viewer (`app/page.tsx`) no longer opens the plain read-only DM chrome — it opens `app/whatsapp/WhatsappWebConversation.tsx`, a WhatsApp-Web-styled surface: a conversation header, the delivered-only store transcript reskinned green/white under a `.wa-web` root class, and a reply composer (`app/whatsapp/ReplyComposer.tsx`). The operator can type text and attach files (the webchat allowlist: `SUPPORTED_MIME_SET` / 50 MB / 5 files) and send them into that WhatsApp conversation. **Send is reply-only and DM-only:** `POST /api/whatsapp-reader/reply` (`requireAdminSession`) refuses any target with no existing stored conversation (`reason=no-existing-conversation`, HTTP 403) — a cold first-contact send bans the account, so the composer is never a cold-contact path, enforced server-side — and refuses `@g.us` groups (`reason=group-send-blocked`), matching the agent's observe-only group invariant; a group thread shows a read-only note in place of the composer. The endpoint is the sole store writer for a manual send: right after each send resolves it appends the record itself, stamped `origin:'operator-manual'` (attributed "You", distinct from an assistant reply), because the Baileys echo that normally persists the store is gated on message text (`manager.ts:906`) and would leave an uncaptioned media send delivered-but-invisible. The record's `messageId` matches the echo's derivation so the async text echo's own persist dedups to a no-op; the send is confirmed by re-read (`op=recorded present=<bool>`). A sent send does not spawn an assistant turn (echo dispatch stays suppressed). Attachments deliver as WhatsApp media/documents and appear in the reader as an attributed text line (`[sent <file>]`) — inline media rendering in the store reader is a follow-up. The `.wa-web` skin is scoped to WhatsApp store threads only, so `/chat`, the admin agent session view, and the public visitor webchat are unchanged; the visitor webchat gains no send capability. The header call/video/search icons and the in-bubble delivered-tick are cosmetic (no presence or read-receipt data is stored); inbound bubbles keep their sender name so group threads stay attributable. Non-WhatsApp reader rows (Telegram/webchat session JSONLs) keep the plain read-only `Transcript`. Observability rides `[wa-manual-send]` lines correlated by a per-request `sendId`.
2694
+
2693
2695
  **`/chat` live activity line.** While a turn is in flight the transcript tail shows one ephemeral `ChatActivity` line (mounted only while `busy`, admin/operator only). It tracks real activity, not a timer: while a `Task` subagent runs it shows that subagent's headline (`agentType · description`, prefixed `N agents ·` when ≥2 are concurrent), sourced from `agent-<hex>.meta.json` via named `activity` SSE events the admin reader pushes from the session's `subagents/` dir; with no subagent active it shows a neutral word that advances only on a real turn arrival. The line carries a turn-elapsed clock and flips to a `stalled` state once nothing has been written for 5 minutes (`now − lastEmitAt`), so the operator can tell a wedge from progress without SSH. It is never added to the persisted timeline. Detail and the `[webchat-activity]` observability live in [`admin-webchat-native-channel.md`](../../../.docs/admin-webchat-native-channel.md).
2694
2696
 
2695
2697
  **maxy title for public sessions.** A `role=public` webchat spawn never produces a useful Claude Code `ai-title` (an anonymous one-line visitor turn), so every public row would otherwise read identically. The webchat route (`chat.ts`) composes a deterministic title — `Web · <senderId[:8]>[ · <personId>] · <UTC YYYY-MM-DD HH:mm>` (personId present only for gated visitors) — and threads it through the native webchat gateway's public spawn (`handleInbound` → `buildPublicWebchatSpawnRequest` → `managerSpawn`) to the manager `POST /public-spawn` body as `name`. `/spawn` validates it with `validateUserTitle` and, for `role=public` only, writes it into `UserTitleStore` so it occupies the operator-rename tier and wins over `ai-title`. Admin (`/rc-spawn`) and WhatsApp titling are unchanged. Observability: every public spawn logs `[spawn] role=public … title="…"` (or `title=missing`); the manager's row builder emits `[public-title] sessionId=… unexpected titleSource=<ai|null>` on the next list read for any public row that did not resolve from the user tier.
@@ -3498,7 +3500,7 @@ If Ollama is unavailable at write time, the node is stored without an embedding
3498
3500
 
3499
3501
  The `memory-reindex` tool backfills missing embeddings by iterating nodes where `embedding IS NULL`. It uses the bounded representation (the `summary` for document/archive labels, never the raw body), embeds in batches with a per-node fallback on batch error, and **skips and continues** past a failing node — keeping skipped nodes out of the re-fetch via an exclude set — instead of aborting the whole run. `embed()`/`embedBatch()` cap oversized input (`EMBED_INPUT_MAX_CHARS`) before the Ollama POST, so a large body embeds truncated rather than throwing. Per-node `[reindex] op=embedded|skip-error` lines and a terminal `[reindex] op=done processed=N skipped=M`.
3500
3502
 
3501
- `embedBatch()` also paces large batches. A document's summary plus every section body in one `/api/embed` POST pins ollama's `llama-server` across all cores for the whole batch with no idle gaps; on a passively-cooled Pi the SoC climbs past its soft-temp limit and the board thermally hard-resets. `embedBatch` splits an input array longer than `EMBED_SUBBATCH_SIZE` (default 32) into sub-batches, waiting `EMBED_SUBBATCH_COOLDOWN_MS` (default 1000) between them so the SoC gets cooling gaps; vectors are concatenated in input order. Both knobs are env-overridable for on-device tuning. The load-bearing thermal protection is the installer's `CPUQuota` cap on `ollama.service` (every embed caller is bounded there regardless of batch shape); this pacing is defence-in-depth. The UI's file-index path uses its own single-input embed client in `platform/ui/app/lib/neo4j-store.ts` (one embed per file, not a batch), so it is covered by the CPU cap, not by `embedBatch` pacing.
3503
+ `embedBatch()` also paces large batches. A document's summary plus every section body in one `/api/embed` POST pins ollama's `llama-server` across all cores for the whole batch with no idle gaps; on a passively-cooled Pi the SoC climbs past its soft-temp limit and the board thermally hard-resets. `embedBatch` splits an input array longer than the effective sub-batch size into sub-batches, waiting `EMBED_SUBBATCH_COOLDOWN_MS` (default 1000) between them so the SoC gets cooling gaps; vectors are concatenated in input order. The effective sub-batch size is the smaller of two ceilings: the thermal count ceiling `EMBED_SUBBATCH_SIZE` (default 32) and a throughput ceiling `floor(EMBED_REQUEST_TIMEOUT_MS * 0.8 / EMBED_MS_PER_INPUT)`. The throughput ceiling exists because on a CPU-only embedder a fixed 32-input POST can exceed `EMBED_REQUEST_TIMEOUT_MS` and abort the atomic ingest write, so `EMBED_MS_PER_INPUT` (the budgeted worst-case wall-time for one capped input, default 6000 ms, env-tunable) bounds each POST to 80 % of the request timeout. Every `/api/embed` POST also carries `keep_alive` (`EMBED_KEEP_ALIVE`, default `-1` = keep the model resident forever) so an interactive graph query-embed never cold-loads and blows its 5 s timeout. All knobs are env-overridable for on-device tuning. The load-bearing thermal protection is the installer's `CPUQuota` cap on `ollama.service` (every embed caller is bounded there regardless of batch shape); this pacing is defence-in-depth. The UI's file-index path uses its own single-input embed client in `platform/ui/app/lib/neo4j-store.ts` (one embed per file, not a batch), so it is covered by the CPU cap, not by `embedBatch` pacing.
3502
3504
 
3503
3505
  ---
3504
3506
 
@@ -75,6 +75,8 @@ either is a regression.
75
75
 
76
76
  **`/chat` Claude-desktop transcript presentation.** The admin webchat keeps the shared `Transcript` shell (stream, follow-tail) but injects a /chat-only item renderer via the component's optional `renderItems` prop: `renderChatTimeline` in `app/chat/transcript-render.tsx`. Presentation: operator turns render as a right-aligned grey bubble showing the message text and, beneath it, a subtle always-visible time-of-day stamp (no label); delivered agent replies render as plain prose with the same time-of-day stamp beneath (the reply-document filename line stays, as prose); the stamp is HH:MM from the turn's `ts` (locale-formatted, right-aligned inside the operator bubble, left-aligned under agent prose), and a turn whose `ts` is null/unparseable shows no stamp, never an empty line — so only delivered operator and agent-reply turns are stamped, while tool runs, the collapsed "Thinking" block, and the agent-error banner stay time-free; a maximal consecutive run of `tool-call`/`tool-result` turns renders as one collapsed grey one-liner ("Used N tools ›") whose expansion shows every call and result payload — a lone call still gets the one-liner, and prose or a directive row ends a run. The WhatsApp/operator reader (`/whatsapp`, `OperatorConversations`) omits the prop and keeps the default `renderTimeline`, so its bubble/DM chrome is unchanged — except that since the attachment-render extraction the default renderer draws inbound `operator-inbound` uploads and `agent-file` deliveries inline (image thumbnail / pdf.js preview / audio player / download chip) via the attachment renderer shared with `/chat`, extracted to `app/chat/attachment-render.tsx` to break the cycle with `transcript-render.tsx`; `renderChatTimeline` is untouched. Both presentations are test-pinned (`app/whatsapp/__tests__/Transcript-*.test.tsx`, `app/chat/__tests__/transcript-render.test.tsx`, `app/chat/__tests__/pdf-preview.test.tsx`). **Day-divider:** both renderers insert a centered `.day-divider` row between two consecutive timeline items on different local calendar days (label `Today`/`Yesterday`, else `Sat 14 Jun 2026`), so a thread spanning midnight is never an ambiguous run of HH:MM; the first dated item gets a leading divider, a null/unparseable `ts` marks no boundary, dividers are computed over the filtered `visibleItems`, and in `/chat` a day crossover flushes any open tool/think run first so a collapsed run never spans it. Per-bubble HH:MM stamps are unchanged. Shared helpers `dayKey`/`dayLabel`/`itemTs`/`DayDivider` live in `app/whatsapp/Transcript.tsx`.
77
77
 
78
+ **WhatsApp reader is read/write (WhatsApp-Web surface).** A WhatsApp store conversation in the operator Conversations reader (`OperatorConversations`) and the admin root channel viewer (`app/page.tsx`) no longer opens the plain read-only DM chrome — it opens `app/whatsapp/WhatsappWebConversation.tsx`, a WhatsApp-Web-styled surface: a conversation header, the delivered-only store transcript reskinned green/white under a `.wa-web` root class, and a reply composer (`app/whatsapp/ReplyComposer.tsx`). The operator can type text and attach files (the webchat allowlist: `SUPPORTED_MIME_SET` / 50 MB / 5 files) and send them into that WhatsApp conversation. **Send is reply-only and DM-only:** `POST /api/whatsapp-reader/reply` (`requireAdminSession`) refuses any target with no existing stored conversation (`reason=no-existing-conversation`, HTTP 403) — a cold first-contact send bans the account, so the composer is never a cold-contact path, enforced server-side — and refuses `@g.us` groups (`reason=group-send-blocked`), matching the agent's observe-only group invariant; a group thread shows a read-only note in place of the composer. The endpoint is the sole store writer for a manual send: right after each send resolves it appends the record itself, stamped `origin:'operator-manual'` (attributed "You", distinct from an assistant reply), because the Baileys echo that normally persists the store is gated on message text (`manager.ts:906`) and would leave an uncaptioned media send delivered-but-invisible. The record's `messageId` matches the echo's derivation so the async text echo's own persist dedups to a no-op; the send is confirmed by re-read (`op=recorded present=<bool>`). A sent send does not spawn an assistant turn (echo dispatch stays suppressed). Attachments deliver as WhatsApp media/documents and appear in the reader as an attributed text line (`[sent <file>]`) — inline media rendering in the store reader is a follow-up. The `.wa-web` skin is scoped to WhatsApp store threads only, so `/chat`, the admin agent session view, and the public visitor webchat are unchanged; the visitor webchat gains no send capability. The header call/video/search icons and the in-bubble delivered-tick are cosmetic (no presence or read-receipt data is stored); inbound bubbles keep their sender name so group threads stay attributable. Non-WhatsApp reader rows (Telegram/webchat session JSONLs) keep the plain read-only `Transcript`. Observability rides `[wa-manual-send]` lines correlated by a per-request `sendId`.
79
+
78
80
  **`/chat` live activity line.** While a turn is in flight the transcript tail shows one ephemeral `ChatActivity` line (mounted only while `busy`, admin/operator only). It tracks real activity, not a timer: while a `Task` subagent runs it shows that subagent's headline (`agentType · description`, prefixed `N agents ·` when ≥2 are concurrent), sourced from `agent-<hex>.meta.json` via named `activity` SSE events the admin reader pushes from the session's `subagents/` dir; with no subagent active it shows a neutral word that advances only on a real turn arrival. The line carries a turn-elapsed clock and flips to a `stalled` state once nothing has been written for 5 minutes (`now − lastEmitAt`), so the operator can tell a wedge from progress without SSH. It is never added to the persisted timeline. Detail and the `[webchat-activity]` observability live in [`admin-webchat-native-channel.md`](../../../.docs/admin-webchat-native-channel.md).
79
81
 
80
82
  **maxy title for public sessions.** A `role=public` webchat spawn never produces a useful Claude Code `ai-title` (an anonymous one-line visitor turn), so every public row would otherwise read identically. The webchat route (`chat.ts`) composes a deterministic title — `Web · <senderId[:8]>[ · <personId>] · <UTC YYYY-MM-DD HH:mm>` (personId present only for gated visitors) — and threads it through the native webchat gateway's public spawn (`handleInbound` → `buildPublicWebchatSpawnRequest` → `managerSpawn`) to the manager `POST /public-spawn` body as `name`. `/spawn` validates it with `validateUserTitle` and, for `role=public` only, writes it into `UserTitleStore` so it occupies the operator-rename tier and wins over `ai-title`. Admin (`/rc-spawn`) and WhatsApp titling are unchanged. Observability: every public spawn logs `[spawn] role=public … title="…"` (or `title=missing`); the manager's row builder emits `[public-title] sessionId=… unexpected titleSource=<ai|null>` on the next list read for any public row that did not resolve from the user tier.
@@ -95,7 +95,7 @@ If Ollama is unavailable at write time, the node is stored without an embedding
95
95
 
96
96
  The `memory-reindex` tool backfills missing embeddings by iterating nodes where `embedding IS NULL`. It uses the bounded representation (the `summary` for document/archive labels, never the raw body), embeds in batches with a per-node fallback on batch error, and **skips and continues** past a failing node — keeping skipped nodes out of the re-fetch via an exclude set — instead of aborting the whole run. `embed()`/`embedBatch()` cap oversized input (`EMBED_INPUT_MAX_CHARS`) before the Ollama POST, so a large body embeds truncated rather than throwing. Per-node `[reindex] op=embedded|skip-error` lines and a terminal `[reindex] op=done processed=N skipped=M`.
97
97
 
98
- `embedBatch()` also paces large batches. A document's summary plus every section body in one `/api/embed` POST pins ollama's `llama-server` across all cores for the whole batch with no idle gaps; on a passively-cooled Pi the SoC climbs past its soft-temp limit and the board thermally hard-resets. `embedBatch` splits an input array longer than `EMBED_SUBBATCH_SIZE` (default 32) into sub-batches, waiting `EMBED_SUBBATCH_COOLDOWN_MS` (default 1000) between them so the SoC gets cooling gaps; vectors are concatenated in input order. Both knobs are env-overridable for on-device tuning. The load-bearing thermal protection is the installer's `CPUQuota` cap on `ollama.service` (every embed caller is bounded there regardless of batch shape); this pacing is defence-in-depth. The UI's file-index path uses its own single-input embed client in `platform/ui/app/lib/neo4j-store.ts` (one embed per file, not a batch), so it is covered by the CPU cap, not by `embedBatch` pacing.
98
+ `embedBatch()` also paces large batches. A document's summary plus every section body in one `/api/embed` POST pins ollama's `llama-server` across all cores for the whole batch with no idle gaps; on a passively-cooled Pi the SoC climbs past its soft-temp limit and the board thermally hard-resets. `embedBatch` splits an input array longer than the effective sub-batch size into sub-batches, waiting `EMBED_SUBBATCH_COOLDOWN_MS` (default 1000) between them so the SoC gets cooling gaps; vectors are concatenated in input order. The effective sub-batch size is the smaller of two ceilings: the thermal count ceiling `EMBED_SUBBATCH_SIZE` (default 32) and a throughput ceiling `floor(EMBED_REQUEST_TIMEOUT_MS * 0.8 / EMBED_MS_PER_INPUT)`. The throughput ceiling exists because on a CPU-only embedder a fixed 32-input POST can exceed `EMBED_REQUEST_TIMEOUT_MS` and abort the atomic ingest write, so `EMBED_MS_PER_INPUT` (the budgeted worst-case wall-time for one capped input, default 6000 ms, env-tunable) bounds each POST to 80 % of the request timeout. Every `/api/embed` POST also carries `keep_alive` (`EMBED_KEEP_ALIVE`, default `-1` = keep the model resident forever) so an interactive graph query-embed never cold-loads and blows its 5 s timeout. All knobs are env-overridable for on-device tuning. The load-bearing thermal protection is the installer's `CPUQuota` cap on `ollama.service` (every embed caller is bounded there regardless of batch shape); this pacing is defence-in-depth. The UI's file-index path uses its own single-input embed client in `platform/ui/app/lib/neo4j-store.ts` (one embed per file, not a batch), so it is covered by the CPU cap, not by `embedBatch` pacing.
99
99
 
100
100
  ---
101
101
 
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=embed-throughput.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"embed-throughput.test.d.ts","sourceRoot":"","sources":["../../../src/tools/__tests__/embed-throughput.test.ts"],"names":[],"mappings":""}