@bendyline/gezel-service 1.1.1 → 1.1.2

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 (185) hide show
  1. package/dist/NOTICE.md +3 -3
  2. package/dist/bin/gezeld.js +8471 -5311
  3. package/dist/handboek-content/technical/building-connected-apps-with-gezel-app-sdk.md +78 -0
  4. package/dist/handboek-content/whats-new/{1.26250.md → 1.26251.md} +5 -5
  5. package/dist/index-store/static-index-worker.js +160 -51
  6. package/dist/index.d.ts +416 -71
  7. package/dist/index.js +8465 -5305
  8. package/dist/memory/embed-worker.js +132 -4
  9. package/dist/memory/image-embed-worker.js +14 -5
  10. package/dist/providers/mlx/python/cache_persist.py +52 -8
  11. package/dist/providers/mlx/python/gezel_mlx_server.py +44 -1
  12. package/dist/providers/mlx/python/qwen4_ple.py +159 -0
  13. package/dist/providers/mlx/python/spec_decode.py +73 -31
  14. package/dist/providers/mlx/python/tool_call_stream.py +10 -2
  15. package/dist/providers/mlx/python/tool_grammar.py +16 -0
  16. package/dist/ui/assets/{AmbientDashboardCard-DcVHxEdC.js → AmbientDashboardCard-_xnAQGnk.js} +1 -1
  17. package/dist/ui/assets/{AudioEngineSettings-BFM2S1mm.js → AudioEngineSettings-CgXgbX0i.js} +1 -1
  18. package/dist/ui/assets/{BenchmarksView-84-YeULA.js → BenchmarksView-BlExZpYA.js} +1 -1
  19. package/dist/ui/assets/CatalogBrowser-DYXUZQ_t.js +1 -0
  20. package/dist/ui/assets/{ChannelsSettings-7NAVUk4k.js → ChannelsSettings-EH27HcqZ.js} +1 -1
  21. package/dist/ui/assets/ChatTimelineView-Dn1TNyVn.js +7 -0
  22. package/dist/ui/assets/{ConnectedAppsPanel-CIgF4Q3R.js → ConnectedAppsPanel-BROry-Y0.js} +1 -1
  23. package/dist/ui/assets/CraftbookEditor-B2RNJnFf.js +1 -0
  24. package/dist/ui/assets/{CraftbookScriptEditorView-Ck5tzj3Y.js → CraftbookScriptEditorView-B-vySmHP.js} +1 -1
  25. package/dist/ui/assets/{CraftbookTabContent-BWg7FhK_.js → CraftbookTabContent-BW_2KC89.js} +1 -1
  26. package/dist/ui/assets/{CraftbookToolsetSetup-EuFOiUzR.js → CraftbookToolsetSetup-J7rEa835.js} +1 -1
  27. package/dist/ui/assets/{CraftbooksView-BIzQ1VVn.js → CraftbooksView-BjBUWPGF.js} +1 -1
  28. package/dist/ui/assets/DiffpackReviewView-CD4qJmgT.js +1 -0
  29. package/dist/ui/assets/DocumentDetail-SYozruTJ.js +1 -0
  30. package/dist/ui/assets/DocumentsView-DE7eL49Z.js +1 -0
  31. package/dist/ui/assets/{Ds4Settings-9soZCSeO.js → Ds4Settings-DGzc7JcD.js} +1 -1
  32. package/dist/ui/assets/{EmbeddedChat-Cce7nxXT.js → EmbeddedChat-D92TaJQ8.js} +1 -1
  33. package/dist/ui/assets/{FaceRecognitionCard-BVZyOwB8.js → FaceRecognitionCard-CB1Dt9X4.js} +1 -1
  34. package/dist/ui/assets/FileMapView-kVBG22FU.js +1 -0
  35. package/dist/ui/assets/FoldersSettings-BWFQU5B4.js +1 -0
  36. package/dist/ui/assets/GezelDetail-Br4v42Nv.js +1 -0
  37. package/dist/ui/assets/{GezelJsonEditor-DXXt165-.js → GezelJsonEditor-B9XEiW5Y.js} +1 -1
  38. package/dist/ui/assets/GezellenView-DheEBAy4.js +1 -0
  39. package/dist/ui/assets/{GildeUpdatesCard-CtY8mUTA.js → GildeUpdatesCard-BeIYv09f.js} +1 -1
  40. package/dist/ui/assets/{GithubSignInChip-DT3l6E0Q.js → GithubSignInChip-eEGCe9ar.js} +1 -1
  41. package/dist/ui/assets/{HandboekMediaProvider-ByJ4NAzz.js → HandboekMediaProvider-D0zcbbpf.js} +1 -1
  42. package/dist/ui/assets/HandboekView-p0BZT1rH.js +1 -0
  43. package/dist/ui/assets/{HistoryView-CkinzFEC.js → HistoryView-CKWcWW-Y.js} +1 -1
  44. package/dist/ui/assets/{HomeView-DNBrQnwy.js → HomeView-b_TT5oV3.js} +1 -1
  45. package/dist/ui/assets/{ImageEngineSettings-BfM9HSd_.js → ImageEngineSettings-CJfk4OJ4.js} +1 -1
  46. package/dist/ui/assets/{ImageRecognitionSettings-CaSQu5Br.js → ImageRecognitionSettings-DzbvZk5S.js} +1 -1
  47. package/dist/ui/assets/{IncompleteDownloads-C36lTCMG.js → IncompleteDownloads-C8_XyP8O.js} +1 -1
  48. package/dist/ui/assets/{KnowledgeCatalogsCard-CK9bqdf6.js → KnowledgeCatalogsCard-CHopG_j-.js} +1 -1
  49. package/dist/ui/assets/KnowledgeView-CXNKZUJq.js +1 -0
  50. package/dist/ui/assets/{LlamaCppSettings-CdNMHBbp.js → LlamaCppSettings-ohcM7kes.js} +1 -1
  51. package/dist/ui/assets/{MarkdownField-Dugmfr_e.js → MarkdownField-BX0qsaFq.js} +1 -1
  52. package/dist/ui/assets/{MemoriesTree-CgFqUNEr.js → MemoriesTree-BPj5Rod8.js} +1 -1
  53. package/dist/ui/assets/{MicrophoneSettings-DzDe9y1q.js → MicrophoneSettings-CWjeYnfI.js} +1 -1
  54. package/dist/ui/assets/MlxSettings-Dq6dRpcl.js +1 -0
  55. package/dist/ui/assets/{ModelPicker-BVkJhNAH.js → ModelPicker-0Ue-eEKj.js} +1 -1
  56. package/dist/ui/assets/NewPathDialog-BhAZm9dp.js +1 -0
  57. package/dist/ui/assets/{NewScriptDialog-TEAd0KLl.js → NewScriptDialog-DuryIe50.js} +1 -1
  58. package/dist/ui/assets/NewTaskDialog-BX24flMY.js +2 -0
  59. package/dist/ui/assets/{OllamaSettings-CPy2vxyZ.js → OllamaSettings-Boj8Nd1G.js} +1 -1
  60. package/dist/ui/assets/ProjectChat-Dhw_M5bq.js +5 -0
  61. package/dist/ui/assets/ProjectConnectionsTab-BwpMQNrg.js +1 -0
  62. package/dist/ui/assets/{ProjectGitStatusBar-BgHZ2ijN.js → ProjectGitStatusBar-BORGYx5l.js} +1 -1
  63. package/dist/ui/assets/{ProjectGithubView-BV2uTNU0.js → ProjectGithubView-CTP1BOMv.js} +1 -1
  64. package/dist/ui/assets/{ProjectMailTab-DLtJdXSL.js → ProjectMailTab-qFmAeV3M.js} +1 -1
  65. package/dist/ui/assets/{ProjectOverviewView-ZMs9nL5N.js → ProjectOverviewView-Cq1FsoaT.js} +1 -1
  66. package/dist/ui/assets/{ProjectTimeline-CTPcn4Kc.js → ProjectTimeline-Cqae2Z92.js} +1 -1
  67. package/dist/ui/assets/ProjectsView-D0uJjUB-.js +7 -0
  68. package/dist/ui/assets/{RecommendedBadge-iUoisQBi.js → RecommendedBadge-CN1jEmSn.js} +1 -1
  69. package/dist/ui/assets/{RemoteServersPanel-CTVJ0IRh.js → RemoteServersPanel-CMakvCCM.js} +1 -1
  70. package/dist/ui/assets/{ScriptEditorTabs-CCDFZjdC.js → ScriptEditorTabs-st6dJePd.js} +2 -2
  71. package/dist/ui/assets/{ScriptEditorView-CergLqKj.js → ScriptEditorView-BkySh57_.js} +2 -2
  72. package/dist/ui/assets/{ScriptsView-v7ckdhDi.js → ScriptsView-7uPKwjDB.js} +1 -1
  73. package/dist/ui/assets/{SecurityComplianceSettings-Bd0-66Gs.js → SecurityComplianceSettings-Tsv_pXfy.js} +1 -1
  74. package/dist/ui/assets/SessionSwitcher-D0fSeeHO.js +1 -0
  75. package/dist/ui/assets/SettingsView-cli_z8ZW.js +3 -0
  76. package/dist/ui/assets/{SquisqIntegration-C5l_OPvD.js → SquisqIntegration-BTZ1kBlg.js} +1 -1
  77. package/dist/ui/assets/{StorageCleanupDialog-3kv9-2mT.js → StorageCleanupDialog-CDBjl8JQ.js} +1 -1
  78. package/dist/ui/assets/{StorageUsageCard-7sWTrZFa.js → StorageUsageCard-DXY1j0dZ.js} +1 -1
  79. package/dist/ui/assets/TaskTabContent-C0P681SL.js +1 -0
  80. package/dist/ui/assets/TasksView-Ddwz10ri.js +1 -0
  81. package/dist/ui/assets/{TerminalCodeEditor-ChmdzmCz.js → TerminalCodeEditor-BkNQ6QoQ.js} +2 -2
  82. package/dist/ui/assets/ToolsetsEditor-KVigemfL.js +8 -0
  83. package/dist/ui/assets/{TransformToolbarButton-gzIxUBH7.js → TransformToolbarButton-8wfJR1bO.js} +2 -2
  84. package/dist/ui/assets/{UnrecognizedModels-0hptopv6.js → UnrecognizedModels-BzdU1clO.js} +1 -1
  85. package/dist/ui/assets/{VideoEngineSettings-BdNzCEdR.js → VideoEngineSettings-f79ff57u.js} +1 -1
  86. package/dist/ui/assets/{WorkspaceIndexPane-DnsOO916.js → WorkspaceIndexPane-fu5V6o1W.js} +1 -1
  87. package/dist/ui/assets/{api-C0I7spBl.js → api-CmrgM5Rf.js} +1 -1
  88. package/dist/ui/assets/chat-thread-memory-8MCtb17Q.js +1 -0
  89. package/dist/ui/assets/{chunk-6ECMNUIP-BvCy0HUI.js → chunk-6ECMNUIP-D5AA6ozp.js} +1 -1
  90. package/dist/ui/assets/{chunk-A6N6IN3I-ofeM4l8g.js → chunk-A6N6IN3I-BpxihS5H.js} +1 -1
  91. package/dist/ui/assets/{chunk-ATWVNF7D-CYuW1Yc6.js → chunk-ATWVNF7D-CKIWh7bN.js} +1 -1
  92. package/dist/ui/assets/{chunk-UBFT2CDQ-FexGDigd.js → chunk-CW24CH7I-BD0FIooX.js} +1 -1
  93. package/dist/ui/assets/{chunk-DXYWKZ52-zxe8pjH4.js → chunk-DXYWKZ52-DMKPZnpo.js} +1 -1
  94. package/dist/ui/assets/chunk-IIQYS2YH-BtDJt4Xr.js +1 -0
  95. package/dist/ui/assets/{chunk-KGWT454C-v5-l6U5m.js → chunk-KGWT454C-DsQOBuLg.js} +1 -1
  96. package/dist/ui/assets/{chunk-Q77KNJIN-BLuPdsQF.js → chunk-Q77KNJIN-BdWizq9s.js} +1 -1
  97. package/dist/ui/assets/{chunk-USU6HTKB-B_p-YcDp.js → chunk-USU6HTKB-DyGZplh4.js} +1 -1
  98. package/dist/ui/assets/{chunk-UXEANVBI-Bt0kX-0Z.js → chunk-UXEANVBI-BUBXvqyE.js} +1 -1
  99. package/dist/ui/assets/{chunk-O7JILDEF-CqcO1qaI.js → chunk-X7BNDUNI-D14WIvlD.js} +2 -2
  100. package/dist/ui/assets/{chunk-AE3IUKMM-mZjzzO8Z.js → chunk-ZSU5DJLX-CrjQf-vs.js} +1 -1
  101. package/dist/ui/assets/{container-BI8hxN1N.js → container-iFj1r6Hk.js} +1 -1
  102. package/dist/ui/assets/csv-Di411Mv-.js +1 -0
  103. package/dist/ui/assets/data-Q05qqAp4.js +1 -0
  104. package/dist/ui/assets/{dist-APSIK7H3.js → dist-1FNFiksd.js} +3 -3
  105. package/dist/ui/assets/dist-BMZhqfv7.js +2 -0
  106. package/dist/ui/assets/dist-CE5CbmDb.js +225 -0
  107. package/dist/ui/assets/doc-BHcGsJZd.js +587 -0
  108. package/dist/ui/assets/{document-import-DPK4r8Am.js → document-import-B2Ya6sbf.js} +1 -1
  109. package/dist/ui/assets/{docx-DRLuCTaC.js → docx-B8Pwz5Ih.js} +1 -1
  110. package/dist/ui/assets/{epub-a0p2Xb1b.js → epub-BQ4g8Q9j.js} +1 -1
  111. package/dist/ui/assets/{export-options-BqYFyzWx.js → export-options-Dylv69b3.js} +1 -1
  112. package/dist/ui/assets/file-browser-BLOPfnS0.js +5 -0
  113. package/dist/ui/assets/{html-BbQNmJ5X.js → html-BnA7qFwq.js} +1 -1
  114. package/dist/ui/assets/icons-CyM7Cg4E.js +1 -0
  115. package/dist/ui/assets/index-DW6qHqhU.css +1 -0
  116. package/dist/ui/assets/index-DivKnJyx.js +36 -0
  117. package/dist/ui/assets/{mail-link-CWfsP5Aq.js → mail-link-NtlcX5OE.js} +1 -1
  118. package/dist/ui/assets/{markdown-ryKA_OmY.js → markdown-CLKH_zzl.js} +1 -1
  119. package/dist/ui/assets/markdown-baseline-0dqv9qbV.js +1 -0
  120. package/dist/ui/assets/materialize-G4WC5UG3-xq2dcwS8.js +1 -0
  121. package/dist/ui/assets/{monaco-base-C0VTMpv-.js → monaco-base-BSvdAweA.js} +1 -1
  122. package/dist/ui/assets/{monaco-setup-CX8q88c5.js → monaco-setup-CT7LZ0o_.js} +1 -1
  123. package/dist/ui/assets/{new-project-meta-ZmDPGYzC.js → new-project-meta-5Y9Blozr.js} +1 -1
  124. package/dist/ui/assets/outside-in-Msmhxf_U.js +3 -0
  125. package/dist/ui/assets/{pdf-BHrygJa5.js → pdf-C4BhW3zg.js} +1 -1
  126. package/dist/ui/assets/{pending-drafts-27ujtQxX.js → pending-drafts-BD227X9H.js} +1 -1
  127. package/dist/ui/assets/{poppetje-CfaAvFkE.js → poppetje-DvCJO36y.js} +3 -3
  128. package/dist/ui/assets/{pptx-nDW64jAN.js → pptx-B6uaM2s7.js} +3 -3
  129. package/dist/ui/assets/{run-export-86KzLZ7R.js → run-export-TscWwR6y.js} +2 -2
  130. package/dist/ui/assets/{standalone-source-7QdiNsDW.js → standalone-source-Cw9SF7Zk.js} +1 -1
  131. package/dist/ui/assets/storage-Cmhlwx70.js +1 -0
  132. package/dist/ui/assets/styles-BjDaWeMA.css +1 -0
  133. package/dist/ui/assets/styles-CqO-rmOt.js +554 -0
  134. package/dist/ui/assets/{terminal-monaco-setup-Bar-sYKd.js → terminal-monaco-setup-me523-j-.js} +1 -1
  135. package/dist/ui/assets/{transform-CmAXy0Ck.js → transform-DxJCwK68.js} +1 -1
  136. package/dist/ui/assets/{use-local-model-manager-D1yiurrC.js → use-local-model-manager-8wvk-I-y.js} +1 -1
  137. package/dist/ui/assets/{useCompactLayout-BDtPQcDh.js → useCompactLayout-cm4E9Vja.js} +0 -0
  138. package/dist/ui/assets/{useCopilotAvailability-BHOfHrfT.js → useCopilotAvailability-D9hglSH_.js} +1 -1
  139. package/dist/ui/assets/{useDiffpacks-B0kYhlWQ.js → useDiffpacks-1BN2XJWg.js} +1 -1
  140. package/dist/ui/assets/{useGitSync-DOt-ODiw.js → useGitSync-7m9YA7h5.js} +1 -1
  141. package/dist/ui/assets/{useShowWorkInProgressFeatures-BtpredG7.js → useShowWorkInProgressFeatures-D6QOaTUT.js} +1 -1
  142. package/dist/ui/assets/{useTotalRamBytes-B8NFM27w.js → useTotalRamBytes-Durk383U.js} +1 -1
  143. package/dist/ui/assets/{xlsx-B_pu1ZvY.js → xlsx-BbHA9bxA.js} +1 -1
  144. package/dist/ui/index.html +15 -15
  145. package/package.json +12 -11
  146. package/dist/ui/assets/CatalogBrowser-7_jHy5Q3.js +0 -1
  147. package/dist/ui/assets/ChatTimelineView-Cg_kLK_U.js +0 -7
  148. package/dist/ui/assets/CraftbookEditor-BKAIsnXv.js +0 -1
  149. package/dist/ui/assets/DiffpackReviewView-B2a3zgE-.js +0 -1
  150. package/dist/ui/assets/DocumentDetail-GCYhwQqV.js +0 -1
  151. package/dist/ui/assets/DocumentsView-Cm3RaQ5N.js +0 -1
  152. package/dist/ui/assets/FileMapView-BKJovLcL.js +0 -1
  153. package/dist/ui/assets/FoldersSettings-CuJd2JdE.js +0 -1
  154. package/dist/ui/assets/GezelDetail-D_KIPDhQ.js +0 -1
  155. package/dist/ui/assets/GezellenView-BOOW1QQU.js +0 -1
  156. package/dist/ui/assets/HandboekView-gRzJcIbJ.js +0 -1
  157. package/dist/ui/assets/KnowledgeView-DJ3cJVHA.js +0 -1
  158. package/dist/ui/assets/MlxSettings-45mBFHbA.js +0 -1
  159. package/dist/ui/assets/NewTaskDialog-CIXVunZn.js +0 -2
  160. package/dist/ui/assets/ProjectChat-BiJRweob.js +0 -5
  161. package/dist/ui/assets/ProjectConnectionsTab-CQgaIzuv.js +0 -1
  162. package/dist/ui/assets/ProjectsView-BUdR76n6.js +0 -7
  163. package/dist/ui/assets/SessionSwitcher-BgZqyiRn.js +0 -1
  164. package/dist/ui/assets/SettingsView-DLhOYiR5.js +0 -3
  165. package/dist/ui/assets/TaskTabContent-CG2KmfwV.js +0 -1
  166. package/dist/ui/assets/TasksView-Dy5Ue4zS.js +0 -1
  167. package/dist/ui/assets/ToolsetsEditor-J9XxQ5Ms.js +0 -8
  168. package/dist/ui/assets/chat-thread-memory-CI2-giEq.js +0 -1
  169. package/dist/ui/assets/chunk-IIQYS2YH-DEkyBlHg.js +0 -1
  170. package/dist/ui/assets/csv-B0D9jJG2.js +0 -1
  171. package/dist/ui/assets/data-BIL84EaT.js +0 -1
  172. package/dist/ui/assets/dist-CJqyWK6E.js +0 -2
  173. package/dist/ui/assets/dist-DOqbRmFj.js +0 -225
  174. package/dist/ui/assets/doc-C2Ph2FWI.js +0 -584
  175. package/dist/ui/assets/document-companion-aDThLOwl.js +0 -1
  176. package/dist/ui/assets/file-browser-DVyWN0LD.js +0 -5
  177. package/dist/ui/assets/icons-DWaUNQbT.js +0 -1
  178. package/dist/ui/assets/index-DDu8nyO6.js +0 -34
  179. package/dist/ui/assets/index-DEDUSiCx.css +0 -1
  180. package/dist/ui/assets/markdown-baseline-9B52XlX2.js +0 -1
  181. package/dist/ui/assets/materialize-G4WC5UG3-BlZQe-88.js +0 -1
  182. package/dist/ui/assets/outside-in-BmIIeMid.js +0 -3
  183. package/dist/ui/assets/storage-B-vhjyV2.js +0 -1
  184. package/dist/ui/assets/styles-BvkZd-EV.js +0 -550
  185. package/dist/ui/assets/styles-IQ4adxWr.css +0 -1
@@ -4,7 +4,7 @@
4
4
  import { parentPort } from "worker_threads";
5
5
 
6
6
  // src/memory/embed-core.ts
7
- import { createHash } from "crypto";
7
+ import { createHash as createHash2 } from "crypto";
8
8
  import { createLogger as createLogger2 } from "@bendyline/gezel";
9
9
  import {
10
10
  EmbedderUnavailableError,
@@ -15,12 +15,131 @@ import {
15
15
  } from "@bendyline/gezel-knowledge";
16
16
 
17
17
  // src/transformers-cache.ts
18
- import { mkdir } from "fs/promises";
19
- import { join } from "path";
18
+ import { createHash } from "crypto";
19
+ import { mkdir, rm, stat, writeFile } from "fs/promises";
20
+ import { dirname as dirname2, join as join2, resolve as resolve2 } from "path";
20
21
  import { createLogger } from "@bendyline/gezel";
22
+
23
+ // src/fs/safe-paths.ts
24
+ import { realpath } from "fs/promises";
25
+ import { basename, dirname, isAbsolute, join, normalize, resolve, sep } from "path";
26
+ function pathEq(a, b) {
27
+ if (process.platform === "win32" || process.platform === "darwin") {
28
+ return a.toLowerCase() === b.toLowerCase();
29
+ }
30
+ return a === b;
31
+ }
32
+ function pathStartsWithDir(candidate, base) {
33
+ if (process.platform === "win32" || process.platform === "darwin") {
34
+ return candidate.toLowerCase().startsWith(base.toLowerCase() + sep.toLowerCase());
35
+ }
36
+ return candidate.startsWith(base + sep);
37
+ }
38
+ function isPathInside(target, root) {
39
+ const resolvedTarget = resolve(target);
40
+ const resolvedRoot = resolve(root);
41
+ if (pathEq(resolvedTarget, resolvedRoot)) return true;
42
+ return pathStartsWithDir(resolvedTarget, resolvedRoot);
43
+ }
44
+
45
+ // src/transformers-cache.ts
21
46
  var log = createLogger("service");
22
47
  var HF_CACHE_DIR_ENV = "GEZEL_HF_CACHE_DIR";
23
48
  var TRANSFORMERS_MODULE = "@huggingface/transformers";
49
+ var DEFAULT_MODEL_LOCK_TIMEOUT_MS = 10 * 60 * 1e3;
50
+ var DEFAULT_MODEL_LOCK_STALE_MS = 2 * 60 * 60 * 1e3;
51
+ var DEFAULT_MODEL_LOCK_POLL_MS = 100;
52
+ function modelLockDir(cacheDir, modelId) {
53
+ const key = createHash("sha256").update(modelId, "utf8").digest("hex").slice(0, 24);
54
+ return join2(cacheDir, ".gezel-locks", `${key}.lock`);
55
+ }
56
+ function cacheModelDir(cacheDir, modelId) {
57
+ const parts = modelId.split("/");
58
+ if (parts.length === 0 || parts.some((part) => !part || part === "." || part === ".." || part.includes("\\"))) {
59
+ throw new Error(`refusing unsafe transformers model id: ${modelId}`);
60
+ }
61
+ const root = resolve2(cacheDir);
62
+ const target = resolve2(root, ...parts);
63
+ if (target === root || !isPathInside(target, root)) {
64
+ throw new Error(`refusing transformers cache path outside ${root}: ${modelId}`);
65
+ }
66
+ return target;
67
+ }
68
+ function errorCode(err) {
69
+ return err?.code;
70
+ }
71
+ function delay(ms) {
72
+ return new Promise((resolveDelay) => setTimeout(resolveDelay, ms));
73
+ }
74
+ async function staleOrMissing(lockDir, staleMs) {
75
+ try {
76
+ const info = await stat(lockDir);
77
+ return Date.now() - info.mtimeMs > staleMs;
78
+ } catch (err) {
79
+ if (errorCode(err) === "ENOENT") return true;
80
+ throw err;
81
+ }
82
+ }
83
+ async function withTransformersModelCacheLock(cacheDir, modelId, run, options = {}) {
84
+ const timeoutMs = options.timeoutMs ?? DEFAULT_MODEL_LOCK_TIMEOUT_MS;
85
+ const staleMs = options.staleMs ?? DEFAULT_MODEL_LOCK_STALE_MS;
86
+ const pollMs = options.pollMs ?? DEFAULT_MODEL_LOCK_POLL_MS;
87
+ const lockDir = modelLockDir(cacheDir, modelId);
88
+ await mkdir(dirname2(lockDir), { recursive: true });
89
+ const startedAt = Date.now();
90
+ while (true) {
91
+ try {
92
+ await mkdir(lockDir);
93
+ break;
94
+ } catch (err) {
95
+ if (errorCode(err) !== "EEXIST") throw err;
96
+ if (await staleOrMissing(lockDir, staleMs)) {
97
+ await rm(lockDir, { recursive: true, force: true });
98
+ continue;
99
+ }
100
+ if (Date.now() - startedAt >= timeoutMs) {
101
+ throw new Error(`timed out waiting for transformers cache lock for ${modelId}`);
102
+ }
103
+ await delay(pollMs);
104
+ }
105
+ }
106
+ try {
107
+ await writeFile(
108
+ join2(lockDir, "owner.json"),
109
+ `${JSON.stringify({ pid: process.pid, modelId, acquiredAt: (/* @__PURE__ */ new Date()).toISOString() })}
110
+ `
111
+ );
112
+ return await run();
113
+ } finally {
114
+ await rm(lockDir, { recursive: true, force: true });
115
+ }
116
+ }
117
+ async function removeTransformersModelCache(cacheDir, modelId) {
118
+ await rm(cacheModelDir(cacheDir, modelId), { recursive: true, force: true });
119
+ }
120
+ function isCorruptTransformersCacheFailure(error) {
121
+ const messages = [];
122
+ let current = error;
123
+ for (let depth = 0; depth < 6 && current; depth++) {
124
+ messages.push(current instanceof Error ? current.message : String(current));
125
+ current = current instanceof Error ? current.cause : void 0;
126
+ }
127
+ return /(?:protobuf parsing failed|failed to parse protobuf|invalid protobuf|unexpected end of (?:file|data)|failed to load model|load model .* failed|invalid wire type)/i.test(
128
+ messages.join(" ")
129
+ );
130
+ }
131
+ async function loadTransformersModelWithCacheRecovery(cacheDir, modelId, load, onCorrupt) {
132
+ return withTransformersModelCacheLock(cacheDir, modelId, async () => {
133
+ try {
134
+ return await load();
135
+ } catch (err) {
136
+ if (!isCorruptTransformersCacheFailure(err)) throw err;
137
+ await onCorrupt?.();
138
+ await removeTransformersModelCache(cacheDir, modelId);
139
+ return await load();
140
+ }
141
+ });
142
+ }
24
143
  function isMissingModule(err, specifier) {
25
144
  if (!(err instanceof Error)) return false;
26
145
  const { code } = err;
@@ -124,7 +243,16 @@ async function loadPipeline() {
124
243
  const { pipeline } = await import("@huggingface/transformers");
125
244
  const modelId = embedModelId();
126
245
  if (modelId !== DEFAULT_EMBED_MODEL) log2.info(`[embed] using model ${modelId}`);
127
- return await pipeline("feature-extraction", modelId, daemonLoadOptions());
246
+ const createPipeline = () => pipeline("feature-extraction", modelId, daemonLoadOptions());
247
+ if (!cacheDir) return await createPipeline();
248
+ return await loadTransformersModelWithCacheRecovery(
249
+ cacheDir,
250
+ modelId,
251
+ createPipeline,
252
+ () => {
253
+ log2.warn(`[embed] discarded corrupt cached model files for ${modelId}; retrying once`);
254
+ }
255
+ );
128
256
  } catch (err) {
129
257
  const missing = isMissingModule(err, TRANSFORMERS_MODULE);
130
258
  const message = missing ? "Local memory embeddings are an optional npm feature. Install @huggingface/transformers@^3.8.1 alongside @bendyline/gezel-service (see the service README)." : err instanceof Error ? err.message : String(err);
@@ -4,7 +4,7 @@
4
4
  import { parentPort } from "worker_threads";
5
5
 
6
6
  // src/memory/embed-core.ts
7
- import { createHash } from "crypto";
7
+ import { createHash as createHash2 } from "crypto";
8
8
  import { createLogger as createLogger2 } from "@bendyline/gezel";
9
9
  import {
10
10
  EmbedderUnavailableError,
@@ -15,12 +15,21 @@ import {
15
15
  } from "@bendyline/gezel-knowledge";
16
16
 
17
17
  // src/transformers-cache.ts
18
- import { mkdir } from "fs/promises";
19
- import { join } from "path";
18
+ import { createHash } from "crypto";
19
+ import { mkdir, rm, stat, writeFile } from "fs/promises";
20
+ import { dirname as dirname2, join as join2, resolve as resolve2 } from "path";
20
21
  import { createLogger } from "@bendyline/gezel";
22
+
23
+ // src/fs/safe-paths.ts
24
+ import { realpath } from "fs/promises";
25
+ import { basename, dirname, isAbsolute, join, normalize, resolve, sep } from "path";
26
+
27
+ // src/transformers-cache.ts
21
28
  var log = createLogger("service");
22
29
  var HF_CACHE_DIR_ENV = "GEZEL_HF_CACHE_DIR";
23
30
  var TRANSFORMERS_MODULE = "@huggingface/transformers";
31
+ var DEFAULT_MODEL_LOCK_TIMEOUT_MS = 10 * 60 * 1e3;
32
+ var DEFAULT_MODEL_LOCK_STALE_MS = 2 * 60 * 60 * 1e3;
24
33
  function isMissingModule(err, specifier) {
25
34
  if (!(err instanceof Error)) return false;
26
35
  const { code } = err;
@@ -268,7 +277,7 @@ import { open } from "fs/promises";
268
277
  import { createRequire } from "module";
269
278
 
270
279
  // src/index-store/image-meta.ts
271
- import { createHash as createHash2 } from "crypto";
280
+ import { createHash as createHash3 } from "crypto";
272
281
  import { inflateSync } from "zlib";
273
282
  function readImageMeta(buf) {
274
283
  if (buf.length < 24) return null;
@@ -329,7 +338,7 @@ function readImageStaticMeta(buf, opts) {
329
338
  const meta = {
330
339
  format: dims?.format ?? (looksLikeSvg(buf) ? "svg" : "unknown"),
331
340
  byteLength: buf.length,
332
- sha256: createHash2("sha256").update(buf).digest("hex")
341
+ sha256: createHash3("sha256").update(buf).digest("hex")
333
342
  };
334
343
  if (dims) {
335
344
  meta.width = dims.width;
@@ -29,6 +29,7 @@ from __future__ import annotations
29
29
  import json
30
30
  import os
31
31
  import time
32
+ from importlib import import_module
32
33
  from pathlib import Path
33
34
  from typing import Any, Optional
34
35
 
@@ -41,8 +42,16 @@ from mlx.utils import tree_flatten, tree_unflatten
41
42
  # or some operator has both installed and a model uses one set vs the
42
43
  # other. We resolve lazily on each load so an import failure here
43
44
  # can't take down the server module at import time.
44
- def _resolve_cache_class(name: str) -> Optional[type]:
45
+ def _resolve_cache_class(name: str, module_name: Optional[str] = None) -> Optional[type]:
45
46
  candidates: list[Any] = []
47
+ # Custom caches such as Qwen4's QSAKVCache live in the model's language
48
+ # module, not either package's shared cache module. Persist the originating
49
+ # module and import it only inside the two trusted MLX package namespaces.
50
+ if module_name and module_name.startswith(("mlx_vlm.models.", "mlx_lm.models.")):
51
+ try:
52
+ candidates.append(import_module(module_name))
53
+ except Exception: # noqa: BLE001 — fall through to legacy lookup
54
+ pass
46
55
  try:
47
56
  from mlx_vlm.models import cache as vlm_cache # type: ignore[attr-defined]
48
57
 
@@ -64,8 +73,24 @@ def _resolve_cache_class(name: str) -> Optional[type]:
64
73
 
65
74
  # Schema version baked into sidecar metadata. Bump when the on-disk
66
75
  # layout changes incompatibly so older entries are rejected on load
67
- # instead of silently corrupting reconstruction.
68
- SCHEMA_VERSION = "1"
76
+ # instead of silently corrupting reconstruction. Version 2 stores scalar
77
+ # cache-state leaves in metadata: Qwen4's QSA cache carries an integer block
78
+ # compression ratio alongside its arrays, and passing that integer to
79
+ # ``mx.save_safetensors`` raises ``std::bad_cast``. Version 3 also records the
80
+ # defining module for model-specific caches such as QSAKVCache.
81
+ SCHEMA_VERSION = "3"
82
+
83
+
84
+ def _split_state_leaves(layer_states: Any) -> tuple[dict[str, Any], dict[str, Any]]:
85
+ """Partition cache state into safetensor arrays and JSON scalar leaves."""
86
+ arrays: dict[str, Any] = {}
87
+ scalars: dict[str, Any] = {}
88
+ for path, value in tree_flatten(layer_states):
89
+ if value is None or isinstance(value, (bool, int, float, str)):
90
+ scalars[path] = value
91
+ else:
92
+ arrays[path] = value
93
+ return arrays, scalars
69
94
 
70
95
 
71
96
  def cache_path(persist_dir: Path, model_fingerprint: str, cache_id: str) -> Path:
@@ -107,11 +132,12 @@ def save_cache(
107
132
  # default to an empty dict so reconstruction can still proceed.
108
133
  meta_state = [getattr(c, "meta_state", {}) for c in cache_layers]
109
134
  class_names = [type(c).__name__ for c in cache_layers]
135
+ class_modules = [type(c).__module__ for c in cache_layers]
110
136
  # tree_flatten turns a nested structure of arrays into a flat
111
137
  # list of (path, array) tuples — exactly what save_safetensors
112
138
  # wants. The path encoding is what tree_unflatten reads back to
113
139
  # reconstruct the structure.
114
- flat = dict(tree_flatten(layer_states))
140
+ flat, state_scalars = _split_state_leaves(layer_states)
115
141
  if not flat:
116
142
  # Nothing array-shaped to save; reconstruction would always
117
143
  # fail. Treat as a no-op rather than writing an empty file.
@@ -120,7 +146,9 @@ def save_cache(
120
146
  metadata = {
121
147
  "schema_version": SCHEMA_VERSION,
122
148
  "classes": json.dumps(class_names),
149
+ "class_modules": json.dumps(class_modules),
123
150
  "meta_state": json.dumps(meta_state, default=str),
151
+ "state_scalars": json.dumps(state_scalars),
124
152
  "token_ids": json.dumps(token_ids),
125
153
  "model_fingerprint": model_fingerprint,
126
154
  "saved_at": str(time.time()),
@@ -193,17 +221,31 @@ def load_cache(
193
221
 
194
222
  try:
195
223
  class_names = json.loads(metadata.get("classes", "[]"))
224
+ class_modules = json.loads(metadata.get("class_modules", "[]"))
196
225
  meta_state_list = json.loads(metadata.get("meta_state", "[]"))
197
226
  token_ids = json.loads(metadata.get("token_ids", "[]"))
227
+ state_scalars = json.loads(metadata.get("state_scalars", "{}"))
198
228
  except json.JSONDecodeError as exc:
199
229
  print(f"[cache] metadata parse failed for cache_id={cache_id}: {exc}", flush=True)
200
230
  return None
201
231
 
202
- if not class_names or not token_ids:
232
+ if (
233
+ not class_names
234
+ or len(class_modules) != len(class_names)
235
+ or not token_ids
236
+ or not isinstance(state_scalars, dict)
237
+ ):
203
238
  return None
204
239
 
205
240
  try:
206
- layer_states = tree_unflatten(list(flat.items()))
241
+ # Scalar leaves use the same tree paths as the mmap-backed arrays, so
242
+ # merging the two flat maps reconstructs the original cache state
243
+ # exactly. Reject an overlap rather than letting metadata replace a
244
+ # tensor silently.
245
+ if set(flat).intersection(state_scalars):
246
+ print(f"[cache] scalar/array path overlap for cache_id={cache_id}; ignoring", flush=True)
247
+ return None
248
+ layer_states = tree_unflatten([*flat.items(), *state_scalars.items()])
207
249
  except Exception as exc: # noqa: BLE001
208
250
  print(f"[cache] tree_unflatten failed for cache_id={cache_id}: {exc}", flush=True)
209
251
  return None
@@ -217,8 +259,10 @@ def load_cache(
217
259
  return None
218
260
 
219
261
  cache_layers: list[Any] = []
220
- for cls_name, meta, layer_state in zip(class_names, meta_state_list, layer_states):
221
- cls = _resolve_cache_class(cls_name)
262
+ for cls_name, cls_module, meta, layer_state in zip(
263
+ class_names, class_modules, meta_state_list, layer_states
264
+ ):
265
+ cls = _resolve_cache_class(cls_name, cls_module)
222
266
  if cls is None:
223
267
  print(
224
268
  f"[cache] unknown cache class {cls_name!r} for cache_id={cache_id}; ignoring",
@@ -80,6 +80,7 @@ import tool_call_stream # noqa: E402
80
80
  import template_stability # noqa: E402
81
81
  from lfm2_compat import ensure_lfm2_config_compat # noqa: E402
82
82
  from qwen3_5_text_compat import is_text_only_qwen3_5_checkpoint # noqa: E402
83
+ from qwen4_ple import prepare_external_ple_view # noqa: E402
83
84
 
84
85
 
85
86
  def log_contained_exception(tag: str) -> None:
@@ -266,6 +267,28 @@ def _tool_call_close_stop_ids(model_dir: str) -> "List[int]":
266
267
 
267
268
  parser = argparse.ArgumentParser(description="gezel MLX wrapper server")
268
269
  parser.add_argument("--model", required=True, help="Path to the MLX model directory")
270
+ parser.add_argument(
271
+ "--external-ple-dir",
272
+ type=str,
273
+ default=None,
274
+ help=(
275
+ "Derived model-view directory for Qwen4's disk-backed PLE table. "
276
+ "Applicable checkpoints are hard-linked into this view and the large "
277
+ "n-gram table is memory-mapped by mlx-vlm 0.7.1. Other model families "
278
+ "keep the source directory; an eligible Qwen4 checkpoint fails safely "
279
+ "when the configured runtime cannot prepare the view."
280
+ ),
281
+ )
282
+ parser.add_argument(
283
+ "--allow-resident-ple-fallback",
284
+ action="store_true",
285
+ help=(
286
+ "Advanced/manual override: if a quantized Qwen4 checkpoint is eligible "
287
+ "for --external-ple-dir but the derived view cannot be prepared, load "
288
+ "the full PLE table resident. Gezel never passes this because its "
289
+ "catalog working-set reservation assumes external PLE is active."
290
+ ),
291
+ )
269
292
  parser.add_argument("--host", default="127.0.0.1")
270
293
  parser.add_argument("--port", type=int, default=8080)
271
294
  parser.add_argument(
@@ -508,6 +531,22 @@ _start_parent_death_watchdog()
508
531
 
509
532
  print(f"Started server process [{os.getpid()}]", flush=True)
510
533
  print("Waiting for application startup.", flush=True)
534
+ _SOURCE_MODEL_DIR = ARGS.model
535
+ ARGS.model, _ple_status, _ple_applicable = prepare_external_ple_view(
536
+ ARGS.model, ARGS.external_ple_dir
537
+ )
538
+ if ARGS.model != _SOURCE_MODEL_DIR:
539
+ print(f"[ple] external view {_ple_status}: {ARGS.model}", flush=True)
540
+ elif ARGS.external_ple_dir:
541
+ if not _ple_applicable:
542
+ print(f"[ple] not applicable: {_ple_status}", flush=True)
543
+ elif ARGS.allow_resident_ple_fallback:
544
+ print(f"[ple] resident fallback explicitly allowed: {_ple_status}", flush=True)
545
+ else:
546
+ raise RuntimeError(
547
+ "external Qwen4 PLE view is required by the planned memory footprint "
548
+ f"but could not be prepared: {_ple_status}"
549
+ )
511
550
  print(f"Loading model from: {ARGS.model}", flush=True)
512
551
  _LFM2_FF_DIM = ensure_lfm2_config_compat(ARGS.model)
513
552
  if _LFM2_FF_DIM is not None:
@@ -729,7 +768,11 @@ def _metal_recommended_working_set() -> int:
729
768
  """Metal's `max_recommended_working_set_size` (bytes), or 0 if unavailable.
730
769
  This is the OS's own soft ceiling for GPU-resident memory."""
731
770
  try:
732
- info = mx.metal.device_info()
771
+ # mlx 0.32.2 (resolved by mlx-vlm 0.7.1) deprecates the old
772
+ # mx.metal.device_info() namespace. Keep the fallback so an operator's
773
+ # older, explicitly configured mlx package still gets a useful cap.
774
+ device_info = getattr(mx, "device_info", None)
775
+ info = device_info() if callable(device_info) else mx.metal.device_info()
733
776
  return int(info.get("max_recommended_working_set_size", 0) or 0)
734
777
  except Exception: # noqa: BLE001 — API shape varies across mlx versions
735
778
  return 0
@@ -0,0 +1,159 @@
1
+ """Prepare MLX-VLM 0.7.1's disk-backed Qwen4 PLE model view.
2
+
3
+ Qwen3.8 Flash Next's hashed n-gram table is tens of GiB. Loading every row
4
+ into Metal leaves a 128 GiB Mac with very little room for KV and activations,
5
+ even though a turn touches only a tiny fraction of those rows. mlx-vlm 0.7.1
6
+ ships ``prepare_external_ple_model`` and ``QuantizedMMapNGramEmbedding`` for
7
+ this exact checkpoint shape.
8
+
9
+ The view contains hard links to the ordinary model shards plus a small index
10
+ that excludes resident PLE tensors. The original install stays immutable and
11
+ is still the source of mmap reads. Publication is lock-protected and atomic,
12
+ so concurrent replicas cannot observe a half-built view.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import fcntl
18
+ import hashlib
19
+ import json
20
+ import os
21
+ import shutil
22
+ import uuid
23
+ from pathlib import Path
24
+ from typing import Callable, Optional, Tuple
25
+
26
+
27
+ PLE_MARKER = ".ple.ple_embedding.ngram_embedding.shards."
28
+ VIEW_MARKER = "GEZEL_PLE_VIEW.json"
29
+
30
+
31
+ def _read_json(path: Path):
32
+ try:
33
+ return json.loads(path.read_text())
34
+ except (OSError, ValueError, TypeError):
35
+ return None
36
+
37
+
38
+ def _file_sha256(path: Path) -> str:
39
+ digest = hashlib.sha256()
40
+ with path.open("rb") as stream:
41
+ for chunk in iter(lambda: stream.read(1024 * 1024), b""):
42
+ digest.update(chunk)
43
+ return digest.hexdigest()
44
+
45
+
46
+ def checkpoint_signature(source: Path) -> Optional[dict]:
47
+ """Return the cheap immutable identity used to validate a derived view."""
48
+ config_path = source / "config.json"
49
+ index_path = source / "model.safetensors.index.json"
50
+ config = _read_json(config_path)
51
+ index = _read_json(index_path)
52
+ if not isinstance(config, dict) or not isinstance(index, dict):
53
+ return None
54
+
55
+ text_config = config.get("text_config")
56
+ if not isinstance(text_config, dict):
57
+ text_config = {}
58
+ model_types = {str(config.get("model_type", "")), str(text_config.get("model_type", ""))}
59
+ if not ({"qwen4_exp", "qwen4_exp_text"} & model_types):
60
+ return None
61
+
62
+ weight_map = index.get("weight_map")
63
+ if not isinstance(weight_map, dict) or not any(PLE_MARKER in str(key) for key in weight_map):
64
+ return None
65
+
66
+ return {
67
+ "source": str(source.resolve()),
68
+ "config_sha256": _file_sha256(config_path),
69
+ "index_sha256": _file_sha256(index_path),
70
+ }
71
+
72
+
73
+ def _valid_view(target: Path, signature: dict) -> bool:
74
+ marker = _read_json(target / VIEW_MARKER)
75
+ config = _read_json(target / "config.json")
76
+ ple_manifest = target / "ple-store.json"
77
+ if marker != signature or not isinstance(config, dict) or not ple_manifest.is_file():
78
+ return False
79
+ text_config = config.get("text_config")
80
+ ple_storage = text_config.get("ple_storage") if isinstance(text_config, dict) else None
81
+ return isinstance(ple_storage, dict) and ple_storage.get("manifest") == ple_manifest.name
82
+
83
+
84
+ def _same_device_target(source: Path, requested: Path) -> Path:
85
+ """Prefer the managed view root, then a same-volume sibling if needed."""
86
+ requested.parent.mkdir(parents=True, exist_ok=True)
87
+ if source.stat().st_dev == requested.parent.stat().st_dev:
88
+ return requested
89
+ sibling = source.parent / ".gezel-views" / requested.name
90
+ sibling.parent.mkdir(parents=True, exist_ok=True)
91
+ return sibling
92
+
93
+
94
+ def prepare_external_ple_view(
95
+ source_dir: str,
96
+ requested_view_dir: Optional[str],
97
+ *,
98
+ prepare_fn: Optional[Callable[..., object]] = None,
99
+ ) -> Tuple[str, str, bool]:
100
+ """Return ``(effective_model_dir, status, applicable)``.
101
+
102
+ Any unsupported shape or contained preparation failure returns the source
103
+ directory plus a diagnostic status. The caller decides whether a resident
104
+ fallback is safe for its memory reservation; Gezel's managed launcher
105
+ requires the view for applicable checkpoints.
106
+ """
107
+ source = Path(source_dir).expanduser().resolve()
108
+ if not requested_view_dir:
109
+ return str(source), "disabled", False
110
+ try:
111
+ signature = checkpoint_signature(source)
112
+ except OSError as exc:
113
+ return str(source), f"signature unavailable: {exc}", False
114
+ if signature is None:
115
+ return str(source), "not a quantized Qwen4 PLE checkpoint", False
116
+
117
+ try:
118
+ target = _same_device_target(source, Path(requested_view_dir).expanduser().resolve())
119
+ except OSError as exc:
120
+ return str(source), f"view root unavailable: {exc}", True
121
+
122
+ lock_path = target.parent / f".{target.name}.lock"
123
+ try:
124
+ with lock_path.open("a+b") as lock:
125
+ fcntl.flock(lock.fileno(), fcntl.LOCK_EX)
126
+ if _valid_view(target, signature):
127
+ return str(target), "reused", True
128
+
129
+ # A SIGKILL during an earlier publication can leave a sibling
130
+ # staging directory whose hard links would otherwise retain the
131
+ # checkpoint after a later model deletion. The per-target lock
132
+ # makes these unambiguously stale here.
133
+ for stale in target.parent.glob(f".{target.name}.*.partial"):
134
+ if stale.is_dir():
135
+ shutil.rmtree(stale, ignore_errors=True)
136
+
137
+ if prepare_fn is None:
138
+ try:
139
+ from mlx_vlm.models.qwen4_exp.ple_storage import (
140
+ prepare_external_ple_model,
141
+ )
142
+ except Exception as exc: # noqa: BLE001 - optional across configured runtimes
143
+ return str(source), f"mlx-vlm external PLE unavailable: {exc}", True
144
+ prepare_fn = prepare_external_ple_model
145
+
146
+ if target.exists():
147
+ shutil.rmtree(target)
148
+ staging = target.parent / f".{target.name}.{os.getpid()}.{uuid.uuid4().hex}.partial"
149
+ try:
150
+ prepare_fn(source, staging, cache_rows=0)
151
+ (staging / VIEW_MARKER).write_text(json.dumps(signature, indent=2) + "\n")
152
+ os.rename(staging, target)
153
+ finally:
154
+ shutil.rmtree(staging, ignore_errors=True)
155
+ if not _valid_view(target, signature):
156
+ return str(source), "prepared view failed validation", True
157
+ return str(target), "prepared", True
158
+ except Exception as exc: # noqa: BLE001 - report a contained preparation failure
159
+ return str(source), f"preparation failed: {exc}", True