@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.
- package/package.json +1 -1
- package/payload/platform/lib/embed-client/dist/index.d.ts +5 -1
- package/payload/platform/lib/embed-client/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/embed-client/dist/index.js +114 -36
- package/payload/platform/lib/embed-client/dist/index.js.map +1 -1
- package/payload/platform/lib/embed-client/src/index.ts +141 -34
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +4 -2
- package/payload/platform/plugins/docs/references/admin-ui.md +2 -0
- package/payload/platform/plugins/docs/references/internals.md +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/embed-throughput.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/embed-throughput.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/embed-throughput.test.js +92 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/embed-throughput.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/embed-timeout.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/embed-timeout.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/embed-timeout.test.js +56 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/embed-timeout.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/embeddings-cap.test.js +56 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/embeddings-cap.test.js.map +1 -1
- package/payload/platform/plugins/work/mcp/dist/lib/embeddings.d.ts +9 -3
- package/payload/platform/plugins/work/mcp/dist/lib/embeddings.d.ts.map +1 -1
- package/payload/platform/plugins/work/mcp/dist/lib/embeddings.js +19 -15
- package/payload/platform/plugins/work/mcp/dist/lib/embeddings.js.map +1 -1
- package/payload/platform/plugins/workflows/mcp/dist/lib/embeddings.d.ts +1 -1
- package/payload/platform/plugins/workflows/mcp/dist/lib/embeddings.d.ts.map +1 -1
- package/payload/platform/plugins/workflows/mcp/dist/lib/embeddings.js +7 -18
- package/payload/platform/plugins/workflows/mcp/dist/lib/embeddings.js.map +1 -1
- package/payload/server/{chunk-3Z5WE53X.js → chunk-UUG6ZSO5.js} +65 -9
- package/payload/server/maxy-edge.js +1 -1
- package/payload/server/public/assets/AdminLoginScreens-C4AZT7cQ.js +1 -0
- package/payload/server/public/assets/AdminShell-BfLQQ27Z.js +2 -0
- package/payload/server/public/assets/{Checkbox-B_lfjsqd.js → Checkbox-BC0c_gbn.js} +1 -1
- package/payload/server/public/assets/admin-SaF1F4e-.js +1 -0
- package/payload/server/public/assets/{arc-dVLpR-ec.js → arc-qKfXIt8v.js} +1 -1
- package/payload/server/public/assets/architecture-YZFGNWBL-FPHT7s-C.js +1 -0
- package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-C6u_CVzb.js → architectureDiagram-Q4EWVU46-DkVrGn7G.js} +1 -1
- package/payload/server/public/assets/audio-attachment-mime-BBW07eyG.js +2 -0
- package/payload/server/public/assets/{blockDiagram-DXYQGD6D-Cy8F5b8O.js → blockDiagram-DXYQGD6D-DqavswGH.js} +1 -1
- package/payload/server/public/assets/{browser-CCWufeXr.js → browser-CvZxNdKw.js} +1 -1
- package/payload/server/public/assets/{c4Diagram-AHTNJAMY-DRXrlepj.js → c4Diagram-AHTNJAMY-B7fyKiXe.js} +1 -1
- package/payload/server/public/assets/calendar-DXxLMF2J.js +1 -0
- package/payload/server/public/assets/channel-j70eMvcL.js +1 -0
- package/payload/server/public/assets/chat-B9Qjt7C9.js +1 -0
- package/payload/server/public/assets/chevron-left-CLPxfjDV.js +1 -0
- package/payload/server/public/assets/{chunk-2KRD3SAO-fEyfhRLz.js → chunk-2KRD3SAO-DsgMtGkv.js} +1 -1
- package/payload/server/public/assets/{chunk-336JU56O-NUWO4y7q.js → chunk-336JU56O-CkdISfNt.js} +2 -2
- package/payload/server/public/assets/chunk-426QAEUC-D-NcAlTW.js +1 -0
- package/payload/server/public/assets/{chunk-4BX2VUAB-BnfiNR3l.js → chunk-4BX2VUAB-CYpiF2bl.js} +1 -1
- package/payload/server/public/assets/{chunk-4TB4RGXK-Bj_aJOz8.js → chunk-4TB4RGXK-CWyc_F7v.js} +1 -1
- package/payload/server/public/assets/{chunk-55IACEB6-aK0DNSKb.js → chunk-55IACEB6-BbzKMw2Z.js} +1 -1
- package/payload/server/public/assets/{chunk-5FUZZQ4R-DeqAkq5C.js → chunk-5FUZZQ4R-u19Nai9e.js} +1 -1
- package/payload/server/public/assets/{chunk-5PVQY5BW-SShlujAQ.js → chunk-5PVQY5BW-Ba8by6Uy.js} +1 -1
- package/payload/server/public/assets/{chunk-67CJDMHE-CZpzQ0DL.js → chunk-67CJDMHE-BwUuxtyb.js} +1 -1
- package/payload/server/public/assets/{chunk-7N4EOEYR-COnjO6jy.js → chunk-7N4EOEYR-BRH9iM9L.js} +1 -1
- package/payload/server/public/assets/{chunk-AA7GKIK3-C9V31L2z.js → chunk-AA7GKIK3-BGoztgLL.js} +1 -1
- package/payload/server/public/assets/{chunk-BSJP7CBP-DSwnSGe3.js → chunk-BSJP7CBP-C5GDAZTl.js} +1 -1
- package/payload/server/public/assets/{chunk-CIAEETIT-CnRUTuta.js → chunk-CIAEETIT-ChY32VPS.js} +1 -1
- package/payload/server/public/assets/{chunk-EDXVE4YY-BexiL2LH.js → chunk-EDXVE4YY-CPSD3p6p.js} +1 -1
- package/payload/server/public/assets/{chunk-ENJZ2VHE-aTklWdJQ.js → chunk-ENJZ2VHE-D1oL-iUr.js} +1 -1
- package/payload/server/public/assets/{chunk-FMBD7UC4-svcEBQzs.js → chunk-FMBD7UC4-CPvYEr54.js} +1 -1
- package/payload/server/public/assets/{chunk-FOC6F5B3-CG7R7Xri.js → chunk-FOC6F5B3-9Yyj7Vbz.js} +1 -1
- package/payload/server/public/assets/{chunk-ICPOFSXX-CGN7Z4x9.js → chunk-ICPOFSXX-BaYgh8FR.js} +2 -2
- package/payload/server/public/assets/{chunk-K5T4RW27-CWBh78jw.js → chunk-K5T4RW27-5ucpeRvm.js} +1 -1
- package/payload/server/public/assets/{chunk-KGLVRYIC-Ctiq4q-5.js → chunk-KGLVRYIC-DW-FHk7T.js} +1 -1
- package/payload/server/public/assets/{chunk-LIHQZDEY-BII77Q7K.js → chunk-LIHQZDEY-CGGVfAyG.js} +1 -1
- package/payload/server/public/assets/{chunk-ORNJ4GCN-DT7R01H8.js → chunk-ORNJ4GCN-Bv8EUa5d.js} +1 -1
- package/payload/server/public/assets/{chunk-OYMX7WX6-y-Jkbd4C.js → chunk-OYMX7WX6-C1mXJnyw.js} +1 -1
- package/payload/server/public/assets/chunk-QZHKN3VN-CAwbQBtf.js +1 -0
- package/payload/server/public/assets/{chunk-U2HBQHQK-DzFo6KbQ.js → chunk-U2HBQHQK-PwSQz6EP.js} +1 -1
- package/payload/server/public/assets/{chunk-X2U36JSP-CYSwgtLV.js → chunk-X2U36JSP--NPHTBlU.js} +1 -1
- package/payload/server/public/assets/{chunk-XPW4576I-DHlM4tOn.js → chunk-XPW4576I-Bbc7Da32.js} +1 -1
- package/payload/server/public/assets/{chunk-YZCP3GAM-B20rTmLl.js → chunk-YZCP3GAM-D-9v8e--.js} +1 -1
- package/payload/server/public/assets/{chunk-ZZ45TVLE-C7hP4J8m.js → chunk-ZZ45TVLE-CMPg670Q.js} +1 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-Ct6Rn0vt.js +1 -0
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-B_2XCIFv.js +1 -0
- package/payload/server/public/assets/clone-DjPWA_NH.js +1 -0
- package/payload/server/public/assets/{cose-bilkent-S5V4N54A-nKjTv9sR.js → cose-bilkent-S5V4N54A-NVSb1Bhy.js} +1 -1
- package/payload/server/public/assets/{dagre-KV5264BT-BNJYTnls.js → dagre-KV5264BT-V2Ai1DFl.js} +1 -1
- package/payload/server/public/assets/{dagre-Cx8LiiUZ.js → dagre-QfLcO-1j.js} +1 -1
- package/payload/server/public/assets/data-BKE6Sa-g.js +1 -0
- package/payload/server/public/assets/{diagram-5BDNPKRD-CoAN5_f9.js → diagram-5BDNPKRD-Caw21693.js} +1 -1
- package/payload/server/public/assets/{diagram-G4DWMVQ6-pRmsN4_x.js → diagram-G4DWMVQ6-CgwVkT2q.js} +1 -1
- package/payload/server/public/assets/{diagram-MMDJMWI5-CRoylyp4.js → diagram-MMDJMWI5-DFUrLZhs.js} +1 -1
- package/payload/server/public/assets/{diagram-TYMM5635-BT3ZtG5b.js → diagram-TYMM5635-CEEiEuZJ.js} +1 -1
- package/payload/server/public/assets/{erDiagram-SMLLAGMA-DSKws1ZV.js → erDiagram-SMLLAGMA-CG10NrT3.js} +1 -1
- package/payload/server/public/assets/file-download-B2B2DTar.js +1 -0
- package/payload/server/public/assets/{flatten-jn8bZgcI.js → flatten-DAO0sfCM.js} +1 -1
- package/payload/server/public/assets/{flowDiagram-DWJPFMVM-C2L38ZE5.js → flowDiagram-DWJPFMVM-BqkV9Okv.js} +1 -1
- package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-Bm2h-vjm.js → ganttDiagram-T4ZO3ILL-DRiCDQF6.js} +1 -1
- package/payload/server/public/assets/gitGraph-7Q5UKJZL-B8Hx7f-D.js +1 -0
- package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-BoowbF7u.js → gitGraphDiagram-UUTBAWPF-BZV2voyy.js} +1 -1
- package/payload/server/public/assets/graph-DRNzDBTV.js +51 -0
- package/payload/server/public/assets/{graph-labels-BLx-0vyT.js → graph-labels-DxqDdO7G.js} +1 -1
- package/payload/server/public/assets/{graphlib-BvT2s_o1.js → graphlib-DGPThX8M.js} +1 -1
- package/payload/server/public/assets/info-OMHHGYJF-BicCJTqU.js +1 -0
- package/payload/server/public/assets/infoDiagram-42DDH7IO-tf-8rOez.js +2 -0
- package/payload/server/public/assets/{isEmpty-Dkk6_L-B.js → isEmpty-Cb_U81C6.js} +1 -1
- package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-nxcmj-IX.js → ishikawaDiagram-UXIWVN3A-BKsAxSvp.js} +1 -1
- package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-q5SNIvmS.js → journeyDiagram-VCZTEJTY-CmliuOd-.js} +1 -1
- package/payload/server/public/assets/{kanban-definition-6JOO6SKY-BugG9Q1o.js → kanban-definition-6JOO6SKY-DAlElMb-.js} +1 -1
- package/payload/server/public/assets/{line-MZCQLR-q.js → line-DKZqLKAC.js} +1 -1
- package/payload/server/public/assets/{linear-DmQGNk-g.js → linear-CbSGdh1x.js} +1 -1
- package/payload/server/public/assets/{maximize-2-CnNQnj_v.js → maximize-2-DKx6OBKv.js} +1 -1
- package/payload/server/public/assets/{mermaid-parser.core-DvoeqLxw.js → mermaid-parser.core-DFvbao9C.js} +2 -2
- package/payload/server/public/assets/{mermaid.core-BLaw7iG4.js → mermaid.core-C0vEBxDm.js} +3 -3
- package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-DMkt10Vc.js → mindmap-definition-QFDTVHPH-CShASUKd.js} +1 -1
- package/payload/server/public/assets/operator-CTJjI8te.js +1 -0
- package/payload/server/public/assets/{ordinal-DuJYM7p1.js → ordinal-H0tVmBDP.js} +1 -1
- package/payload/server/public/assets/packet-4T2RLAQJ-Ckn_5hq0.js +1 -0
- package/payload/server/public/assets/page-CbI1wtef.js +1 -0
- package/payload/server/public/assets/page-JzxyFGle.js +32 -0
- package/payload/server/public/assets/{pdf-render-riHufgrn.js → pdf-render-jAQU0nXt.js} +1 -1
- package/payload/server/public/assets/pie-ZZUOXDRM-DzlGQW3J.js +1 -0
- package/payload/server/public/assets/{pieDiagram-DEJITSTG-DRp4lD2B.js → pieDiagram-DEJITSTG-CBZHpDAJ.js} +1 -1
- package/payload/server/public/assets/{play-yEkd7ouW.js → play-DHttF4fl.js} +1 -1
- package/payload/server/public/assets/public-DzeW3_eM.js +1 -0
- package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-DkT63Dn4.js → quadrantDiagram-34T5L4WZ-n3bB3K91.js} +1 -1
- package/payload/server/public/assets/radar-PYXPWWZC-BHo2YD8b.js +1 -0
- package/payload/server/public/assets/{reduce-COyotL2V.js → reduce-DAvwat-a.js} +1 -1
- package/payload/server/public/assets/{requirementDiagram-MS252O5E-65GGtDkc.js → requirementDiagram-MS252O5E-DiDvQK3c.js} +1 -1
- package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-D1vRwLWd.js → sankeyDiagram-XADWPNL6-DWDQQZmd.js} +1 -1
- package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-Bp47ZSN2.js → sequenceDiagram-FGHM5R23-DmAqRlBT.js} +1 -1
- package/payload/server/public/assets/square-CHcQHtK8.js +1 -0
- package/payload/server/public/assets/{stateDiagram-FHFEXIEX-BZHHcP_e.js → stateDiagram-FHFEXIEX-D4R-n6bZ.js} +1 -1
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-BcFRbGRu.js +1 -0
- package/payload/server/public/assets/tasks-ClfJU1CK.js +1 -0
- package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-DZ1wUyj-.js → timeline-definition-GMOUNBTQ-CjiHGP4l.js} +1 -1
- package/payload/server/public/assets/treeView-SZITEDCU-BtRr5vXp.js +1 -0
- package/payload/server/public/assets/treemap-W4RFUUIX-D8p0sbyo.js +1 -0
- package/payload/server/public/assets/useCopyFeedback-CPEOUSGU.js +1 -0
- package/payload/server/public/assets/{useSubAccountSwitcher-ayajZz8T.css → useSubAccountSwitcher-CdS1CorH.css} +1 -1
- package/payload/server/public/assets/{vennDiagram-DHZGUBPP-CttBahsn.js → vennDiagram-DHZGUBPP-CBdDQ7c-.js} +1 -1
- package/payload/server/public/assets/wardley-RL74JXVD-DgMn-9ZJ.js +1 -0
- package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-Y5tWieZg.js → wardleyDiagram-NUSXRM2D-BlIQPKLb.js} +1 -1
- package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-C78A9qbe.js → xychartDiagram-5P7HB3ND-CslKgh9B.js} +1 -1
- package/payload/server/public/browser.html +6 -6
- package/payload/server/public/calendar.html +7 -7
- package/payload/server/public/chat.html +13 -14
- package/payload/server/public/data.html +10 -10
- package/payload/server/public/graph.html +11 -11
- package/payload/server/public/index.html +14 -15
- package/payload/server/public/operator.html +15 -16
- package/payload/server/public/public.html +13 -14
- package/payload/server/public/tasks.html +5 -5
- package/payload/server/server.js +228 -9
- package/payload/server/public/assets/AdminLoginScreens-yAIhWcGc.js +0 -1
- package/payload/server/public/assets/AdminShell-Bk1AFQ4D.js +0 -2
- package/payload/server/public/assets/Transcript-D67OZnVM.js +0 -2
- package/payload/server/public/assets/admin-DuOhkvrZ.js +0 -1
- package/payload/server/public/assets/admin-types-ClHBm0ni.js +0 -1
- package/payload/server/public/assets/architecture-YZFGNWBL-CgeA3HPD.js +0 -1
- package/payload/server/public/assets/calendar-CNk1iPrA.js +0 -1
- package/payload/server/public/assets/channel-B04VBWf7.js +0 -1
- package/payload/server/public/assets/chat-7qB-dwBq.js +0 -1
- package/payload/server/public/assets/chevron-left-CpcDbHIz.js +0 -1
- package/payload/server/public/assets/chunk-426QAEUC-DZkHcXRA.js +0 -1
- package/payload/server/public/assets/chunk-QZHKN3VN-D9Mwgzti.js +0 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-CIo_TzfF.js +0 -1
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-SzQYvAUZ.js +0 -1
- package/payload/server/public/assets/clone-BVjcjsBA.js +0 -1
- package/payload/server/public/assets/data-BnHcS27X.js +0 -1
- package/payload/server/public/assets/file-download-Hf_UILaA.js +0 -1
- package/payload/server/public/assets/gitGraph-7Q5UKJZL-DzQ18i0T.js +0 -1
- package/payload/server/public/assets/graph-klLugHEK.js +0 -51
- package/payload/server/public/assets/info-OMHHGYJF-CwsoaVzV.js +0 -1
- package/payload/server/public/assets/infoDiagram-42DDH7IO-ieNsH2bV.js +0 -2
- package/payload/server/public/assets/operator-CRcySYv9.js +0 -1
- package/payload/server/public/assets/packet-4T2RLAQJ-DxB-bO4L.js +0 -1
- package/payload/server/public/assets/page-CVo3CWIY.js +0 -1
- package/payload/server/public/assets/page-DPTbZRwl.js +0 -32
- package/payload/server/public/assets/pie-ZZUOXDRM-PSdKvB8-.js +0 -1
- package/payload/server/public/assets/public-Fi6ccYaE.js +0 -1
- package/payload/server/public/assets/radar-PYXPWWZC-D8FqRiWq.js +0 -1
- package/payload/server/public/assets/rotate-ccw-xHksz9mr.js +0 -1
- package/payload/server/public/assets/square-BZqaxg7D.js +0 -1
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-qVtoGx56.js +0 -1
- package/payload/server/public/assets/tasks-COWmfts8.js +0 -1
- package/payload/server/public/assets/treeView-SZITEDCU-i-w4rJmQ.js +0 -1
- package/payload/server/public/assets/treemap-W4RFUUIX-Dij6AlAj.js +0 -1
- package/payload/server/public/assets/wardley-RL74JXVD-BJhOQVqW.js +0 -1
- /package/payload/server/public/assets/{_baseFor-Bjj9It3Q.js → _baseFor-D3BqAOBr.js} +0 -0
- /package/payload/server/public/assets/{array-BrrxEo12.js → array-iYP9fzJp.js} +0 -0
- /package/payload/server/public/assets/{cytoscape.esm-CTWJxUxo.js → cytoscape.esm-BQSBzPsB.js} +0 -0
- /package/payload/server/public/assets/{defaultLocale-CFDbRV4k.js → defaultLocale-BrdA1gDi.js} +0 -0
- /package/payload/server/public/assets/{dist-CRpLAnCP.js → dist-Cvgxm7qv.js} +0 -0
- /package/payload/server/public/assets/{init-dXCzGgLP.js → init-Bx0S6pkp.js} +0 -0
- /package/payload/server/public/assets/{katex-CHA1JFYJ.js → katex-BsiH_a6d.js} +0 -0
- /package/payload/server/public/assets/{path-CCRpdXhK.js → path-BLSgTy3w.js} +0 -0
- /package/payload/server/public/assets/{preload-helper-CH6UZRzu.js → preload-helper-ClBMtM1g.js} +0 -0
- /package/payload/server/public/assets/{rough.esm-Bud4g4fh.js → rough.esm-BMOtDaaS.js} +0 -0
- /package/payload/server/public/assets/{src-xN1oULc8.js → src-DV4LKv2E.js} +0 -0
- /package/payload/server/public/assets/{useSubAccountSwitcher-DADvGnr1.js → useSubAccountSwitcher-DyjTbzQR.js} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
export declare const EMBED_INPUT_MAX_CHARS
|
|
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":"
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
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
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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 <=
|
|
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 +=
|
|
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 +
|
|
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":";AAAA,yEAAyE;AACzE,6EAA6E;AAC7E,2EAA2E;AAC3E,6EAA6E;AAC7E,+EAA+E;AAC/E,gDAAgD;;;
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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(
|
|
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
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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 <=
|
|
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 +=
|
|
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 +
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/embed-throughput.test.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"embed-throughput.test.d.ts","sourceRoot":"","sources":["../../../src/tools/__tests__/embed-throughput.test.ts"],"names":[],"mappings":""}
|