mlx-bun 0.0.4 → 0.0.6

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 (90) hide show
  1. package/README.md +175 -93
  2. package/docs/dag/training-inference-map.html +575 -0
  3. package/docs/design/adapters-end-to-end.md +50 -46
  4. package/docs/design/compat-cli-surface-design.md +10 -8
  5. package/docs/design/docs-reorg-plan.md +14 -1
  6. package/docs/design/hlg-sampling.md +12 -2
  7. package/docs/design/memory-system.md +193 -295
  8. package/docs/design/optimization_plan.md +4 -2
  9. package/docs/design/orpo-dynamic-lambda.md +179 -0
  10. package/docs/design/orpo-future-enhancements.md +340 -0
  11. package/docs/design/orpo-training.md +1212 -0
  12. package/docs/design/parallel-slots.md +34 -26
  13. package/docs/design/parity-tier-dag.md +143 -0
  14. package/docs/design/runtime-isolation.md +1 -1
  15. package/docs/design/segmented-backward-training.md +58 -39
  16. package/docs/design/tauri-desktop-app.md +127 -0
  17. package/docs/design/w4a16-compute-precision-spike.md +232 -0
  18. package/docs/investigations/orpo-base-uf-experiment-and-directions.md +120 -0
  19. package/docs/investigations/orpo-flash-cce-pin-leak.md +167 -0
  20. package/docs/investigations/orpo-uf-testing-handoff.md +126 -0
  21. package/docs/{segmented-backward-handoff.md → investigations/segmented-backward-handoff.md} +1 -1
  22. package/docs/investigations/starter-model-port-handoff.md +32 -13
  23. package/docs/investigations/steel-flash-cce-handoff.md +489 -0
  24. package/docs/investigations/trainer-validation-experiment.md +173 -0
  25. package/docs/planning/IDEAS.md +29 -0
  26. package/docs/planning/PRODUCT_ROADMAP.md +147 -27
  27. package/docs/planning/chunk-task-roadmap.md +1 -1
  28. package/docs/planning/journal.md +161 -0
  29. package/docs/reference/distribution.md +61 -5
  30. package/docs/reference/library-api.md +29 -14
  31. package/docs/reference/memory.md +77 -0
  32. package/docs/reference/orpo-quickstart.md +130 -0
  33. package/docs/reference/server-api.md +105 -7
  34. package/docs/reference/server-config.md +12 -5
  35. package/docs/reference/training.md +296 -0
  36. package/package.json +1 -1
  37. package/src/cli.ts +651 -18
  38. package/src/eval/ifeval.ts +138 -0
  39. package/src/eval/runner.ts +76 -6
  40. package/src/eval/tasks/bfcl.ts +49 -15
  41. package/src/eval/tasks/gsm8k.ts +21 -9
  42. package/src/eval/tasks/hashhop.ts +33 -4
  43. package/src/eval/tasks/humaneval.ts +30 -8
  44. package/src/eval/tasks/ifeval.ts +52 -18
  45. package/src/eval/tasks/mmlu.ts +85 -74
  46. package/src/eval/ultrafeedback.ts +60 -0
  47. package/src/jobs/runner.ts +6 -0
  48. package/src/lora.ts +12 -7
  49. package/src/memory/chunk.ts +25 -0
  50. package/src/memory/cluster.ts +24 -0
  51. package/src/memory/db.ts +114 -0
  52. package/src/memory/ingest.ts +22 -0
  53. package/src/memory/pipeline.ts +63 -0
  54. package/src/memory/prompts.ts +23 -0
  55. package/src/memory/schedule.ts +163 -0
  56. package/src/memory/synthesize.ts +29 -0
  57. package/src/memory/tools.ts +399 -0
  58. package/src/memory/vault.ts +694 -0
  59. package/src/memory/wikify.ts +23 -0
  60. package/src/mlx/metal-kernel.ts +31 -4
  61. package/src/model/flash-attention.ts +37 -19
  62. package/src/model/fused-geglu-kernel.ts +111 -0
  63. package/src/model/gemma4-base.ts +39 -1
  64. package/src/model/gemma4.ts +196 -21
  65. package/src/model/minicpm5.ts +44 -3
  66. package/src/pi-provider.ts +29 -13
  67. package/src/pi-session.ts +81 -0
  68. package/src/pi-terminal.ts +13 -24
  69. package/src/pi-web.ts +246 -166
  70. package/src/quantize/job.ts +17 -2
  71. package/src/quantize/quantizer.ts +26 -2
  72. package/src/server.ts +216 -47
  73. package/src/tokenizer.ts +6 -2
  74. package/src/train/dataset.ts +29 -6
  75. package/src/train/flash-cce.ts +1175 -0
  76. package/src/train/job.ts +64 -3
  77. package/src/train/lora-params.ts +53 -4
  78. package/src/train/loss.ts +947 -0
  79. package/src/train/optimizer.ts +10 -3
  80. package/src/train/prefix-shared.ts +435 -0
  81. package/src/train/rank.ts +36 -0
  82. package/src/train/segmented.ts +1148 -3
  83. package/src/train/steel-qmm-header.ts +1461 -0
  84. package/src/train/trainer.ts +570 -54
  85. package/src/train/watch.ts +542 -0
  86. package/src/web/app.html +285 -19
  87. package/src/web/skills/memory/SKILL.md +97 -0
  88. package/src/web/skills/web-research/SKILL.md +3 -1
  89. package/src/web/skills.ts +46 -8
  90. /package/{docs → src/assets}/curve-designer.html +0 -0
package/src/web/app.html CHANGED
@@ -105,6 +105,39 @@
105
105
  #chat-think.think-on .dot{background:var(--blue);box-shadow:0 0 11px var(--blue)}
106
106
  .pill.bad .dot{background:var(--red);box-shadow:0 0 11px var(--red)}
107
107
  .pill.warn .dot{background:var(--orange);box-shadow:0 0 11px var(--orange)}
108
+ /* sampling disclosure: a small pill that opens a popover above the composer */
109
+ #chat-sampling-wrap{position:relative;display:inline-flex}
110
+ #chat-sampling{cursor:pointer;gap:6px;padding:5px 11px;font-size:12px;user-select:none;-webkit-user-select:none;
111
+ transition:color .22s var(--ease),border-color .22s var(--ease)}
112
+ #chat-sampling:hover{border-color:rgba(255,255,255,.28);color:var(--ink)}
113
+ #chat-sampling.on{color:var(--ink);border-color:rgba(255,255,255,.28)}
114
+ #chat-sampling.dirty .dot{background:var(--blue);box-shadow:0 0 11px var(--blue)}
115
+ #chat-sampling .dot{width:7px;height:7px}
116
+ .samplepop{position:absolute;bottom:calc(100% + 9px);right:0;z-index:40;width:300px;
117
+ background:rgba(30,30,34,.98);border:1px solid rgba(255,255,255,.12);border-radius:13px;padding:13px 14px;
118
+ backdrop-filter:blur(22px) saturate(1.3);-webkit-backdrop-filter:blur(22px) saturate(1.3);
119
+ box-shadow:0 18px 50px rgba(0,0,0,.7);display:none;
120
+ transform:translateY(4px);opacity:0;transition:opacity .16s var(--ease),transform .16s var(--ease)}
121
+ .samplepop.open{display:block;transform:translateY(0);opacity:1}
122
+ .samplepop h4{font-size:12px;font-weight:600;color:var(--dim);letter-spacing:.02em;
123
+ text-transform:uppercase;margin:0 0 10px}
124
+ .samprow{display:flex;align-items:center;gap:9px;margin:0 0 11px}
125
+ .samprow label{font-size:12.5px;color:var(--ink);width:74px;flex:0 0 auto}
126
+ .samprow input[type=range]{flex:1 1 auto;-webkit-appearance:none;appearance:none;height:4px;margin:0;
127
+ border-radius:3px;background:rgba(255,255,255,.15);outline:none;cursor:pointer}
128
+ .samprow input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:15px;height:15px;
129
+ border-radius:50%;background:var(--ink);border:0;box-shadow:0 1px 4px rgba(0,0,0,.5);cursor:pointer;
130
+ transition:background .15s var(--ease)}
131
+ .samprow input[type=range]:hover::-webkit-slider-thumb,
132
+ .samprow input[type=range]:focus::-webkit-slider-thumb{background:var(--blue)}
133
+ .samp-val{width:40px;flex:0 0 auto;text-align:right;font-family:var(--mono);font-size:12px;color:var(--ink)}
134
+ .samp-val.auto{color:var(--dimmer)}
135
+ .samppop-foot{display:flex;justify-content:space-between;align-items:center;margin-top:3px}
136
+ .samppop-foot .note{font-size:10.5px;color:var(--dimmer);line-height:1.3;flex:1 1 auto;padding-right:8px}
137
+ .samppop-foot button{appearance:none;cursor:pointer;background:var(--card);border:1px solid var(--hairline);
138
+ color:var(--dim);border-radius:8px;padding:5px 11px;font-size:11.5px;font-weight:600;flex:0 0 auto;
139
+ transition:color .18s var(--ease),border-color .18s var(--ease)}
140
+ .samppop-foot button:hover{color:var(--ink);border-color:rgba(255,255,255,.28)}
108
141
  #nav-download{color:var(--ink);font-family:var(--mono);font-size:11.5px;font-weight:500}
109
142
  #nav-download .dot{background:var(--orange);box-shadow:0 0 11px var(--orange);animation:dlpulse 1.4s ease-in-out infinite}
110
143
  @keyframes dlpulse{0%,100%{opacity:.4}50%{opacity:1}}
@@ -485,6 +518,7 @@
485
518
  <a class="tab" href="#/dataset" data-tab="dataset">Build Dataset</a>
486
519
  <a class="tab" href="#/status" data-tab="status">Status</a>
487
520
  <a class="tab" href="/curves" data-tab="curves">Curves</a>
521
+ <a class="tab" href="#/routes" data-tab="routes">Routes</a>
488
522
  </div>
489
523
  <div class="right">
490
524
  <button class="hf-gear" id="nav-hf" title="Hugging Face token settings" aria-label="Hugging Face settings">
@@ -552,6 +586,32 @@
552
586
  <div class="chat-hint">
553
587
  <span id="chat-status-line">Enter to send · Shift+Enter for a new line · type while it streams to steer</span>
554
588
  <span class="hint-right">
589
+ <span id="chat-sampling-wrap">
590
+ <span id="chat-sampling" class="pill" role="button" tabindex="0" aria-haspopup="true" aria-expanded="false"
591
+ title="Sampling controls (temperature · top_p · top_k)"><span class="dot"></span>Sampling</span>
592
+ <div id="chat-sampling-pop" class="samplepop" role="dialog" aria-label="Sampling controls">
593
+ <h4>Sampling</h4>
594
+ <div class="samprow">
595
+ <label for="samp-temp">Temperature</label>
596
+ <input id="samp-temp" type="range" min="0" max="2" step="0.05" aria-label="Temperature">
597
+ <output id="samp-temp-val" class="samp-val auto">0.70</output>
598
+ </div>
599
+ <div class="samprow">
600
+ <label for="samp-topp">top_p</label>
601
+ <input id="samp-topp" type="range" min="0" max="1" step="0.01" aria-label="top_p">
602
+ <output id="samp-topp-val" class="samp-val auto">0.95</output>
603
+ </div>
604
+ <div class="samprow">
605
+ <label for="samp-topk">top_k</label>
606
+ <input id="samp-topk" type="range" min="0" max="100" step="1" aria-label="top_k">
607
+ <output id="samp-topk-val" class="samp-val auto">off</output>
608
+ </div>
609
+ <div class="samppop-foot">
610
+ <span class="note">Sliders default to the recommended value — temperature follows the thinking toggle. Reset returns to defaults.</span>
611
+ <button id="samp-reset" type="button">Reset</button>
612
+ </div>
613
+ </div>
614
+ </span>
555
615
  <select id="chat-adapter" class="pill" title="LoRA adapter for this chat (none = base model)">
556
616
  <option value="">no adapter</option>
557
617
  </select>
@@ -581,10 +641,14 @@
581
641
  <p class="lead" style="margin:6px 0 22px;font-size:14.5px">Point at a Hugging Face repo id or a local snapshot path. Inspect first to confirm support and size.</p>
582
642
  <div class="field">
583
643
  <label>Model id or path</label>
584
- <input type="text" id="q-model" placeholder="mlx-community/Qwen3-8B-bf16" autocomplete="off">
585
- <div class="hint">Anything on huggingface.co, or an absolute path to a local snapshot.</div>
644
+ <div id="q-drop">
645
+ <input type="text" id="q-model" placeholder="mlx-community/Qwen3-8B-bf16" autocomplete="off">
646
+ </div>
647
+ <input type="file" id="q-folder" webkitdirectory directory style="display:none">
648
+ <div class="hint">Anything on huggingface.co, an absolute path to a local snapshot, or <strong>choose / drop a folder</strong> below — we read its <code>config.json</code> and find the snapshot on disk.</div>
586
649
  </div>
587
650
  <div class="btnrow">
651
+ <button class="btn ghost" id="q-browse">Choose folder…</button>
588
652
  <button class="btn ghost" id="q-inspect">Inspect</button>
589
653
  </div>
590
654
  <div id="q-inspect-out"></div>
@@ -643,7 +707,7 @@
643
707
  <p class="lead" style="margin:6px 0 18px;font-size:14.5px">Your quantized model is written to disk.</p>
644
708
  <div class="field"><label>Output directory</label>
645
709
  <pre class="logbox" id="q-out" style="max-height:none;margin-top:0"></pre></div>
646
- <div class="flash ok">Run <code>mlx-bun scan</code> (or it's auto-picked up on restart) to make this model serveable from the Library.</div>
710
+ <div class="flash ok">Written to your model cache and registered automatically — it's already serveable from the Library and the model picker.</div>
647
711
  <div class="grid g4" style="margin-top:14px">
648
712
  <div class="card"><h3>Push to Hugging Face</h3><div class="cap">Publish this quantized model to a Hugging Face repo.</div>
649
713
  <div class="btnrow" style="margin-top:14px"><button class="btn ghost sm" id="q-push">Push to HF</button></div>
@@ -671,10 +735,16 @@
671
735
  <p class="lead" style="margin:6px 0 22px;font-size:14.5px">The model you're adapting. Point at a local model directory or snapshot path.</p>
672
736
  <div class="field">
673
737
  <label>Model directory / path</label>
674
- <input type="text" id="f-model" placeholder="/abs/path/to/model or mlx-community/Qwen3-8B-4bit" autocomplete="off">
675
- <div class="hint">An OptiQ quant or any supported local model works.</div>
738
+ <div id="f-drop">
739
+ <input type="text" id="f-model" placeholder="/abs/path/to/model or mlx-community/Qwen3-8B-4bit" autocomplete="off">
740
+ </div>
741
+ <input type="file" id="f-folder" webkitdirectory directory style="display:none">
742
+ <div class="hint">An OptiQ quant or any supported local model works — type a path/repo id, or <strong>choose / drop a folder</strong> below.</div>
743
+ </div>
744
+ <div class="btnrow">
745
+ <button class="btn ghost" id="f-browse">Choose folder…</button>
746
+ <button class="btn primary" id="f-next0">Continue</button>
676
747
  </div>
677
- <div class="btnrow"><button class="btn primary" id="f-next0">Continue</button></div>
678
748
  </div>
679
749
 
680
750
  <!-- step 2: dataset -->
@@ -702,6 +772,7 @@
702
772
  <div class="seg" id="f-method">
703
773
  <button data-v="sft" class="on">SFT</button>
704
774
  <button data-v="dpo">DPO</button>
775
+ <button data-v="orpo">ORPO</button>
705
776
  </div>
706
777
  <div class="hint" id="f-method-hint">Supervised fine-tuning — data is messages or prompt+completion.</div>
707
778
  </div>
@@ -713,7 +784,18 @@
713
784
  <div class="hpgrid" style="margin-bottom:18px">
714
785
  <div class="field"><label>DPO beta (KL strength)</label><input type="number" id="f-beta" value="0.1" step="0.01" min="0.01" max="1"></div>
715
786
  <div class="field"><label>LR schedule</label>
716
- <select id="f-sched"><option value="cosine">cosine (recommended)</option><option value="constant">constant</option><option value="linear">linear</option></select></div>
787
+ <select id="f-sched"><option value="cosine">cosine (recommended)</option><option value="constant">constant</option></select></div>
788
+ </div>
789
+ </div>
790
+
791
+ <div id="f-orpo-extra" style="display:none">
792
+ <div class="flash warn" style="margin-bottom:18px">
793
+ <strong>ORPO is reference-free.</strong> One monolithic loss (SFT + odds-ratio), no reference model — half the forwards of DPO. Default LR 1e-5 (lower than DPO: the loss carries a full SFT term). λ weights only the odds-ratio term. Data is {prompt, chosen, rejected}.
794
+ </div>
795
+ <div class="hpgrid" style="margin-bottom:18px">
796
+ <div class="field"><label>ORPO λ (odds-ratio weight)</label><input type="number" id="f-orpo-lambda" value="0.1" step="0.05" min="0.01" max="2"></div>
797
+ <div class="field"><label>LR schedule</label>
798
+ <select id="f-orpo-sched"><option value="cosine">cosine (recommended)</option><option value="constant">constant</option></select></div>
717
799
  </div>
718
800
  </div>
719
801
 
@@ -950,6 +1032,12 @@
950
1032
  </div></div>
951
1033
  </section>
952
1034
 
1035
+ <!-- ═══════════════════ ROUTES ═══════════════════ -->
1036
+ <section data-route="routes" id="s-routes">
1037
+ <iframe src="/dag" title="Training · inference route map"
1038
+ style="width:100%;height:100%;border:0;display:block"></iframe>
1039
+ </section>
1040
+
953
1041
  </div>
954
1042
 
955
1043
  <div id="toasts"></div>
@@ -1027,7 +1115,7 @@ function mdInline(t) {
1027
1115
  // Pull code spans out first (any run of backticks), then escape the rest.
1028
1116
  t = String(t).replace(/(`+)([\s\S]+?)\1/g, (_m, _ticks, c) => {
1029
1117
  codes.push(c.replace(/^ | $/g, ""));
1030
- return "C" + (codes.length - 1) + "";
1118
+ return "\u0000C" + (codes.length - 1) + "\u0000";
1031
1119
  });
1032
1120
  let s = esc(t);
1033
1121
  // [text](url "title") — title optional and ignored.
@@ -1045,7 +1133,7 @@ function mdInline(t) {
1045
1133
  // Autolink bare URLs that markdown didn't already wrap.
1046
1134
  s = s.replace(/(^|[\s(])(https?:\/\/[^\s<)]+[^\s<).,;:!?'"])/g,
1047
1135
  (_m, pre, url) => pre + '<a href="' + url + '" target="_blank" rel="noopener noreferrer">' + url + "</a>");
1048
- return s.replace(/C(\d+)/g, (_m, i) => "<code>" + esc(codes[+i]) + "</code>");
1136
+ return s.replace(/\u0000C(\d+)\u0000/g, (_m, i) => "<code>" + esc(codes[+i]) + "</code>");
1049
1137
  }
1050
1138
 
1051
1139
  function mdCodeBlock(lang, code) {
@@ -1276,7 +1364,7 @@ async function pushToHub(panel, { kind, job_id, source_path }) {
1276
1364
  /* ════════════════════════════════════════════════════════════════════
1277
1365
  ROUTER — toggles section[data-route]; lazily inits each controller.
1278
1366
  ════════════════════════════════════════════════════════════════════ */
1279
- const ROUTES = ["chat", "quantize", "finetune", "dataset", "status"];
1367
+ const ROUTES = ["chat", "quantize", "finetune", "dataset", "status", "routes"];
1280
1368
  const inited = {};
1281
1369
  const controllers = {}; // name -> { init, enter, leave }
1282
1370
 
@@ -1419,7 +1507,7 @@ controllers.chat = (() => {
1419
1507
  el("div", "who", m).textContent = "local agent";
1420
1508
  const bubble = el("div", "bubble", m);
1421
1509
  const thinkBox = el("details", "thinkbox", bubble);
1422
- thinkBox.open = true;
1510
+ thinkBox.open = false; // collapsed by default; the reasoning stays tucked behind "Thinking" — click to expand
1423
1511
  el("summary", "", thinkBox).textContent = "Thinking";
1424
1512
  const thinkBody = el("div", "thinkbody", thinkBox);
1425
1513
  thinkBox.style.display = "none";
@@ -1726,7 +1814,15 @@ controllers.chat = (() => {
1726
1814
  let attachments = []; // { id, kind:"image"|"text", name, mimeType, data, text, truncated }
1727
1815
  let visionCapable = false; // from the `ready` frame; gates image attachments
1728
1816
  let thinkingCapable = false; // from `ready`; gates the reasoning toggle
1729
- let thinkingOn = true; // on/off state for the model's reasoning channel
1817
+ let thinkingOn = true; // default on: with tools (read/web_search) the reasoning step gives the small model far better tool judgment (answers math/writing itself, only searches when needed). The thinking box defaults collapsed. Users can toggle it off.
1818
+ // Per-request sampling. Each field is either a user override (a number) or
1819
+ // null = "use the recommended default" (resolved server-side). The recommended
1820
+ // TEMPERATURE tracks the thinking toggle (0.7 off ↔ 0.9 on); top_p/top_k recs
1821
+ // are fixed mirrors of the server defaults (display only). The placeholder in
1822
+ // each input shows the live recommended value when the field isn't overridden.
1823
+ const sampling = { temperature: null, top_p: null, top_k: null };
1824
+ const SAMP_REC = { topP: 0.95, topK: 0 }; // fixed recommended top_p / top_k (server genDefaults)
1825
+ function recTemp() { return thinkingOn ? 0.9 : 0.7; } // mirrors server toOptions defaultTemp
1730
1826
  let attachSeq = 0;
1731
1827
  const MAX_TEXT_BYTES = 256 * 1024;
1732
1828
  const TEXT_EXTS = /\.(txt|md|markdown|csv|tsv|json|jsonl|ya?ml|toml|ini|cfg|conf|log|xml|html?|css|scss|js|mjs|cjs|ts|tsx|jsx|py|rs|go|java|c|h|cpp|hpp|cc|cs|rb|php|sh|bash|zsh|fish|sql|swift|kt|lua|r|jl|tex|svg|env|gitignore|dockerfile|makefile)$/i;
@@ -1823,6 +1919,81 @@ controllers.chat = (() => {
1823
1919
  b.classList.toggle("think-on", thinkingOn);
1824
1920
  b.title = thinkingOn ? "Reasoning on — click to answer directly" : "Reasoning off — click to let the model think";
1825
1921
  b.setAttribute("aria-pressed", String(thinkingOn));
1922
+ refreshSamplingRecs(); // recommended temperature follows the thinking state
1923
+ }
1924
+
1925
+ /* — sampling controls — */
1926
+ // Each row is a slider + a live value readout. A field is "auto" (null
1927
+ // override → the server uses its mode-aware default; temperature's default
1928
+ // follows the thinking toggle) until the user drags it, which records an
1929
+ // explicit override. Reset returns every field to auto.
1930
+ const SAMP_FIELDS = [
1931
+ { id: "samp-temp", field: "temperature", valId: "samp-temp-val", rec: () => recTemp(), fmt: (v) => v.toFixed(2) },
1932
+ { id: "samp-topp", field: "top_p", valId: "samp-topp-val", rec: () => SAMP_REC.topP, fmt: (v) => v.toFixed(2) },
1933
+ { id: "samp-topk", field: "top_k", valId: "samp-topk-val", rec: () => SAMP_REC.topK, fmt: (v) => (v >= 1 ? String(Math.round(v)) : "off") },
1934
+ ];
1935
+
1936
+ // Sync every slider + readout to current state: an overridden field shows its
1937
+ // set value (bright readout); an auto field shows the recommended value
1938
+ // (dimmed) and tracks it live — e.g. temperature when the thinking toggle
1939
+ // flips. Called on thinking-toggle flips, popover open, and reset.
1940
+ function refreshSamplingRecs() {
1941
+ for (const f of SAMP_FIELDS) {
1942
+ const slider = $(f.id), val = $(f.valId); if (!slider || !val) continue;
1943
+ const over = sampling[f.field];
1944
+ const v = over != null ? over : f.rec();
1945
+ slider.value = String(v);
1946
+ val.textContent = f.fmt(v);
1947
+ val.classList.toggle("auto", over == null);
1948
+ }
1949
+ }
1950
+
1951
+ // User dragged a slider → record the explicit override, brighten the readout,
1952
+ // and push the whole sampling state to the server.
1953
+ function onSlide(f) {
1954
+ const slider = $(f.id); if (!slider) return;
1955
+ const v = Number(slider.value);
1956
+ sampling[f.field] = v;
1957
+ const val = $(f.valId); if (val) { val.textContent = f.fmt(v); val.classList.remove("auto"); }
1958
+ updateSamplingUi();
1959
+ send({ type: "set_sampling", temperature: sampling.temperature, top_p: sampling.top_p, top_k: sampling.top_k });
1960
+ }
1961
+
1962
+ // The pill shows a lit dot whenever any field is overridden.
1963
+ function updateSamplingUi() {
1964
+ const dirty = sampling.temperature != null || sampling.top_p != null || sampling.top_k != null;
1965
+ const pill = $("chat-sampling");
1966
+ if (pill) { pill.classList.toggle("dirty", dirty); pill.title = dirty
1967
+ ? "Sampling overridden — click to edit or reset" : "Sampling controls (temperature · top_p · top_k)"; }
1968
+ }
1969
+
1970
+ function resetSampling() {
1971
+ sampling.temperature = null; sampling.top_p = null; sampling.top_k = null;
1972
+ refreshSamplingRecs(); updateSamplingUi();
1973
+ send({ type: "set_sampling", temperature: null, top_p: null, top_k: null });
1974
+ }
1975
+ function initSampling() {
1976
+ const pill = $("chat-sampling"), pop = $("chat-sampling-pop");
1977
+ if (!pill || !pop) return;
1978
+ const setOpen = (open) => {
1979
+ pop.classList.toggle("open", open);
1980
+ pill.classList.toggle("on", open);
1981
+ pill.setAttribute("aria-expanded", String(open));
1982
+ if (open) refreshSamplingRecs();
1983
+ };
1984
+ const toggle = () => setOpen(!pop.classList.contains("open"));
1985
+ pill.onclick = (e) => { e.stopPropagation(); toggle(); };
1986
+ pill.addEventListener("keydown", (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); toggle(); } });
1987
+ pop.addEventListener("click", (e) => e.stopPropagation());
1988
+ // Click-away / Escape closes the popover.
1989
+ document.addEventListener("click", () => setOpen(false));
1990
+ document.addEventListener("keydown", (e) => { if (e.key === "Escape") setOpen(false); });
1991
+ for (const f of SAMP_FIELDS) {
1992
+ const e = $(f.id); if (!e) continue;
1993
+ e.addEventListener("input", () => onSlide(f));
1994
+ }
1995
+ const reset = $("samp-reset"); if (reset) reset.onclick = (e) => { e.stopPropagation(); resetSampling(); };
1996
+ updateSamplingUi(); refreshSamplingRecs();
1826
1997
  }
1827
1998
 
1828
1999
  /* — composer — */
@@ -1842,8 +2013,10 @@ controllers.chat = (() => {
1842
2013
  addUserMsg(text, attachments);
1843
2014
  box.value = ""; box.style.height = "auto";
1844
2015
  const frame = imgs.length ? { text: combined, images: imgs } : { text: combined };
1845
- if (turnActive) send({ type: "steer", ...frame }); // mid-turn: steer/queue
1846
- else send({ type: "prompt", ...frame });
2016
+ // A composer submission is always a new user turn. If the model is still
2017
+ // streaming, the server passes it to pi with streamingBehavior:"followUp"
2018
+ // so it becomes the next turn instead of steering/mutating the current one.
2019
+ send({ type: "prompt", ...frame });
1847
2020
  clearAttachments();
1848
2021
  }
1849
2022
  const scroll = () => $("chat-scroll");
@@ -1860,6 +2033,7 @@ controllers.chat = (() => {
1860
2033
  $("chat-think").addEventListener("keydown", (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); thinkToggle(); } });
1861
2034
  const adapterSel = $("chat-adapter");
1862
2035
  if (adapterSel) adapterSel.onchange = () => onSelectAdapter(adapterSel);
2036
+ initSampling();
1863
2037
  refreshAdapters();
1864
2038
  const box = $("chat-box");
1865
2039
  box.addEventListener("keydown", (e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); submit(); } });
@@ -1922,6 +2096,33 @@ controllers.quantize = (() => {
1922
2096
  }));
1923
2097
  }
1924
2098
 
2099
+ // --- OS folder picker / drag-drop → real on-disk snapshot path ---
2100
+ // The browser can't reveal an absolute path, and the HF cache uses symlinked
2101
+ // snapshots over a blobs/ store — so we DON'T read any files. We only take the
2102
+ // picked folder's NAME (which encodes the repo id) and let the server
2103
+ // reconstruct the snapshot path it owns. No upload, no "config.json" hunt.
2104
+ async function resolveByName(name, relPath) {
2105
+ if (!name && !relPath) return;
2106
+ const out = $("q-inspect-out");
2107
+ out.innerHTML = '<div class="flash"><span class="shimmer">locating on disk…</span></div>';
2108
+ const d = await api("/api/quantize/resolve-folder", { method: "POST", body: { folder_name: name || "", rel_path: relPath || "" } })
2109
+ .catch(() => ({ ok: false, error: "request failed" }));
2110
+ if (!d.ok) { out.innerHTML = '<div class="flash err">' + esc(d.error || "could not locate this folder on the server") + "</div>"; return; }
2111
+ $("q-model").value = d.path;
2112
+ inspect();
2113
+ }
2114
+ async function pickFolder() {
2115
+ // Native OS folder dialog with no "upload N files" prompt; we only read the
2116
+ // folder's .name, never its contents.
2117
+ if (window.showDirectoryPicker) {
2118
+ try {
2119
+ const handle = await window.showDirectoryPicker({ id: "mlx-models", mode: "read" });
2120
+ return resolveByName(handle.name);
2121
+ } catch (e) { return; } // user cancelled / denied
2122
+ }
2123
+ $("q-folder").click(); // fallback for browsers without the File System Access API
2124
+ }
2125
+
1925
2126
  async function inspect() {
1926
2127
  const id = $("q-model").value.trim();
1927
2128
  if (!id) { toast("Enter a model id or path first.", "err"); return; }
@@ -1978,6 +2179,26 @@ controllers.quantize = (() => {
1978
2179
  show(0);
1979
2180
  $("q-inspect").onclick = inspect;
1980
2181
  $("q-model").addEventListener("keydown", (e) => { if (e.key === "Enter") inspect(); });
2182
+ // OS folder dialog (native; resolves by folder name, no upload)
2183
+ $("q-browse").onclick = pickFolder;
2184
+ $("q-folder").addEventListener("change", (e) => {
2185
+ const f = e.target.files[0]; e.target.value = "";
2186
+ if (f) resolveByName((f.webkitRelativePath || f.name).split("/")[0], f.webkitRelativePath || "");
2187
+ });
2188
+ // drag a folder onto the box — use the directory entry's NAME (no read)
2189
+ const drop = $("q-drop");
2190
+ const hi = (on) => { drop.style.outline = on ? "2px dashed var(--blue)" : ""; drop.style.outlineOffset = on ? "4px" : ""; };
2191
+ ["dragenter", "dragover"].forEach((ev) => drop.addEventListener(ev, (e) => { e.preventDefault(); hi(true); }));
2192
+ ["dragleave", "drop"].forEach((ev) => drop.addEventListener(ev, (e) => {
2193
+ e.preventDefault(); hi(false);
2194
+ if (ev !== "drop" || !e.dataTransfer) return;
2195
+ for (const it of [...(e.dataTransfer.items || [])]) {
2196
+ const en = it.webkitGetAsEntry && it.webkitGetAsEntry();
2197
+ if (en && en.isDirectory) return resolveByName(en.name);
2198
+ }
2199
+ const f = e.dataTransfer.files && e.dataTransfer.files[0];
2200
+ if (f) resolveByName((f.webkitRelativePath || f.name).split("/")[0]);
2201
+ }));
1981
2202
  seg($("q-bits"), (v) => { bits = +v; $("q-bits-hint").textContent = v === "4"
1982
2203
  ? "4-bit · ~4× smaller than bf16 · the recommended default for local serving."
1983
2204
  : "8-bit · ~2× smaller than bf16 · highest fidelity quant."; });
@@ -1997,7 +2218,7 @@ controllers.quantize = (() => {
1997
2218
  ════════════════════════════════════════════════════════════════════ */
1998
2219
  controllers.finetune = (() => {
1999
2220
  const STEPS = ["Base", "Dataset", "Hyperparams", "Train", "Done"];
2000
- let step = 0, method = "sft", lrTouched = false, es = null, log = [];
2221
+ let step = 0, method = "sft", lrTouched = false, rankTouched = false, es = null, log = [];
2001
2222
  let datasetOk = false, adapterPath = "";
2002
2223
  const trainPts = [], valPts = [];
2003
2224
 
@@ -2007,6 +2228,23 @@ controllers.finetune = (() => {
2007
2228
  renderSteps($("f-steps"), STEPS, n);
2008
2229
  }
2009
2230
 
2231
+ // OS folder picker for the base model — resolve by NAME server-side (no
2232
+ // upload, works with the HF cache's symlinked layout). Mirrors Quantize.
2233
+ async function resolveBaseByName(name, relPath) {
2234
+ if (!name && !relPath) return;
2235
+ const d = await api("/api/model/resolve-folder", { method: "POST", body: { folder_name: name || "", rel_path: relPath || "" } })
2236
+ .catch(() => ({ ok: false, error: "request failed" }));
2237
+ if (!d.ok) { toast(d.error || "could not locate this folder", "err"); return; }
2238
+ $("f-model").value = d.path;
2239
+ }
2240
+ async function pickBaseFolder() {
2241
+ if (window.showDirectoryPicker) {
2242
+ try { const h = await window.showDirectoryPicker({ id: "mlx-models", mode: "read" }); return resolveBaseByName(h.name); }
2243
+ catch (e) { return; }
2244
+ }
2245
+ $("f-folder").click();
2246
+ }
2247
+
2010
2248
  async function inspectDataset() {
2011
2249
  const path = $("f-data").value.trim();
2012
2250
  if (!path) { toast("Enter a dataset directory.", "err"); return; }
@@ -2024,10 +2262,15 @@ controllers.finetune = (() => {
2024
2262
  method = m;
2025
2263
  $("f-method").querySelectorAll("button").forEach((b) => b.classList.toggle("on", b.dataset.v === m));
2026
2264
  $("f-dpo-extra").style.display = m === "dpo" ? "" : "none";
2265
+ $("f-orpo-extra").style.display = m === "orpo" ? "" : "none";
2027
2266
  $("f-method-hint").textContent = m === "dpo"
2028
2267
  ? "Direct Preference Optimization — data is {prompt, chosen, rejected}."
2029
- : "Supervised fine-tuning — data is messages or prompt+completion.";
2030
- if (!lrTouched) $("f-lr").value = m === "dpo" ? 0.00005 : 0.0002;
2268
+ : m === "orpo"
2269
+ ? "Odds Ratio Preference Optimization (reference-free) — data is {prompt, chosen, rejected}."
2270
+ : "Supervised fine-tuning — data is messages or prompt+completion.";
2271
+ if (!lrTouched) $("f-lr").value = m === "dpo" ? 0.00005 : m === "orpo" ? 0.00001 : 0.0002;
2272
+ // ORPO's validated LoRA recipe uses rank 16 (vs 8 for SFT); apply unless edited.
2273
+ if (!rankTouched) $("f-rank").value = m === "orpo" ? 16 : 8;
2031
2274
  }
2032
2275
 
2033
2276
  function collectHP() {
@@ -2036,11 +2279,12 @@ controllers.finetune = (() => {
2036
2279
  model_dir: $("f-model").value.trim(),
2037
2280
  data_dir: $("f-data").value.trim(),
2038
2281
  method,
2039
- rank: +$("f-rank").value, scale: +$("f-scale").value, dropout: +$("f-dropout").value,
2282
+ rank: +$("f-rank").value, scale: +$("f-scale").value, lora_dropout: +$("f-dropout").value,
2040
2283
  target_modules: modules, num_layers: +$("f-layers").value, iters: +$("f-iters").value,
2041
2284
  batch_size: +$("f-batch").value, learning_rate: +$("f-lr").value, max_seq_length: +$("f-seq").value,
2042
2285
  };
2043
- if (method === "dpo") { hp.dpo_beta = +$("f-beta").value; hp.lr_schedule = $("f-sched").value; }
2286
+ if (method === "dpo") { hp.dpo_beta = +$("f-beta").value; hp.dpo_lr_schedule = $("f-sched").value; }
2287
+ if (method === "orpo") { hp.orpo_lambda = +$("f-orpo-lambda").value; hp.orpo_lr_schedule = $("f-orpo-sched").value; }
2044
2288
  return hp;
2045
2289
  }
2046
2290
 
@@ -2162,12 +2406,34 @@ controllers.finetune = (() => {
2162
2406
  init() {
2163
2407
  show(0);
2164
2408
  $("f-next0").onclick = () => { if (!$("f-model").value.trim()) { toast("Enter a base model path.", "err"); return; } show(1); };
2409
+ // OS folder dialog for the base model (native; resolves by name, no upload)
2410
+ $("f-browse").onclick = pickBaseFolder;
2411
+ $("f-folder").addEventListener("change", (e) => {
2412
+ const f = e.target.files[0]; e.target.value = "";
2413
+ if (f) resolveBaseByName((f.webkitRelativePath || f.name).split("/")[0], f.webkitRelativePath || "");
2414
+ });
2415
+ {
2416
+ const drop = $("f-drop");
2417
+ const hi = (on) => { drop.style.outline = on ? "2px dashed var(--blue)" : ""; drop.style.outlineOffset = on ? "4px" : ""; };
2418
+ ["dragenter", "dragover"].forEach((ev) => drop.addEventListener(ev, (e) => { e.preventDefault(); hi(true); }));
2419
+ ["dragleave", "drop"].forEach((ev) => drop.addEventListener(ev, (e) => {
2420
+ e.preventDefault(); hi(false);
2421
+ if (ev !== "drop" || !e.dataTransfer) return;
2422
+ for (const it of [...(e.dataTransfer.items || [])]) {
2423
+ const en = it.webkitGetAsEntry && it.webkitGetAsEntry();
2424
+ if (en && en.isDirectory) return resolveBaseByName(en.name);
2425
+ }
2426
+ const f = e.dataTransfer.files && e.dataTransfer.files[0];
2427
+ if (f) resolveBaseByName((f.webkitRelativePath || f.name).split("/")[0]);
2428
+ }));
2429
+ }
2165
2430
  $("f-inspect").onclick = inspectDataset;
2166
2431
  $("f-data").addEventListener("keydown", (e) => { if (e.key === "Enter") inspectDataset(); });
2167
2432
  $("f-data").addEventListener("input", () => { datasetOk = false; $("f-next1").disabled = true; $("f-inspect-out").innerHTML = ""; });
2168
2433
  $("f-next1").onclick = () => { if (datasetOk) show(2); };
2169
2434
  $("f-method").querySelectorAll("button").forEach((b) => b.onclick = () => setMethod(b.dataset.v));
2170
2435
  $("f-lr").addEventListener("input", () => { lrTouched = true; });
2436
+ $("f-rank").addEventListener("input", () => { rankTouched = true; });
2171
2437
  document.querySelectorAll("#s-finetune [data-fback]").forEach((b) => b.onclick = () => show(step - 1));
2172
2438
  $("f-submit").onclick = submit;
2173
2439
  $("f-merge-go").onclick = mergeAdapters;
@@ -0,0 +1,97 @@
1
+ ---
2
+ name: memory
3
+ description: Use when the answer depends on the user's prior conversations or personal context: explicit memory/recall requests, named personal projects, people, preferences, decisions, or history not explained in the current chat. Do not use for weather, current public facts, generic web research, or ordinary coding/file tasks when current files are sufficient. Also use when the user wants to set up, inspect, or manage memory.
4
+ ---
5
+
6
+ # Memory
7
+
8
+ The user has a personal **memory** — a wiki of Markdown articles synthesized
9
+ from their past conversations, stored locally at `~/.mlx-bun/wiki/`. User memory
10
+ articles live under `articles/`. Read-only mlx-bun reference docs live under
11
+ `Reference/` as symlinks and are also searchable/readable, but synthesis never
12
+ writes them. Each user article is the consolidated, finished understanding of one
13
+ topic. Your job is to *use it* so the user never has to re-explain what they've
14
+ already told you.
15
+
16
+ It is entirely local: the articles, the synthesis, and these tools all run on
17
+ the user's machine. Nothing about their memory leaves the device.
18
+
19
+ ## Consulting memory (the common case)
20
+
21
+ When the answer depends on personal continuity — an explicit memory/recall
22
+ request, or a named project, person, decision, preference, or history that is
23
+ not explained in the current chat — consult memory before answering or asking
24
+ the user to re-explain.
25
+
26
+ Do **not** use memory for weather, current public facts, generic web research,
27
+ or ordinary coding/file tasks when the current files or the web tools answer the
28
+ question. Memory is for user-specific context, not for performing familiarity.
29
+
30
+ 1. **Search.** Call `memory_search` with the topic (plain phrasing is fine —
31
+ a name or key noun). It returns ranked articles and sample lines.
32
+ 2. **Read enough.** For short or central articles, call `memory_read`. For long
33
+ articles, use `memory_toc` then `memory_section` to read the relevant part.
34
+ The article is the answer — read it rather than reconstructing the user's
35
+ position from scattered fragments. Treat it as their current view.
36
+ 3. **Follow nearby context when useful.** Use `memory_links` if backlinks or
37
+ related concepts matter. `memory_list` gives an overview when search comes up
38
+ short. mlx-bun reference docs are separate from personal memory; use
39
+ `reference_search`, `reference_read`, and `reference_list` for those.
40
+ `memory_status` answers setup/schedule/location questions.
41
+ 4. **Use it silently, as a prior.** Let what you find shape your response like
42
+ a long-time colleague would — don't announce "I searched your memory."
43
+
44
+ Use memory as an invisible prior: it should narrow relevance and improve the
45
+ answer, not become the topic of the answer. Avoid performative recall — do not
46
+ name-drop unrelated remembered facts to sound familiar.
47
+
48
+ ## Opening memory for the user
49
+
50
+ The wiki is a normal Markdown/Obsidian vault. If the user asks to browse, open,
51
+ inspect, or edit their memory directly, you may open it for them after a brief
52
+ confirmation:
53
+
54
+ ```bash
55
+ mlx-bun memory open
56
+ mlx-bun memory open <article>
57
+ ```
58
+
59
+ The first form opens `~/.mlx-bun/wiki/` in Obsidian when available, falling back
60
+ to the folder in Finder. The second form opens a specific article stem such as
61
+ `Archie_Project` directly in Obsidian, falling back to the default Markdown app.
62
+ Opening the vault or article is allowed; editing articles during chat is not. If
63
+ the user wants a change to become durable memory, keep the conversation natural
64
+ and let synthesis incorporate it later, unless they explicitly ask to edit files
65
+ by hand in Obsidian.
66
+
67
+ ## Setting up memory (if it doesn't exist yet)
68
+
69
+ If memory isn't set up (the memory tools aren't available, or the user asks to
70
+ turn it on), explain it plainly and set it up on agreement:
71
+
72
+ > Your memory is a folder of Markdown articles at `~/.mlx-bun/wiki/`,
73
+ > synthesized from your conversations so I can remember your context across
74
+ > sessions. It's a normal git-tracked wiki you fully own and can edit or
75
+ > delete — and it never leaves your machine.
76
+
77
+ On a yes, run the setup via the bash tool (the user approves it):
78
+
79
+ ```
80
+ mlx-bun memory init
81
+ ```
82
+
83
+ This creates the vault and walks through optional steps (importing an existing
84
+ wiki, scheduling nightly synthesis). Confirm before each persistent action.
85
+
86
+ ## What not to do
87
+
88
+ - Don't claim a fact "from memory" that you didn't read in an article — search
89
+ and read first, or say you don't have it.
90
+ - Don't write to or edit the wiki during a normal conversation; the read tools
91
+ are read-only by design. Synthesis is a separate, deliberate step.
92
+ - Do open the wiki or a specific article in Obsidian/Finder if the user asks to
93
+ browse or edit their memory themselves (`mlx-bun memory open [article]`).
94
+ - Don't dump article contents at the user verbatim — use them to inform a
95
+ direct, relevant answer.
96
+ - Don't perform familiarity. If the user asks for weather, current facts, or a
97
+ normal coding task, use the task-appropriate tool instead of memory.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: web-research
3
- description: Use when the user asks you to research something, do a deep dive, fact-check a claim, compare options, or answer a question that needs current, accurate, real-world information ("what's the latest on…", prices, releases, news, specs). Gives a rigorous workflow for searching the web, reading primary sources, cross-checking claims, and answering with citations.
3
+ description: Use only when the user asks you to research something, do a deep dive, fact-check a claim, compare options, or answer a question that needs current, accurate, real-world information ("what's the latest on…", prices, releases, news, specs). Do not use for ordinary conversation, ordinary coding tasks, or questions answerable from the current conversation/files.
4
4
  ---
5
5
 
6
6
  # Web Research
@@ -16,6 +16,8 @@ trustworthy, well-sourced answer — not a guess dressed up as one.
16
16
  - The user wants a thorough or authoritative answer, or asks for sources.
17
17
  - A specific claim needs verifying, or you would otherwise be speculating.
18
18
 
19
+ Do **not** use this for ordinary conversation that does not require external information. In that case, reply naturally instead of explaining the research workflow.
20
+
19
21
  ## Workflow
20
22
 
21
23
  1. **Plan.** Restate the question in your own words and list the 2–4 concrete