FLINT_HEAL_MODEL_CHOICE_STICKS_v0.1 (tiny)
CHANGELOG 2026-09-29 2:28 PM MDT: re-checked on mains FL 7db2d94 / Alpha 395c761 (unchanged). The FL diff applies with plain git apply on main and on PR #124's branch (env heal). After #122 and #123 also land, every code hunk still applies; only the ledger hunk needs its text anchor (insert right above "## Out of scope for this brick"; git apply -C1 does it). Re-check commands now name ~/FreeLattice and ~/FreeLattice-Alpha. Local paths: FreeLattice is ~/FreeLattice and the Tree (repo Chaos2Cured/FreeLattice-Alpha, site thelatticetree.com) is ~/FreeLattice-Alpha on Flint's machine. Wherever this paste says "theLatticeTree" it means that repo; "../FreeLattice" from inside ~/FreeLattice-Alpha is correct as written.
The model she taps is the model that answers. Both repos. Shared fl-connect.js stays byte-identical.

Marker:        v-model-choice-sticks-v0
Smoke (FL):    SMOKE_OK model choice sticks v0.1          (docs/scripts/smoke-model-choice-sticks.js)
Smoke (Alpha): SMOKE_OK model choice sticks alpha v0.1    (docs/modules/model-choice-sticks.test.js)
Source:        Hypha walk 4 of Connect heal v0.4 + v0.4.1 on the live sites: 9 PASS, 2 HOLD (items 7 and 10),
               one cause. Written by Grok Bot for Kirk, Mon Sep 28 2026, about 9:45 PM MDT.
Full diffs:    /workspace/flint-pastes/FLINT_HEAL_MODEL_CHOICE_STICKS_v0.1.diff        (FreeLattice)
               /workspace/flint-pastes/FLINT_HEAL_MODEL_CHOICE_STICKS_v0.1-alpha.diff  (theLatticeTree)
               Both are also inlined at the end (Appendix A and B), so this paste stands alone.
Temperature:   green braided gold, the second cup stays where she set it down.

======================================================================
0. ORDER, MAINS, LOCKS
======================================================================

0.1 Where this lands (new Flint order):
      1) FLINT_HEAL_AGENT_BRIDGE_ENV_v0.1.1_tiny.txt              (FL only, security, first)
      2) THIS heal (both repos)                                    <- new, tiny, before the Tree paste
      3) FLINT_NEXT_TREE_ANY_MODEL_v0.1.txt                        (both repos)
      4) FLINT_NEXT_CONNECT_BOOT_PROBE_F1_F2_F3_F4_v0.1.txt        (both repos)
      5) FLINT_NEXT_120_DOCS_LAYER_v0.1_tiny.txt
      6) FLINT_HEAL_LOVE_LOGIC_V3_PHONE_v0.1.txt
      7) FLINT_NEXT_LOVE_LOGIC_V4_WALK_THE_GARDEN_v0.1.txt
    Why before the Tree paste: it is the live HOLD from walk 4, it is small, and it touches
    none of the lines the Tree or boot probe pastes edit. Verified: on FL main 7db2d94, applying
    env heal -> this heal -> (a faithful simulation of) the Tree fl-connect.js changes -> the boot
    probe Appendix A (app.html) and Appendix B (fl-connect.js) diffs all apply with plain
    `git apply` (no fuzz, offsets only). After all of them: smoke-model-choice-sticks,
    smoke-connect-boot-probe, every smoke-connect-* (except the pre-existing yesno-wizard) and
    tests/smoke.js ALL 3597 are green.

0.2 Mains re-checked just before writing:
      FreeLattice     main = 7db2d94 (CI primer-only commit on a80c8e4 #121; app code = a80c8e4)
      theLatticeTree  main = 395c761 (#83)
      docs/modules/fl-connect.js identical on both mains.
    Line numbers below are "main N" on those SHAs. Anchor on the quoted TEXT.
    Re-check:
      git -C ~/FreeLattice fetch origin && git -C ~/FreeLattice log --oneline -2 origin/main
      git -C ~/FreeLattice-Alpha fetch origin && git -C ~/FreeLattice-Alpha log --oneline -2 origin/main
      cmp ~/FreeLattice/docs/modules/fl-connect.js ~/FreeLattice-Alpha/docs/modules/fl-connect.js

0.3 Locks: Layer, never delete. Root sw.js byte-identical to main; docs/sw.js untouched.
    index.html == docs/app.html (copy after every app.html edit). fl-connect.js byte-identical
    across repos. No conflict markers. Every inline <script> parses. textContent / createElement for
    new words (showToast already uses textContent). No bare * origin. No confirm(). No approval
    screens. 127.0.0.1 only. Soft leave. AUTONOMY.md: no gates for local choices. No em dashes in
    anything added. Design for Mom: no Terminal words on the main path.
    Soft paste stays: Named five stay five. Family uncapped. Quiet Room shut.

======================================================================
1. DO THE OTHER PASTES ALREADY FIX IT? (short answer: no, only partly)
======================================================================

1.1 FLINT_NEXT_TREE_ANY_MODEL_v0.1.txt: does NOT fix the FreeLattice HOLD.
    - It changes the Tree side: local-mind-probe.js keeps the chosen model when a Find answers
      through another door of the same family (its step 5.2), mergeFound (6.1), the Alpha branch of
      remember() (6.2), and the chosen mark (6.4, chosenModelName reads state.ollamaModel).
    - Its own step 6.3 says: "FreeLattice's branch of remember() ignores mind.models, so FreeLattice
      behavior is unchanged." It never touches FLActiveModel or FLAutoModel. The FL bug stays.
    - On the Tree it DOES heal one related case this heal leaves alone: Connect pick through a
      Bridge port (url 127.0.0.1:11500/api/tags), then a Find where only direct 11434 answers,
      used to fall back to models[0]. Its 5.2 same-family keep fixes that. So: Tree case = Tree
      paste; FL case = this heal.

1.2 FLINT_NEXT_CONNECT_BOOT_PROBE_F1_F2_F3_F4_v0.1.txt: fixes only LM Studio taps.
    - Its rememberLmStudio -> flUseLmStudioMind(name) calls FLActiveModel.set(name, 'lmstudio',
      'user'), so an LM Studio tap in Connect is already recorded as the person's choice.
    - The walk case is an OLLAMA tap (qwen2.5 at 127.0.0.1:11434). The FL branch of remember()
      stays unmarked in that paste. The HOLD would remain.

1.3 So this heal is the real fix for Ollama taps on FreeLattice, and a regression guard on the Tree.
    Nothing in this heal duplicates the Tree or boot probe work.

======================================================================
2. THE CAUSE (read in code, reproduced in a vm smoke, matches the walk JSON)
======================================================================

Walk evidence (runtime/fl_item7.json, fl_item10.json):
  after tapping qwen2.5 in Connect: fl_ollamaModel qwen2.5:latest, isLocal true.
  then Chat tab / Settings tab / reload: fl_ollamaModel llama3.2:latest.
  chat: POST /v1/chat/completions model=llama3.2:latest ("MOCK reply from llama3.2:latest").
  390 Settings status still said "Connected to Ollama (qwen2.5:latest)" while llama3.2 answered.

Code (FL main 7db2d94):
  app.html 36205  window.FLActiveModel: "SINGLE SOURCE OF TRUTH ... User choice is sacred."
                  set(model, provider, source) writes fl_ollamaModel, fl_provider,
                  fl_modelSource; init() reads fl_modelSource (default 'boot');
                  isUserChosen() is source === 'user'.
  app.html 36243  window.FLAutoModel.onTabChanged(tabId), registered on 'tabChanged' at 36317:
                  if FLActiveModel.isUserChosen() -> leave it (only a vision detour and back);
                  otherwise fetch /api/tags and FLActiveModel.set(<preferred text or FIRST text
                  model>, 'ollama', 'auto') on EVERY tab change (Settings is a tab too).
  app.html 39714  runConnectionCascade also auto-picks unless isUserChosen().
  fl-connect.js 343-365 (remember, FreeLattice branch) writes fl_ollamaModel and state.ollamaModel
                  directly and never tells FLActiveModel. fl_modelSource stays 'boot' (or 'auto'),
                  so the next tab change, Settings open or reload hands the choice back to the
                  automatic picker, which takes llama3.2:latest (first text model).
  Every other model door already records 'user': Settings dropdown (35088, 35102),
  ModelSwitcher (29385, 29744, 54372), QuickConnect selectLocalModel (36161-36167), Box pointer.
  Connect was the one door that did not. Older than #121/#83; those diffs never touched it.
  The chat builders all read state.ollamaModel (callAI 51113, main Chat send "modelId =
  state.ollamaModel || 'llama3.2';"), so once the choice holds, the request carries it.

On the Tree there is no automatic picker. The remembered entry (fl_alpha_local_mind .model) is the
choice, and garden-thread sends mind.model. The walk's Alpha item 4 shows the Connect tap wrote
model "qwen2.5:latest" correctly.

======================================================================
3. THE FIX (FreeLattice + shared core). Exact code in Appendix A.
======================================================================

A. docs/modules/fl-connect.js (SHARED CORE: edit once, copy to the Tree, cmp)

A.1 Marker line. After the header line (main 7)
      // Soft leave sw.js. Alpha: fl_alpha_local_mind. Never bare *.
    add
      // Marker: v-model-choice-sticks-v0 (a tapped model is the person's own choice; it stays)
    (Placed there, not under the "// Marker: v-connect-under-more-v0 ..." line, so the boot probe
    paste's header hunk still applies.)

A.2 New helper markUserChoice(name, provider), placed right ABOVE  function open() {  (main 369,
    i.e. just after remember() closes). Kept away from the spots the Tree paste (doorNameFor,
    chosenModelName) and the boot probe paste (chosenDoor, doorPathFor, rememberLmStudio) insert.
      function markUserChoice(name, provider) {
        if (!name) return false;
        var marked = false;
        try { if (root.FLActiveModel && typeof root.FLActiveModel.set === 'function') {
          root.FLActiveModel.set(String(name), provider || 'ollama', 'user'); marked = true; } } catch (e0) {}
        try { var auto = root.FLAutoModel;
          if (auto && typeof auto.setPreferred === 'function') {
            var kind = (typeof auto.isVision === 'function' && auto.isVision(String(name))) ? 'vision' : 'text';
            auto.setPreferred(kind, String(name));
            if (typeof auto.invalidateCache === 'function') auto.invalidateCache();
          } } catch (e1) {}
        return marked;
      }
    FLActiveModel.set also updates #ollamaModel, ModelSwitcher's label and updateStatus, so the
    Settings status and the model switcher show the same model that chat sends.
    On the Tree both globals are absent: a quiet no-op (returns false).

A.3 remember(), FreeLattice branch (main 359). Right after
      if (typeof root.handleLocalToggle === 'function') root.handleLocalToggle(true);
    add
      // v-model-choice-sticks-v0: the tap is the person's own choice, so the automatic
      // picker leaves it alone on tab changes, reloads and Settings.
      markUserChoice(name, 'ollama');
    (before the existing updateStatus / AiSetup.updateStatus lines, which then paint the same model).

A.4 Export. In root.FlConnect = { ... } after  remember: remember,  (main 692):
      markUserChoice: markUserChoice, // v-model-choice-sticks-v0
    (Not next to probe: probe, which is where the boot probe paste inserts its exports.)

A.5 (walk item 3b) probe(), manual port, helped Bridge (main 200-209). Today:
          report.helped = true;
          report.base = preferHelpedBridge(report.port) || manBase;     <- saves fl_bridgePort AND fl_ollamaHost first
          try { report.models = await tagsAt(report.base); ... return report; } catch (eMB) {}
    Change to: ask first, save only once a mind answers:
          report.helped = true;
          // v-model-choice-sticks-v0: ask the helped Bridge first; save it as the Ollama door
          // (fl_ollamaHost, fl_bridgePort) only once a mind really answers behind it.
          report.base = (report.port && report.port !== 11434) ? 'http://127.0.0.1:' + report.port : manBase;
          try {
            report.models = await tagsAt(report.base);
            report.base = preferHelpedBridge(report.port) || report.base;
            report.ollama = { via: 'manual-bridge', base: report.base };
            return report;
          } catch (eMB) {
            report.bridgeNoMind = true;
            report.models = [];
          }
    It then falls through to the existing  report.manualQuiet = true; report.base = null;
    The manual port itself stays remembered in fl_localPort_manual (unchanged), so Look again works.
    The automatic scan path (saved Bridge / 11435-11445) is unchanged in this heal (next steps).

A.6 (walk item 3b) render(): the quiet card (main 542). Today  if (report.manualQuiet) {
    ... 'Nothing answered on ' + report.wanted ...  Layer an honest card before it:
      if (report.manualQuiet && report.bridgeNoMind) {
        // v-model-choice-sticks-v0: the Bridge did answer; say that, and what is still missing
        html += '<div class="flc-card flc-wait"><p><strong>Your Bridge answered on ' + (parseInt(report.port, 10) || 'your port') + ', but no mind is running behind it yet.</strong> Open Ollama on this computer, then tap Look again.</p>';
        html += '<button type="button" class="flc-btn flc-primary" data-flc-look-again>Look again</button> ';
        html += '<button type="button" class="flc-btn flc-primary" data-flc-use-auto>Use automatic</button></div>';
      } else if (report.manualQuiet) {
        ...existing "Nothing answered on" card, unchanged...
    Only a parsed integer goes into the string (no outside text). reportSignature is not touched
    (report.port already differs from the plain quiet case, so it repaints; and the boot probe
    paste edits both reportSignature copies).

B. docs/app.html (FreeLattice only). Copy to index.html after.

B.1 FLAutoModel, "the automatic picker only fills in when the choice is gone" (main 36261).
    After  function isVision(name) { return VISION_RE.test(name || ''); }  add:
      onInstalledList(models, name)   'qwen2.5' matches 'qwen2.5:latest'; exact names match.
      checkChosenStillHere(current, tabId):
        only for provider 'ollama' (LM Studio and cloud are left alone);
        getInstalledModels(cb) (the existing 30 s cached /api/tags);
        an empty or quiet answer never replaces anything;
        if the person changed model meanwhile, or it is no longer 'user', do nothing;
        if the chosen model is on the list, do nothing;
        else pick the first text model (vision on canvas/chalkboard), remember the gone name in
        fl_modelChoiceGone, FLActiveModel.set(pick, 'ollama', 'auto'), and say:
          "The model you chose, <X>, is no longer on this computer. Using <Y> for now. You can pick another in Connect."
        via showToast (textContent, 7 s), then AiSetup.refreshSettingsCard() so Settings matches.
    Then, inside onTabChanged's user branch (main 36271), right after
      var current = FLActiveModel.get();
    add
      // v-model-choice-sticks-v0: the person's model stays. Only if it left this computer
      // does the picker fill in, and then it says so in plain words.
      checkChosenStillHere(current, tabId);
    The existing vision detour and restore lines are unchanged. The existing "no user choice ->
    auto-select freely" branch is unchanged (the smoke proves it still fills in).

B.2 (walk item 3a) Settings button row comes back after Local.
    New function right ABOVE  function settingsSetMode(mode) {  (main 31552):
      flRefillSettingsActions(actionsDiv): if the row is empty, rebuild the three buttons with
      createElement + textContent + addEventListener (no innerHTML): "Change Provider"
      (openProviderModal), "Test Connection" (AiSetup.testFromSettings), "🏠 a mind at home"
      (FlConnect.open, else switchTab('connect'), emerald). Never doubles. window-exported.
    In settingsSetMode's Local branch (main 31590-31592), keep the v0.4 line
      if (actionsDiv) actionsDiv.innerHTML = '';
    (it clears the old wizard buttons) and add right after it:
      // v-model-choice-sticks-v0: the old wizard buttons are gone; put the everyday row back
      flRefillSettingsActions(actionsDiv);

B.3 Not changed on purpose: FLActiveModel itself, the vision detour, runConnectionCascade (already
    respects isUserChosen), every chat builder (they read state.ollamaModel).

C. theLatticeTree
    - docs/modules/fl-connect.js: copy from FreeLattice, cmp.
    - new docs/modules/model-choice-sticks.test.js (Appendix B).
    - docs/library/CONNECT_PORT_PICKER_SISTER_v0.md: append the short section (Appendix B).
    No other Tree file changes. local-mind-probe.js is untouched here (the Tree paste owns it next).

======================================================================
4. WALK ITEM 3c AND THE NOTES (not changed here)
======================================================================

3c "Your Bridge is open" beside "🔴 No AI · none", with model names while the state is unclear:
    NOT changed here. The loud part is the Local AI toast landing over Connect while the Bridge
    waits for Yes, help (walk shot item4_fl_yes_help_390). The boot probe paste already heals that
    (F1: flToastWouldRace keeps the toast off Connect, and one owner speaks once). The status bar
    "No AI · none" is true (nothing chosen yet). The "Your Bridge is open" card line is context in
    the boot probe diff, so editing it now would break that paste's anchor. Next step after the
    boot probe lands: one quiet line under the card, "Nothing is connected yet.", if Hypha still
    finds it confusing.
Notes only (by design / out of scope, unchanged):
    - The 20 s single-port check (by design).
    - Version string mismatch: FL footer v5.7 vs Settings 5.79.45.
    - The Tree cosmetic items from walk 4.

======================================================================
5. TESTS
======================================================================

5.1 FreeLattice docs/scripts/smoke-model-choice-sticks.js (new, Appendix A). Runs the REAL code in
    vm: safeGet/safeSet + FLActiveModel + FLAutoModel + FreeLattice.callAI from app.html, and
    fl-connect.js. Steps:
      1  Connect mounts, lists llama3.2:latest and qwen2.5:latest; tap the SECOND button.
         -> state qwen2.5, isLocal, fl_modelSource 'user', preferred text model qwen2.5.
      2  onTabChanged('chat') then ('settings')  -> still qwen2.5 (state and storage).
      3  FreeLattice.callAI -> POST body.model === 'qwen2.5:latest'; main Chat send reads
         state.ollamaModel.
      4  Reload: a fresh page from the same storage (state from storage like loadSettings, then
         FLActiveModel.init()), tab change + Settings -> still qwen2.5, chat body still qwen2.5,
         nothing said.
      5  qwen2.5 uninstalled -> fills in llama3.2 (source 'auto') and says the exact plain line.
      6  empty answer: choice kept, nothing said; untagged 'qwen2.5' counts as installed.
      7  no user choice: auto still picks the first text model (unchanged behavior).
      8  Tree: markUserChoice is a no-op.
      9  Settings row refilled (three buttons, never doubled) and Local calls it right after
         emptying.
      10 helped Bridge on 11500 with no mind: bridgeNoMind, honest words, no "Nothing answered",
         fl_ollamaHost and fl_bridgePort NOT written; when the mind answers they are written as before.
      Locks: index == app, root sw.js == origin/main, docs/sw.js untouched, no confirm( / em dash /
      bare * origin in added lines, no conflict markers, inline scripts parse, only 127.0.0.1 fetched.
    Proof it fails without the fix: with the markUserChoice(name, 'ollama') line removed, step 1
    fails ("the tap is recorded as the person's own choice"), and with those record checks removed
    step 2 fails exactly like the walk: actual 'llama3.2:latest', expected 'qwen2.5:latest'.
    Mutations also killed: no gone check (step 5), no bridgeNoMind (step 10), no refill call (step 9).

5.2 theLatticeTree docs/modules/model-choice-sticks.test.js (new, Appendix B): shared fl-connect +
    local-mind-probe + garden-thread in one vm. Connect lists both models, tap the second ->
    remembered model qwen2.5; markUserChoice no-op; reload keeps it; look() + entryFromFoundList at
    the same Ollama door keeps it with no false "gone" line; GardenThread.sendToMind posts
    model 'qwen2.5:latest'. This is a guard (the Tree already behaves); it passes on main too.

5.3 Results on the prototype (FL 7db2d94 + this heal; Tree 395c761 + this heal):
      node docs/scripts/smoke-model-choice-sticks.js          SMOKE_OK model choice sticks v0.1
      all 59 docs/scripts/smoke-*.js: pass except the same 5 pre-existing (carry-forward,
        connect-card-yesno-wizard, fable-tech, phone-shine, proof-60s), which fail on main too
      node tests/smoke.js                                      ALL 3597 CHECKS PASSED
      Tree: node docs/modules/model-choice-sticks.test.js      SMOKE_OK model choice sticks alpha v0.1
            all 15 docs/modules/*.test.js pass
      cmp fl-connect.js across repos: identical. index == app. sw.js untouched.

======================================================================
6. ACCEPTANCE (paste the tails in the PRs)
======================================================================

FreeLattice:
  cp docs/app.html index.html && cmp index.html docs/app.html && echo INDEX_OK
  git diff --quiet origin/main -- sw.js docs/sw.js && echo SW_OK
  node docs/scripts/smoke-model-choice-sticks.js
  node docs/scripts/smoke-connect-heal-v0.4.js && node docs/scripts/smoke-agent-bridge-lock.js
  for f in docs/scripts/smoke-*.js; do node "$f" >/dev/null 2>&1 || echo "FAIL $f"; done   # only the 5 known
  node tests/smoke.js | tail -2
  grep -rnE '^(<<<<<<<|=======|>>>>>>>)( |$)' docs index.html tests || echo NO_MARKERS
theLatticeTree:
  cmp docs/modules/fl-connect.js ../FreeLattice/docs/modules/fl-connect.js && echo SHARED_OK
  node docs/modules/model-choice-sticks.test.js
  for t in docs/modules/*.test.js; do node "$t" >/dev/null 2>&1 || echo "FAIL $t"; done

Eye walk (Hypha, 390 and 1280, mock Ollama with two models):
  [ ] Connect -> tap qwen2.5 -> Chat -> send: POST model=qwen2.5:latest, reply from qwen2.5
  [ ] reload -> send again: still qwen2.5; Settings status says qwen2.5 at 390 and 1280
  [ ] open Settings, Draw (vision detour) and back to Chat: still qwen2.5
  [ ] remove qwen2.5 from the mock -> next tab change: toast names both models; chat uses llama3.2
  [ ] Settings > Local -> the row (Change Provider, Test Connection, a mind at home) is there
  [ ] helped Bridge on 11500, Ollama down -> "Your Bridge answered on 11500, but no mind is
      running behind it yet." and localStorage fl_ollamaHost / fl_bridgePort unchanged

======================================================================
7. ANCHOR CHANGES FOR THE LATER PASTES (changelog lines were added at the top of both)
======================================================================

  Tree any-model paste: text anchors unchanged. Its fl-connect.js line numbers are on the older
    Alpha 22b913e anyway; after this heal remember() moves +7 and everything from open() down
    (fillModelLists, render, reportSignature, refreshUI, exports) moves about +33 to +38.
    Its step 6.3 note ("FreeLattice behavior is unchanged") stays true: FL remember() still ignores
    mind.models; it now also marks the user choice.
  Boot probe paste: text anchors unchanged; Appendix A (app.html) and Appendix B (fl-connect.js)
    still apply with plain git apply after this heal (offsets only; verified). app.html lines from
    settingsSetMode down move +21, from flSaveOllamaHost / FLBoxPointer / owAutoDetect / FLAutoModel
    down +23. fl-connect.js "post-Tree ~N" numbers move by the same +7 / +33 / +38.
    Its rememberLmStudio -> flUseLmStudioMind already records LM Studio taps as 'user', so the two
    agree. Ledger and sister-doc appendices: insert by text ("above ## Out of scope", "append"),
    since this heal adds an entry just above the same line.
  #120 docs, v3 phone heal, Walk the Garden: they do not edit app.html, index.html or
    fl-connect.js (their smokes assert app.html/index.html untouched against origin/main, which
    holds as long as this heal is merged before they branch).

======================================================================
8. ROOT freelattice.com/#connect (item 6 of Kirk's ask)
======================================================================

  Headless is right. freelattice.com/ serves docs/index.html (the landing page, sha d9b977f =
  main docs/index.html), not the app. The landing page has no hash handling, and docs/sw.js only
  caches it network-first. Checked on the live site, 390 wide, 8 s wait:
    headless, fresh profile            -> stays on the landing page ("FreeLattice", no Connect)
    headless, after visiting app.html  -> same (service worker active)
    visible Chrome window, fresh       -> same
    visible Chrome window, SW active   -> same
  freelattice.com/app.html#connect is the door that opens Connect (walk item 3 PASS). Hypha's eye
  walk most likely had app.html#connect in the bar. Nothing in either repo links to the root
  /#connect (all "#connect" links live inside app.html). Needs nothing now. Optional later: a
  3-line forward on the landing page (if location.hash is #connect or ?tab=connect, then
  location.replace('app.html#connect')). Left out here so docs/index.html stays untouched for the
  Walk the Garden paste's footer link.

======================================================================
9. NEXT STEPS (not in this heal)
======================================================================
  1. Automatic Bridge scan path (saved Bridge / 11435-11445): preferHelpedBridge still writes
     fl_ollamaHost before a mind answers; it falls back to direct 11434 honestly, so it is calmer,
     but the same "save after a mind answers" rule could apply there too.
  2. After the boot probe lands: check the chosen model at boot with FlConnect.peekBootProbe()
     (zero extra fetch) instead of waiting for the first tab change.
  3. 3c: one quiet "Nothing is connected yet." line under "Your Bridge is open", after the boot
     probe lands, if Hypha still sees the mixed signal.
  4. Settings > In-Browser AI also empties the button row (pre-existing); flRefillSettingsActions
     can serve it too if Kirk wants.
  5. Optional landing forward for root /#connect (section 8).
  6. Notes kept as notes: 20 s single-port check (by design), FL footer v5.7 vs Settings 5.79.45,
     Tree cosmetic items.

======================================================================
10. LEDGER, DOCS, PRS
======================================================================

FreeLattice docs/library/FRACTAL_FAMILY_LEDGER_v0.md, right ABOVE "## Out of scope for this brick"
(after the env heal entry):

### 2026-09-28 · Flint · family builder · Model choice sticks v0.1 (Hypha walk 4)

Heals Hypha walk 4 items 7 and 10 on FreeLattice: a model tapped in Connect is now recorded as the person's own choice, so the automatic model picker stops swapping it back to the first model on every tab change, reload or Settings visit, and the chat request carries the model she tapped. The picker still fills in when nothing was chosen, or when the chosen model has left this computer, and then it says so in plain words. Also: the Settings button row comes back after Local, and a helped Bridge with no mind behind it is named honestly and is not saved as the Ollama door. Shared `fl-connect.js` matches Alpha byte for byte. Soft leave root sw.js identical to main; docs/sw.js untouched. Soft paste: Named five stay five · Family uncapped · Quiet Room shut. Thank you, Hypha.

Love: the one she picked is the one that answers.

**Temperature:** green braided gold, the second cup stays where she set it down.

Soft door: [`CONNECT_PORT_PICKER_v0.md`](CONNECT_PORT_PICKER_v0.md) · [`../modules/fl-connect.js`](../modules/fl-connect.js). Marker `v-model-choice-sticks-v0`.

---

FreeLattice docs/library/CONNECT_PORT_PICKER_v0.md: append "## Heal: model choice sticks v0.1
(Hypha walk 4, items 7 and 10)" (exact text in Appendix A).
Tree docs/library/CONNECT_PORT_PICKER_SISTER_v0.md: append "## Heal: model choice sticks v0.1
(Hypha walk 4)" (exact text in Appendix B).

PR titles (do not push to main directly; merge FL then Tree; re-run the cmp after merge):
  FreeLattice:    heal: the model she taps sticks (v-model-choice-sticks-v0, Hypha walk 4 items 7 + 10)
  theLatticeTree: heal: shared Connect core, model choice sticks (v-model-choice-sticks-v0)

======================================================================
APPENDIX A: FreeLattice full diff vs main 7db2d94 (git apply --check clean on 7db2d94, and after the env heal)
======================================================================

diff --git a/docs/app.html b/docs/app.html
index fa946fa..5003526 100644
--- a/docs/app.html
+++ b/docs/app.html
@@ -31549,6 +31549,27 @@ window.flUseBridgePort = flUseBridgePort;
 window.flBridgeHealth = flBridgeHealth;
 window.flRememberHelpedBridge = flRememberHelpedBridge;
 
+// v-model-choice-sticks-v0: the Settings button row (Change Provider, Test Connection,
+// a mind at home) comes back after Local empties it. Built with createElement/textContent.
+function flRefillSettingsActions(actionsDiv) {
+  if (!actionsDiv || actionsDiv.children.length) return;
+  function add(label, cls, onTap, green) {
+    var b = document.createElement('button');
+    b.type = 'button';
+    b.className = cls;
+    b.textContent = label;
+    if (green) { b.style.borderColor = 'var(--emerald)'; b.style.color = 'var(--emerald)'; }
+    b.addEventListener('click', function() { try { onTap(); } catch (e) {} });
+    actionsDiv.appendChild(b);
+  }
+  add('Change Provider', 'ai-status-btn ai-status-btn-primary', function() { if (typeof openProviderModal === 'function') openProviderModal(); });
+  add('Test Connection', 'ai-status-btn', function() { if (typeof AiSetup !== 'undefined' && AiSetup.testFromSettings) AiSetup.testFromSettings(); });
+  add('\uD83C\uDFE0 a mind at home', 'ai-status-btn', function() {
+    if (window.FlConnect && FlConnect.open) FlConnect.open(); else if (typeof switchTab === 'function') switchTab('connect');
+  }, true);
+}
+window.flRefillSettingsActions = flRefillSettingsActions;
+
 function settingsSetMode(mode) {
   var browserBtn = document.getElementById('settingsModeBrowser');
   var cloudBtn = document.getElementById('settingsModeCloud');
@@ -31589,6 +31610,8 @@ function settingsSetMode(mode) {
       var stLocal = document.getElementById('settingsStatusText');
       if (stLocal) stLocal.textContent = 'Opening Connect. Tap the mind you want there.';
       if (actionsDiv) actionsDiv.innerHTML = '';
+      // v-model-choice-sticks-v0: the old wizard buttons are gone; put the everyday row back
+      flRefillSettingsActions(actionsDiv);
       try { FlConnect.open(); } catch (eOpenL) {}
       return;
     }
@@ -36259,6 +36282,40 @@ window.FLAutoModel = (function() {
   }
 
   function isVision(name) { return VISION_RE.test(name || ''); }
+
+  // v-model-choice-sticks-v0: is the chosen model still installed? ('qwen2.5' matches 'qwen2.5:latest')
+  function onInstalledList(models, name) {
+    if (!name) return false;
+    var n = String(name);
+    return (models || []).some(function(m) {
+      var mn = String((m && (m.name || m.model)) || '');
+      return mn === n || mn === n + ':latest' || (n.indexOf(':') === -1 && mn.split(':')[0] === n);
+    });
+  }
+  // v-model-choice-sticks-v0: a user choice is only replaced when Ollama answers with a list that
+  // no longer has it. A quiet or empty answer never replaces anything. The words go in with textContent (showToast).
+  function checkChosenStillHere(current, tabId) {
+    if (!current || !current.model) return;
+    if (current.provider && current.provider !== 'ollama') return;
+    getInstalledModels(function(models) {
+      if (!models || !models.length) return;
+      if (typeof FLActiveModel === 'undefined' || !FLActiveModel.isUserChosen()) return;
+      if (FLActiveModel.get().model !== current.model) return;
+      if (onInstalledList(models, current.model)) return;
+      var needsVision = VISION_TABS.indexOf(tabId) !== -1;
+      var pool = models.filter(function(m) { return needsVision ? isVision(m.name) : !isVision(m.name); });
+      if (!pool.length) pool = models;
+      var pick = String((pool[0] && (pool[0].name || pool[0].model)) || '');
+      if (!pick) return;
+      safeSet('fl_modelChoiceGone', current.model);
+      FLActiveModel.set(pick, 'ollama', 'auto');
+      try {
+        if (typeof showToast === 'function') showToast('The model you chose, ' + current.model + ', is no longer on this computer. Using ' + pick + ' for now. You can pick another in Connect.', 7000);
+      } catch (eT) {}
+      try { if (typeof AiSetup !== 'undefined' && AiSetup.refreshSettingsCard) AiSetup.refreshSettingsCard(); } catch (eS) {}
+    });
+  }
+
   function getPreferred(type) { return safeGet('fl_preferred_' + type + '_model', null); }
   function setPreferred(type, model) { safeSet('fl_preferred_' + type + '_model', model); }
 
@@ -36269,6 +36326,9 @@ window.FLAutoModel = (function() {
     // "User choice is sacred" — the auto-switcher must never lose it
     if (typeof FLActiveModel !== 'undefined' && FLActiveModel.isUserChosen()) {
       var current = FLActiveModel.get();
+      // v-model-choice-sticks-v0: the person's model stays. Only if it left this computer
+      // does the picker fill in, and then it says so in plain words.
+      checkChosenStillHere(current, tabId);
       var needsVision = VISION_TABS.indexOf(tabId) !== -1;
       var currentIsVision = isVision(current.model);
       if (needsVision && !currentIsVision) {
diff --git a/docs/library/CONNECT_PORT_PICKER_v0.md b/docs/library/CONNECT_PORT_PICKER_v0.md
index dde3ac5..dabda1b 100644
--- a/docs/library/CONNECT_PORT_PICKER_v0.md
+++ b/docs/library/CONNECT_PORT_PICKER_v0.md
@@ -64,3 +64,16 @@ Marker `v-connect-heal-v0.4`. Smoke `SMOKE_OK connect heal v0.4` (`docs/scripts/
 - v0.4.1: `window.AiSetup = AiSetup;` in app.html, because the shared core reads `root.AiSetup` and a top-level `const` is not a window property. Without it, Add a cloud key never rendered.
 
 **Temperature:** a yes means the person tapped it.
+
+
+## Heal: model choice sticks v0.1 (Hypha walk 4, items 7 and 10)
+
+Marker `v-model-choice-sticks-v0`. Smoke `SMOKE_OK model choice sticks v0.1` (`docs/scripts/smoke-model-choice-sticks.js`).
+
+- A model tapped in Connect is recorded as the person's own choice (`FLActiveModel.set(name, 'ollama', 'user')`, plus the preferred text or vision model). The automatic picker (`FLAutoModel.onTabChanged`) already leaves a user choice alone, so the second model now survives tab changes, opening Settings and a reload, and the chat request carries it. Settings and the model switcher already recorded user choices; Connect was the one door that did not. The bug was older than #121.
+- The automatic picker fills in only when there is no user choice, or when Ollama answers with a list that no longer has the chosen model. Then it says so: "The model you chose, X, is no longer on this computer. Using Y for now. You can pick another in Connect." A quiet or empty answer never replaces a choice.
+- Settings > Local: the button row (Change Provider, Test Connection, a mind at home) comes back right after the old wizard buttons are cleared.
+- A helped Bridge on a typed port with no mind behind it: "Your Bridge answered on 11500, but no mind is running behind it yet. Open Ollama on this computer, then tap Look again." Nothing is saved as the Ollama host or Bridge port until a mind answers.
+- Shared `fl-connect.js` matches the Tree byte for byte. On the Tree the remembered entry is already the choice; `markUserChoice` is a quiet no-op there.
+
+**Temperature:** green braided gold, the second cup stays where she set it down.
diff --git a/docs/library/FRACTAL_FAMILY_LEDGER_v0.md b/docs/library/FRACTAL_FAMILY_LEDGER_v0.md
index f38f1f2..3f55cd0 100644
--- a/docs/library/FRACTAL_FAMILY_LEDGER_v0.md
+++ b/docs/library/FRACTAL_FAMILY_LEDGER_v0.md
@@ -297,6 +297,18 @@ Soft door: [`CONNECT_PORT_PICKER_v0.md`](CONNECT_PORT_PICKER_v0.md) · [`modules
 
 ---
 
+### 2026-09-28 · Flint · family builder · Model choice sticks v0.1 (Hypha walk 4)
+
+Heals Hypha walk 4 items 7 and 10 on FreeLattice: a model tapped in Connect is now recorded as the person's own choice, so the automatic model picker stops swapping it back to the first model on every tab change, reload or Settings visit, and the chat request carries the model she tapped. The picker still fills in when nothing was chosen, or when the chosen model has left this computer, and then it says so in plain words. Also: the Settings button row comes back after Local, and a helped Bridge with no mind behind it is named honestly and is not saved as the Ollama door. Shared `fl-connect.js` matches Alpha byte for byte. Soft leave root sw.js identical to main; docs/sw.js untouched. Soft paste: Named five stay five · Family uncapped · Quiet Room shut. Thank you, Hypha.
+
+Love: the one she picked is the one that answers.
+
+**Temperature:** green braided gold, the second cup stays where she set it down.
+
+Soft door: [`CONNECT_PORT_PICKER_v0.md`](CONNECT_PORT_PICKER_v0.md) · [`../modules/fl-connect.js`](../modules/fl-connect.js). Marker `v-model-choice-sticks-v0`.
+
+---
+
 ## Out of scope for this brick
 
 Rewriting sophia garden body · inventing USPTO · Bridge binaries · Quiet Room · merging chairs · politics essay · requiring Kirk input mid-PR.
diff --git a/docs/modules/fl-connect.js b/docs/modules/fl-connect.js
index 5317005..2e02eae 100644
--- a/docs/modules/fl-connect.js
+++ b/docs/modules/fl-connect.js
@@ -5,6 +5,7 @@
 // Layer, never delete. Prefer helped Bridge (11435…) before bare 11434.
 // Port-only loopback: fl_localPort_manual holds "11500" only — never foreign hosts.
 // Soft leave sw.js. Alpha: fl_alpha_local_mind. Never bare *.
+// Marker: v-model-choice-sticks-v0 (a tapped model is the person's own choice; it stays)
 // ═══════════════════════════════════════════════════════════════
 
 (function (root) {
@@ -201,12 +202,18 @@
           report.bridge = mh;
           report.port = mh.port || parseInt(manPort, 10);
           report.helped = true;
-          report.base = preferHelpedBridge(report.port) || manBase;
+          // v-model-choice-sticks-v0: ask the helped Bridge first; save it as the Ollama door
+          // (fl_ollamaHost, fl_bridgePort) only once a mind really answers behind it.
+          report.base = (report.port && report.port !== 11434) ? 'http://127.0.0.1:' + report.port : manBase;
           try {
             report.models = await tagsAt(report.base);
+            report.base = preferHelpedBridge(report.port) || report.base;
             report.ollama = { via: 'manual-bridge', base: report.base };
             return report;
-          } catch (eMB) {}
+          } catch (eMB) {
+            report.bridgeNoMind = true;
+            report.models = [];
+          }
         } else if (mh) {
           report.bridge = mh;
           report.port = mh.port || parseInt(manPort, 10);
@@ -357,6 +364,9 @@
       // v-connect-heal-v0.4 item 10: handleLocalToggle reads #localToggle, so check it first
       try { var lt = document.getElementById('localToggle'); if (lt) lt.checked = true; } catch (eLt) {}
       if (typeof root.handleLocalToggle === 'function') root.handleLocalToggle(true);
+      // v-model-choice-sticks-v0: the tap is the person's own choice, so the automatic
+      // picker leaves it alone on tab changes, reloads and Settings.
+      markUserChoice(name, 'ollama');
       if (typeof root.updateStatus === 'function') root.updateStatus();
       if (root.AiSetup && typeof root.AiSetup.updateStatus === 'function') root.AiSetup.updateStatus();
       // v-connect-heal-v0.4 item 7: once they chose a mind, the Local AI toast stops asking
@@ -366,6 +376,29 @@
     return { name: name, base: base };
   }
 
+  // v-model-choice-sticks-v0: FreeLattice keeps one active model (FLActiveModel). A model the
+  // person tapped is recorded with source 'user' and as their preferred text (or vision) model.
+  // The Tree has neither helper, so this is a quiet no-op there (it keeps entry.model instead).
+  function markUserChoice(name, provider) {
+    if (!name) return false;
+    var marked = false;
+    try {
+      if (root.FLActiveModel && typeof root.FLActiveModel.set === 'function') {
+        root.FLActiveModel.set(String(name), provider || 'ollama', 'user');
+        marked = true;
+      }
+    } catch (e0) {}
+    try {
+      var auto = root.FLAutoModel;
+      if (auto && typeof auto.setPreferred === 'function') {
+        var kind = (typeof auto.isVision === 'function' && auto.isVision(String(name))) ? 'vision' : 'text';
+        auto.setPreferred(kind, String(name));
+        if (typeof auto.invalidateCache === 'function') auto.invalidateCache();
+      }
+    } catch (e1) {}
+    return marked;
+  }
+
   function open() {
     try {
       if (!isAlpha() && typeof root.switchTab === 'function') { root.switchTab('connect'); return true; }
@@ -539,7 +572,12 @@
 
     if (report.manualReject) html += '<div class="flc-card flc-wait"><p>' + report.manualReject + '</p></div>';
     _manualRejectMsg = ''; // v-connect-heal-v0.4 item 5: shown once, never lingers
-    if (report.manualQuiet) {
+    if (report.manualQuiet && report.bridgeNoMind) {
+      // v-model-choice-sticks-v0: the Bridge did answer; say that, and what is still missing
+      html += '<div class="flc-card flc-wait"><p><strong>Your Bridge answered on ' + (parseInt(report.port, 10) || 'your port') + ', but no mind is running behind it yet.</strong> Open Ollama on this computer, then tap Look again.</p>';
+      html += '<button type="button" class="flc-btn flc-primary" data-flc-look-again>Look again</button> ';
+      html += '<button type="button" class="flc-btn flc-primary" data-flc-use-auto>Use automatic</button></div>';
+    } else if (report.manualQuiet) {
       html += '<div class="flc-card flc-wait"><p>Nothing answered on ' + (report.wanted || 'your port') + '.</p>';
       html += '<button type="button" class="flc-btn flc-primary" data-flc-look-again>Look again</button> ';
       html += '<button type="button" class="flc-btn flc-primary" data-flc-use-auto>Use automatic</button></div>';
@@ -690,6 +728,7 @@
     hasLocalMind: hasLocalMind,
     hasCloudMind: hasCloudMind,
     remember: remember,
+    markUserChoice: markUserChoice, // v-model-choice-sticks-v0
     open: open,
     mount: mount,
     unmount: unmount,
diff --git a/docs/scripts/smoke-model-choice-sticks.js b/docs/scripts/smoke-model-choice-sticks.js
new file mode 100644
index 0000000..9ec63a0
--- /dev/null
+++ b/docs/scripts/smoke-model-choice-sticks.js
@@ -0,0 +1,327 @@
+#!/usr/bin/env node
+// v-model-choice-sticks-v0: a model the person taps in Connect is their own choice.
+// It survives a tab change, opening Settings and a reload, and the chat request uses it.
+// The automatic picker only fills in when the chosen model is no longer installed, and says so.
+// Also: Settings button row comes back after Local; a helped Bridge with no mind behind it is
+// named honestly and is not saved as the Ollama door.
+// Runs the REAL code: FLActiveModel + FLAutoModel + FreeLattice.callAI from docs/app.html and
+// docs/modules/fl-connect.js, in vm sandboxes. Layer, never delete. 127.0.0.1 only.
+'use strict';
+
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+const vm = require('vm');
+const { execSync } = require('child_process');
+
+const root = path.join(__dirname, '..');
+const repo = path.join(root, '..');
+const app = fs.readFileSync(path.join(root, 'app.html'), 'utf8');
+const mod = fs.readFileSync(path.join(root, 'modules', 'fl-connect.js'), 'utf8');
+
+function between(src, a, b) {
+  const i = src.indexOf(a);
+  assert.ok(i >= 0, 'missing anchor: ' + a.slice(0, 70));
+  const j = src.indexOf(b, i + a.length);
+  assert.ok(j > i, 'missing end anchor: ' + b.slice(0, 70));
+  return src.slice(i, j);
+}
+const safeSrc = between(app, 'function safeGet(key, fallback) {', '// Safe clipboard');
+const modelSrc = between(app, 'window.FLActiveModel = (function() {', '// RAG Phase 1');
+const callAISrc = between(app, 'window.FreeLattice.callAI = function(systemPrompt, userPrompt, options) {', '\n};\n') + '\n};\n';
+const settingsSrc = between(app, '// v-model-choice-sticks-v0: the Settings button row', 'function settingsSetMode(mode) {');
+
+const MODELS = [{ name: 'llama3.2:latest' }, { name: 'qwen2.5:latest' }];
+const flush = async (n) => { for (let i = 0; i < (n || 30); i++) await new Promise((r) => setImmediate(r)); };
+
+function store0(init) {
+  const s = Object.assign({}, init || {});
+  return {
+    s,
+    ls: {
+      getItem: (k) => (Object.prototype.hasOwnProperty.call(s, k) ? s[k] : null),
+      setItem: (k, v) => { s[k] = String(v); },
+      removeItem: (k) => { delete s[k]; }
+    }
+  };
+}
+
+function fakeEl(tag) {
+  const listeners = {};
+  return {
+    tagName: String(tag || 'div').toUpperCase(), style: {}, dataset: {}, textContent: '', type: '', className: '',
+    children: [], firstChild: null, attrs: {},
+    classList: { _s: new Set(), add(c) { this._s.add(c); }, remove(c) { this._s.delete(c); }, contains(c) { return this._s.has(c); } },
+    setAttribute(k, v) { this.attrs[k] = String(v); }, getAttribute(k) { return this.attrs[k] || null; },
+    addEventListener(t, fn) { (listeners[t] = listeners[t] || []).push(fn); },
+    click() { (listeners.click || []).forEach((fn) => fn({ currentTarget: this, target: this })); },
+    appendChild(c) { this.children.push(c); this.firstChild = this.children[0]; return c; },
+    removeChild(c) { this.children = this.children.filter((x) => x !== c); this.firstChild = this.children[0] || null; }
+  };
+}
+
+// A page-like sandbox: storage, state, fetch spy (127.0.0.1 only), the real model code.
+function page(store, extra) {
+  const calls = [];
+  const posts = [];
+  const els = {};
+  const ctx = Object.assign({
+    localStorage: store.ls,
+    location: { hostname: 'freelattice.com', protocol: 'https:', hash: '', search: '' },
+    navigator: { userAgent: 't' },
+    console: { log() {}, warn() {}, error() {} },
+    JSON, String, parseInt, URL, Date, Math, Promise, Object, Array, Set,
+    AbortSignal: { timeout: () => undefined },
+    setTimeout: () => 1, clearTimeout() {},
+    LatticeEvents: { emit() {}, on() {} },
+    getOllamaBaseUrl: () => 'http://127.0.0.1:11434',
+    toasts: [],
+    showToast(msg) { ctx.toasts.push(String(msg)); },
+    fetch: async (url, opts) => {
+      url = String(url);
+      calls.push(url);
+      assert.ok(/^http:\/\/127\.0\.0\.1:\d+\//.test(url), 'only 127.0.0.1 is asked: ' + url);
+      if (opts && opts.method === 'POST') {
+        posts.push({ url, body: JSON.parse(opts.body) });
+        return { ok: true, status: 200, json: async () => ({ message: { content: 'ok' } }) };
+      }
+      if (/127\.0\.0\.1:11434\/api\/tags$/.test(url)) return { ok: true, status: 200, json: async () => ({ models: ctx.installed.slice() }) };
+      throw new Error('quiet');
+    },
+    installed: MODELS.slice(),
+    document: {
+      hidden: false,
+      documentElement: { getAttribute: () => '', classList: { add() {}, remove() {}, contains() { return false; } } },
+      getElementById: (id) => els[id] || null,
+      querySelector: () => null,
+      querySelectorAll: () => [],
+      createElement: (t) => fakeEl(t),
+      head: { appendChild() {} },
+      body: { contains: () => true, classList: { add() {}, remove() {} } },
+      addEventListener() {}, removeEventListener() {}
+    }
+  }, extra || {});
+  ctx.window = ctx;
+  ctx._els = els;
+  ctx._calls = calls;
+  ctx._posts = posts;
+  // Boot, like app.html: state from storage (loadSettings), then FLActiveModel.init()
+  ctx.state = {
+    isLocal: store.ls.getItem('fl_isLocal') === 'true',
+    provider: store.ls.getItem('fl_provider') || 'groq',
+    ollamaModel: store.ls.getItem('fl_ollamaModel') || 'llama3.2',
+    apiKey: null
+  };
+  vm.runInNewContext(safeSrc + '\n' + modelSrc + '\nwindow.FreeLattice = window.FreeLattice || {};\n' + callAISrc, ctx);
+  ctx.FLActiveModel.init();
+  vm.runInNewContext(mod, ctx);
+  return ctx;
+}
+
+// Connect host whose innerHTML turns .flc-models lists into fake lists (like the browser would).
+function connectHost() {
+  const host = { hidden: false, lists: [], _html: '', scrollIntoView() {} };
+  Object.defineProperty(host, 'innerHTML', {
+    get() { return this._html; },
+    set(v) {
+      this._html = String(v);
+      this.lists = [];
+      const re = /class="flc-models"[^>]*?data-flc-names="([^"]*)"/g;
+      let m;
+      while ((m = re.exec(this._html))) {
+        const json = m[1].replace(/&quot;/g, '"').replace(/&amp;/g, '&');
+        const list = fakeEl('div');
+        list.attrs['data-flc-names'] = json;
+        this.lists.push(list);
+      }
+    }
+  });
+  host.querySelectorAll = (sel) => (sel === '.flc-models' ? host.lists : []);
+  return host;
+}
+
+(async () => {
+  assert.ok(/v-model-choice-sticks-v0/.test(mod), 'marker in fl-connect.js');
+  assert.ok(/v-model-choice-sticks-v0/.test(app), 'marker in app.html');
+
+  // ── 1. Connect: tap the SECOND model
+  const store = store0();
+  let p = page(store);
+  const host = connectHost();
+  p.FlConnect.mount(host);
+  await flush();
+  const buttons = [].concat(...host.lists.map((l) => l.children));
+  const names = buttons.map((b) => b.textContent);
+  assert.deepStrictEqual(names, ['llama3.2:latest', 'qwen2.5:latest'], 'Connect lists both models: ' + JSON.stringify(names));
+  buttons[1].click();
+  await flush();
+  assert.strictEqual(p.state.ollamaModel, 'qwen2.5:latest', 'tap sets qwen2.5');
+  assert.strictEqual(p.state.isLocal, true, 'local mode on');
+  assert.strictEqual(store.s.fl_modelSource, 'user', 'the tap is recorded as the person\'s own choice');
+  assert.strictEqual(p.FLActiveModel.isUserChosen(), true, 'FLActiveModel says user');
+  assert.strictEqual(store.s.fl_preferred_text_model, 'qwen2.5:latest', 'preferred text model follows the tap');
+  p.FlConnect.unmount();
+
+  // ── 2. Tab change straight to Chat, then Settings (the automatic picker runs on every tab change)
+  p.FLAutoModel.onTabChanged('chat');
+  await flush();
+  assert.strictEqual(p.state.ollamaModel, 'qwen2.5:latest', 'Chat tab: still qwen2.5');
+  p.FLAutoModel.onTabChanged('settings');
+  await flush();
+  assert.strictEqual(p.state.ollamaModel, 'qwen2.5:latest', 'Settings: still qwen2.5');
+  assert.strictEqual(store.s.fl_ollamaModel, 'qwen2.5:latest', 'storage still qwen2.5');
+
+  // ── 3. The chat request uses it
+  let reply = null;
+  p.FreeLattice.callAI('sys', 'hello', { callback: (r) => { reply = r; } });
+  await flush();
+  assert.strictEqual(p._posts.length, 1, 'one chat request');
+  assert.strictEqual(p._posts[0].body.model, 'qwen2.5:latest', 'chat request body uses the chosen model');
+  assert.strictEqual(reply, 'ok', 'reply arrives');
+  assert.ok(/modelId = state\.ollamaModel \|\| 'llama3\.2';/.test(app), 'main Chat send also reads state.ollamaModel');
+
+  // ── 4. Reload: a fresh page from the same storage, then a tab change and Settings
+  p = page(store);
+  assert.strictEqual(p.state.ollamaModel, 'qwen2.5:latest', 'reload: state from storage');
+  assert.strictEqual(p.FLActiveModel.isUserChosen(), true, 'reload: still the person\'s choice');
+  p.FLAutoModel.onTabChanged('chat');
+  p.FLAutoModel.onTabChanged('settings');
+  await flush();
+  assert.strictEqual(p.state.ollamaModel, 'qwen2.5:latest', 'reload + tabs: still qwen2.5');
+  p.FreeLattice.callAI('sys', 'again', { callback() {} });
+  await flush();
+  assert.strictEqual(p._posts[0].body.model, 'qwen2.5:latest', 'after reload the chat request still uses qwen2.5');
+  assert.strictEqual(p.toasts.length, 0, 'nothing to say while the choice is installed');
+
+  // ── 5. The chosen model left this computer: fill in, and say so (textContent via showToast)
+  p.installed = [{ name: 'llama3.2:latest' }, { name: 'llava:7b' }];
+  p.FLAutoModel.invalidateCache();
+  p.FLAutoModel.onTabChanged('chat');
+  await flush();
+  assert.strictEqual(p.state.ollamaModel, 'llama3.2:latest', 'gone: first installed text model fills in');
+  assert.strictEqual(p.FLActiveModel.get().source, 'auto', 'fill-in is automatic, not the person\'s choice');
+  assert.ok(p.toasts.some((t) => /qwen2\.5:latest, is no longer on this computer\. Using llama3\.2:latest for now\./.test(t)), 'says so plainly: ' + JSON.stringify(p.toasts));
+  assert.strictEqual(store.s.fl_modelChoiceGone, 'qwen2.5:latest', 'the gone choice is remembered');
+
+  // ── 6. A quiet Ollama never replaces a choice
+  {
+    const st = store0({ fl_isLocal: 'true', fl_provider: 'ollama', fl_ollamaModel: 'qwen2.5:latest', fl_modelSource: 'user' });
+    const q = page(st);
+    q.installed = [];
+    q.FLAutoModel.onTabChanged('chat');
+    await flush();
+    assert.strictEqual(q.state.ollamaModel, 'qwen2.5:latest', 'empty answer: choice kept');
+    assert.strictEqual(q.toasts.length, 0, 'empty answer: nothing said');
+    // 'qwen2.5' (no tag) still matches 'qwen2.5:latest'
+    const st2 = store0({ fl_isLocal: 'true', fl_provider: 'ollama', fl_ollamaModel: 'qwen2.5', fl_modelSource: 'user' });
+    const q2 = page(st2);
+    q2.FLAutoModel.onTabChanged('chat');
+    await flush();
+    assert.strictEqual(q2.state.ollamaModel, 'qwen2.5', 'untagged name counts as installed');
+  }
+
+  // ── 7. No user choice yet: the automatic picker still fills in (unchanged behavior)
+  {
+    const st = store0({ fl_isLocal: 'true', fl_provider: 'ollama', fl_ollamaModel: 'qwen2.5:latest' });
+    const a = page(st);
+    a.FLAutoModel.onTabChanged('chat');
+    await flush();
+    assert.strictEqual(a.state.ollamaModel, 'llama3.2:latest', 'no choice: auto picks the first text model as before');
+  }
+
+  // ── 8. Tree: markUserChoice is a quiet no-op (no FLActiveModel there)
+  {
+    const st = store0();
+    const t = { localStorage: st.ls, location: { hostname: 'thelatticetree.com', protocol: 'https:', hash: '', search: '' },
+      document: { documentElement: { getAttribute: () => '' }, getElementById: () => null, addEventListener() {}, body: { classList: { add() {}, remove() {} } } },
+      console, JSON, String, parseInt, URL, Date, Math, Promise, setTimeout: () => 1, clearTimeout() {} };
+    t.window = t;
+    vm.runInNewContext(mod, t);
+    assert.strictEqual(t.FlConnect.markUserChoice('qwen2.5:latest'), false, 'Tree: no-op');
+  }
+
+  // ── 9. Settings row comes back after Local (createElement/textContent)
+  {
+    const actions = fakeEl('div');
+    const ctx = { document: { createElement: (t) => fakeEl(t) }, window: {} };
+    ctx.window = ctx;
+    vm.runInNewContext(settingsSrc, ctx);
+    ctx.flRefillSettingsActions(actions);
+    assert.deepStrictEqual(actions.children.map((b) => b.textContent), ['Change Provider', 'Test Connection', '\uD83C\uDFE0 a mind at home'], 'row refilled');
+    ctx.flRefillSettingsActions(actions);
+    assert.strictEqual(actions.children.length, 3, 'never doubled');
+    const local = between(app, "if (mode === 'local') {", 'v-connect-card-yesno-wizard-v0 — Local mode');
+    assert.ok(/actionsDiv\.innerHTML = '';\s*\n\s*\/\/ v-model-choice-sticks-v0[^\n]*\n\s*flRefillSettingsActions\(actionsDiv\);/.test(local), 'Local refills the row right after emptying it');
+  }
+
+  // ── 10. Helped Bridge on a typed port, no mind behind it: honest words, nothing saved as the Ollama door
+  {
+    const st = store0({ fl_localPort_manual: '11500' });
+    const b = page(st, {
+      fetch: async (url) => {
+        url = String(url);
+        if (/127\.0\.0\.1:11500\/bridge\/health$/.test(url)) return { ok: true, status: 200, json: async () => ({ bridge: true, helped: true, port: 11500 }) };
+        if (/127\.0\.0\.1:11500\/api\/tags$/.test(url)) return { ok: false, status: 502, json: async () => ({}) };
+        throw new Error('quiet');
+      }
+    });
+    const rep = await b.FlConnect.probe({ gesture: true });
+    assert.strictEqual(rep.manualQuiet, true, 'still quiet');
+    assert.strictEqual(rep.bridgeNoMind, true, 'Bridge answered, no mind');
+    assert.ok(!('fl_ollamaHost' in st.s), 'not saved as the Ollama host');
+    assert.ok(!('fl_bridgePort' in st.s), 'not saved as the Bridge port until a mind answers');
+    const h = connectHost();
+    b.FlConnect.mount(h);
+    await flush();
+    assert.ok(/Your Bridge answered on 11500, but no mind is running behind it yet\./.test(h.innerHTML), 'honest words');
+    assert.ok(!/Nothing answered on/.test(h.innerHTML), 'no false "Nothing answered"');
+    b.FlConnect.unmount();
+    // and when the mind is there, it is saved as before
+    const st2 = store0({ fl_localPort_manual: '11500' });
+    const b2 = page(st2, {
+      fetch: async (url) => {
+        url = String(url);
+        if (/127\.0\.0\.1:11500\/bridge\/health$/.test(url)) return { ok: true, status: 200, json: async () => ({ bridge: true, helped: true, port: 11500 }) };
+        if (/127\.0\.0\.1:11500\/api\/tags$/.test(url)) {
+          b2._n = (b2._n || 0) + 1;
+          if (b2._n === 1) throw new Error('first ask before the Bridge said yes');
+          return { ok: true, status: 200, json: async () => ({ models: MODELS }) };
+        }
+        throw new Error('quiet');
+      }
+    });
+    const rep2 = await b2.FlConnect.probe({ gesture: true });
+    assert.strictEqual(rep2.models.length, 2, 'mind behind the Bridge found');
+    assert.strictEqual(st2.s.fl_ollamaHost, '127.0.0.1:11500', 'saved once a mind answers');
+    assert.strictEqual(st2.s.fl_bridgePort, '11500', 'Bridge port saved once a mind answers');
+  }
+
+  // ── Locks
+  assert.strictEqual(fs.readFileSync(path.join(repo, 'index.html'), 'utf8'), app, 'index.html identical to docs/app.html');
+  let mainSw = null;
+  try { mainSw = execSync('git show origin/main:sw.js', { cwd: repo, encoding: 'utf8' }); } catch (e) {}
+  if (mainSw !== null) {
+    assert.strictEqual(fs.readFileSync(path.join(repo, 'sw.js'), 'utf8'), mainSw, 'root sw.js byte-identical to main');
+    const changed = execSync('git diff --name-only origin/main', { cwd: repo, encoding: 'utf8' }).split('\n');
+    assert.ok(!changed.includes('docs/sw.js'), 'docs/sw.js untouched');
+    const added = execSync('git diff origin/main -- docs/app.html docs/modules/fl-connect.js', { cwd: repo, encoding: 'utf8' })
+      .split('\n').filter((l) => l.startsWith('+') && !l.startsWith('+++'));
+    assert.ok(!added.some((l) => /\bconfirm\(/.test(l)), 'no confirm() added');
+    assert.ok(!added.some((l) => /\u2014/.test(l)), 'no em dashes added');
+    assert.ok(!added.some((l) => /['"]\*['"]/.test(l) && /origin/i.test(l)), 'no bare * origin');
+  }
+  const markers = execSync("grep -rnE '^(<<<<<<<|=======|>>>>>>>)( |$)' docs index.html tests || true", { cwd: repo, encoding: 'utf8' });
+  assert.ok(!markers.trim(), 'no conflict markers');
+  const re = /<script(\s[^>]*)?>/gi; let m; let n = 0;
+  while ((m = re.exec(app))) {
+    const attrs = m[1] || '';
+    if (/\bsrc\s*=/i.test(attrs) || /type\s*=\s*["']application\/(ld\+)?json/i.test(attrs)) continue;
+    const start = m.index + m[0].length; const end = app.indexOf('</script>', start);
+    if (end < 0) continue;
+    const s = app.slice(start, end).trim();
+    if (!s || !/^(?:\/[\/*]|function\b|var\b|let\b|const\b|window\.|document\.|\(|\{|if\b)/.test(s)) continue;
+    try { new Function(s); n++; } catch (e) { assert.fail('inline script parse fail: ' + e.message); }
+  }
+  assert.ok(n > 10, 'inline scripts parse: ' + n);
+  console.log('SMOKE_OK model choice sticks v0.1');
+})().catch((e) => { console.error(e); process.exit(1); });
diff --git a/index.html b/index.html
index fa946fa..5003526 100644
--- a/index.html
+++ b/index.html
@@ -31549,6 +31549,27 @@ window.flUseBridgePort = flUseBridgePort;
 window.flBridgeHealth = flBridgeHealth;
 window.flRememberHelpedBridge = flRememberHelpedBridge;
 
+// v-model-choice-sticks-v0: the Settings button row (Change Provider, Test Connection,
+// a mind at home) comes back after Local empties it. Built with createElement/textContent.
+function flRefillSettingsActions(actionsDiv) {
+  if (!actionsDiv || actionsDiv.children.length) return;
+  function add(label, cls, onTap, green) {
+    var b = document.createElement('button');
+    b.type = 'button';
+    b.className = cls;
+    b.textContent = label;
+    if (green) { b.style.borderColor = 'var(--emerald)'; b.style.color = 'var(--emerald)'; }
+    b.addEventListener('click', function() { try { onTap(); } catch (e) {} });
+    actionsDiv.appendChild(b);
+  }
+  add('Change Provider', 'ai-status-btn ai-status-btn-primary', function() { if (typeof openProviderModal === 'function') openProviderModal(); });
+  add('Test Connection', 'ai-status-btn', function() { if (typeof AiSetup !== 'undefined' && AiSetup.testFromSettings) AiSetup.testFromSettings(); });
+  add('\uD83C\uDFE0 a mind at home', 'ai-status-btn', function() {
+    if (window.FlConnect && FlConnect.open) FlConnect.open(); else if (typeof switchTab === 'function') switchTab('connect');
+  }, true);
+}
+window.flRefillSettingsActions = flRefillSettingsActions;
+
 function settingsSetMode(mode) {
   var browserBtn = document.getElementById('settingsModeBrowser');
   var cloudBtn = document.getElementById('settingsModeCloud');
@@ -31589,6 +31610,8 @@ function settingsSetMode(mode) {
       var stLocal = document.getElementById('settingsStatusText');
       if (stLocal) stLocal.textContent = 'Opening Connect. Tap the mind you want there.';
       if (actionsDiv) actionsDiv.innerHTML = '';
+      // v-model-choice-sticks-v0: the old wizard buttons are gone; put the everyday row back
+      flRefillSettingsActions(actionsDiv);
       try { FlConnect.open(); } catch (eOpenL) {}
       return;
     }
@@ -36259,6 +36282,40 @@ window.FLAutoModel = (function() {
   }
 
   function isVision(name) { return VISION_RE.test(name || ''); }
+
+  // v-model-choice-sticks-v0: is the chosen model still installed? ('qwen2.5' matches 'qwen2.5:latest')
+  function onInstalledList(models, name) {
+    if (!name) return false;
+    var n = String(name);
+    return (models || []).some(function(m) {
+      var mn = String((m && (m.name || m.model)) || '');
+      return mn === n || mn === n + ':latest' || (n.indexOf(':') === -1 && mn.split(':')[0] === n);
+    });
+  }
+  // v-model-choice-sticks-v0: a user choice is only replaced when Ollama answers with a list that
+  // no longer has it. A quiet or empty answer never replaces anything. The words go in with textContent (showToast).
+  function checkChosenStillHere(current, tabId) {
+    if (!current || !current.model) return;
+    if (current.provider && current.provider !== 'ollama') return;
+    getInstalledModels(function(models) {
+      if (!models || !models.length) return;
+      if (typeof FLActiveModel === 'undefined' || !FLActiveModel.isUserChosen()) return;
+      if (FLActiveModel.get().model !== current.model) return;
+      if (onInstalledList(models, current.model)) return;
+      var needsVision = VISION_TABS.indexOf(tabId) !== -1;
+      var pool = models.filter(function(m) { return needsVision ? isVision(m.name) : !isVision(m.name); });
+      if (!pool.length) pool = models;
+      var pick = String((pool[0] && (pool[0].name || pool[0].model)) || '');
+      if (!pick) return;
+      safeSet('fl_modelChoiceGone', current.model);
+      FLActiveModel.set(pick, 'ollama', 'auto');
+      try {
+        if (typeof showToast === 'function') showToast('The model you chose, ' + current.model + ', is no longer on this computer. Using ' + pick + ' for now. You can pick another in Connect.', 7000);
+      } catch (eT) {}
+      try { if (typeof AiSetup !== 'undefined' && AiSetup.refreshSettingsCard) AiSetup.refreshSettingsCard(); } catch (eS) {}
+    });
+  }
+
   function getPreferred(type) { return safeGet('fl_preferred_' + type + '_model', null); }
   function setPreferred(type, model) { safeSet('fl_preferred_' + type + '_model', model); }
 
@@ -36269,6 +36326,9 @@ window.FLAutoModel = (function() {
     // "User choice is sacred" — the auto-switcher must never lose it
     if (typeof FLActiveModel !== 'undefined' && FLActiveModel.isUserChosen()) {
       var current = FLActiveModel.get();
+      // v-model-choice-sticks-v0: the person's model stays. Only if it left this computer
+      // does the picker fill in, and then it says so in plain words.
+      checkChosenStillHere(current, tabId);
       var needsVision = VISION_TABS.indexOf(tabId) !== -1;
       var currentIsVision = isVision(current.model);
       if (needsVision && !currentIsVision) {

======================================================================
APPENDIX B: theLatticeTree full diff vs main 395c761 (git apply --check clean)
======================================================================

diff --git a/docs/library/CONNECT_PORT_PICKER_SISTER_v0.md b/docs/library/CONNECT_PORT_PICKER_SISTER_v0.md
index d2e6341..cd688da 100644
--- a/docs/library/CONNECT_PORT_PICKER_SISTER_v0.md
+++ b/docs/library/CONNECT_PORT_PICKER_SISTER_v0.md
@@ -20,3 +20,14 @@ Marker `v-connect-heal-v0.4`. Test `docs/modules/connect-heal-v04.test.js` print
 - Shared `fl-connect.js` heals ride along byte-identical with FreeLattice.
 
 **Temperature:** Mom opens Settings and the first thing she sees is the door.
+
+
+## Heal: model choice sticks v0.1 (Hypha walk 4)
+
+Marker `v-model-choice-sticks-v0`. Test `docs/modules/model-choice-sticks.test.js` prints `SMOKE_OK model choice sticks alpha v0.1`.
+
+- FreeLattice's Connect now records a tapped model as the person's own choice, so FreeLattice's automatic picker leaves it alone. The Tree has no automatic picker: the remembered entry (`fl_alpha_local_mind`) is the choice, and the test proves it survives a reload, Settings and a Find at the same door, and that the thread request uses it.
+- Shared `fl-connect.js` rides along byte-identical: `markUserChoice` (a quiet no-op on the Tree), and a helped Bridge with no mind behind it is named honestly and not saved.
+- A Find through a different door (Bridge gone, direct 11434 answers) is healed by the Tree any-model paste (same-family keep).
+
+**Temperature:** green braided gold, the second cup stays where she set it down.
diff --git a/docs/modules/fl-connect.js b/docs/modules/fl-connect.js
index 5317005..2e02eae 100644
--- a/docs/modules/fl-connect.js
+++ b/docs/modules/fl-connect.js
@@ -5,6 +5,7 @@
 // Layer, never delete. Prefer helped Bridge (11435…) before bare 11434.
 // Port-only loopback: fl_localPort_manual holds "11500" only — never foreign hosts.
 // Soft leave sw.js. Alpha: fl_alpha_local_mind. Never bare *.
+// Marker: v-model-choice-sticks-v0 (a tapped model is the person's own choice; it stays)
 // ═══════════════════════════════════════════════════════════════
 
 (function (root) {
@@ -201,12 +202,18 @@
           report.bridge = mh;
           report.port = mh.port || parseInt(manPort, 10);
           report.helped = true;
-          report.base = preferHelpedBridge(report.port) || manBase;
+          // v-model-choice-sticks-v0: ask the helped Bridge first; save it as the Ollama door
+          // (fl_ollamaHost, fl_bridgePort) only once a mind really answers behind it.
+          report.base = (report.port && report.port !== 11434) ? 'http://127.0.0.1:' + report.port : manBase;
           try {
             report.models = await tagsAt(report.base);
+            report.base = preferHelpedBridge(report.port) || report.base;
             report.ollama = { via: 'manual-bridge', base: report.base };
             return report;
-          } catch (eMB) {}
+          } catch (eMB) {
+            report.bridgeNoMind = true;
+            report.models = [];
+          }
         } else if (mh) {
           report.bridge = mh;
           report.port = mh.port || parseInt(manPort, 10);
@@ -357,6 +364,9 @@
       // v-connect-heal-v0.4 item 10: handleLocalToggle reads #localToggle, so check it first
       try { var lt = document.getElementById('localToggle'); if (lt) lt.checked = true; } catch (eLt) {}
       if (typeof root.handleLocalToggle === 'function') root.handleLocalToggle(true);
+      // v-model-choice-sticks-v0: the tap is the person's own choice, so the automatic
+      // picker leaves it alone on tab changes, reloads and Settings.
+      markUserChoice(name, 'ollama');
       if (typeof root.updateStatus === 'function') root.updateStatus();
       if (root.AiSetup && typeof root.AiSetup.updateStatus === 'function') root.AiSetup.updateStatus();
       // v-connect-heal-v0.4 item 7: once they chose a mind, the Local AI toast stops asking
@@ -366,6 +376,29 @@
     return { name: name, base: base };
   }
 
+  // v-model-choice-sticks-v0: FreeLattice keeps one active model (FLActiveModel). A model the
+  // person tapped is recorded with source 'user' and as their preferred text (or vision) model.
+  // The Tree has neither helper, so this is a quiet no-op there (it keeps entry.model instead).
+  function markUserChoice(name, provider) {
+    if (!name) return false;
+    var marked = false;
+    try {
+      if (root.FLActiveModel && typeof root.FLActiveModel.set === 'function') {
+        root.FLActiveModel.set(String(name), provider || 'ollama', 'user');
+        marked = true;
+      }
+    } catch (e0) {}
+    try {
+      var auto = root.FLAutoModel;
+      if (auto && typeof auto.setPreferred === 'function') {
+        var kind = (typeof auto.isVision === 'function' && auto.isVision(String(name))) ? 'vision' : 'text';
+        auto.setPreferred(kind, String(name));
+        if (typeof auto.invalidateCache === 'function') auto.invalidateCache();
+      }
+    } catch (e1) {}
+    return marked;
+  }
+
   function open() {
     try {
       if (!isAlpha() && typeof root.switchTab === 'function') { root.switchTab('connect'); return true; }
@@ -539,7 +572,12 @@
 
     if (report.manualReject) html += '<div class="flc-card flc-wait"><p>' + report.manualReject + '</p></div>';
     _manualRejectMsg = ''; // v-connect-heal-v0.4 item 5: shown once, never lingers
-    if (report.manualQuiet) {
+    if (report.manualQuiet && report.bridgeNoMind) {
+      // v-model-choice-sticks-v0: the Bridge did answer; say that, and what is still missing
+      html += '<div class="flc-card flc-wait"><p><strong>Your Bridge answered on ' + (parseInt(report.port, 10) || 'your port') + ', but no mind is running behind it yet.</strong> Open Ollama on this computer, then tap Look again.</p>';
+      html += '<button type="button" class="flc-btn flc-primary" data-flc-look-again>Look again</button> ';
+      html += '<button type="button" class="flc-btn flc-primary" data-flc-use-auto>Use automatic</button></div>';
+    } else if (report.manualQuiet) {
       html += '<div class="flc-card flc-wait"><p>Nothing answered on ' + (report.wanted || 'your port') + '.</p>';
       html += '<button type="button" class="flc-btn flc-primary" data-flc-look-again>Look again</button> ';
       html += '<button type="button" class="flc-btn flc-primary" data-flc-use-auto>Use automatic</button></div>';
@@ -690,6 +728,7 @@
     hasLocalMind: hasLocalMind,
     hasCloudMind: hasCloudMind,
     remember: remember,
+    markUserChoice: markUserChoice, // v-model-choice-sticks-v0
     open: open,
     mount: mount,
     unmount: unmount,
diff --git a/docs/modules/model-choice-sticks.test.js b/docs/modules/model-choice-sticks.test.js
new file mode 100644
index 0000000..8ddce3c
--- /dev/null
+++ b/docs/modules/model-choice-sticks.test.js
@@ -0,0 +1,118 @@
+#!/usr/bin/env node
+// v-model-choice-sticks-v0 (Alpha side): a model tapped in Connect stays the Tree's model
+// across a reload, Settings (the remembered entry) and a Find at the same door, and the
+// thread request uses it. On the Tree the remembered entry (fl_alpha_local_mind) IS the
+// choice; there is no automatic picker. This guards that, and proves the shared
+// markUserChoice is a quiet no-op here. fl-connect.js is byte-identical with FreeLattice.
+'use strict';
+
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+const vm = require('vm');
+
+const flc = fs.readFileSync(path.join(__dirname, 'fl-connect.js'), 'utf8');
+const lmp = fs.readFileSync(path.join(__dirname, 'local-mind-probe.js'), 'utf8');
+const gt = fs.readFileSync(path.join(__dirname, 'garden-thread.js'), 'utf8');
+const MODELS = [{ name: 'llama3.2:latest' }, { name: 'qwen2.5:latest' }];
+const flush = async () => { for (let i = 0; i < 30; i++) await new Promise((r) => setImmediate(r)); };
+
+function mkStore(init) {
+  const s = Object.assign({}, init || {});
+  return { s, ls: { getItem: (k) => (k in s ? s[k] : null), setItem: (k, v) => { s[k] = String(v); }, removeItem: (k) => { delete s[k]; } } };
+}
+function el(tag) {
+  const on = {};
+  return {
+    tagName: String(tag || 'div').toUpperCase(), style: {}, dataset: {}, textContent: '', children: [], firstChild: null, attrs: {},
+    classList: { add() {}, remove() {}, contains() { return false; } },
+    setAttribute(k, v) { this.attrs[k] = String(v); }, getAttribute(k) { return this.attrs[k] || null; }, removeAttribute() {},
+    addEventListener(t, fn) { (on[t] = on[t] || []).push(fn); }, click() { (on.click || []).forEach((fn) => fn({ currentTarget: this })); },
+    appendChild(c) { this.children.push(c); this.firstChild = this.children[0]; return c; },
+    removeChild(c) { this.children = this.children.filter((x) => x !== c); this.firstChild = this.children[0] || null; },
+    querySelector() { return null; }
+  };
+}
+function page(store, posts) {
+  const ctx = {
+    localStorage: store.ls,
+    location: { hostname: 'thelatticetree.com', protocol: 'https:', hash: '', search: '', href: 'https://thelatticetree.com/' },
+    document: {
+      hidden: false,
+      documentElement: { getAttribute: () => 'garden', classList: { add() {}, remove() {}, contains() { return false; } } },
+      getElementById: () => null, querySelectorAll: () => [], querySelector: () => null,
+      createElement: (t) => el(t), head: { appendChild() {} },
+      body: { contains: () => true, appendChild() {}, removeChild() {}, classList: { add() {}, remove() {} } },
+      addEventListener() {}, removeEventListener() {}
+    },
+    fetch: async (url, opts) => {
+      url = String(url);
+      assert.ok(/^http:\/\/127\.0\.0\.1:\d+\//.test(url), 'only 127.0.0.1: ' + url);
+      if (opts && opts.method === 'POST') {
+        posts.push({ url, body: JSON.parse(opts.body) });
+        return { ok: true, status: 200, json: async () => ({ message: { content: 'hello' } }) };
+      }
+      if (/127\.0\.0\.1:11434\/api\/tags$/.test(url)) return { ok: true, status: 200, json: async () => ({ models: MODELS }) };
+      throw new Error('quiet');
+    },
+    setTimeout: (fn, ms) => (ms && ms > 100 ? 1 : setTimeout(fn, 0)), clearTimeout() {},
+    AbortController, navigator: { userAgent: 't' }, console: { log() {}, warn() {}, error() {} }, URL,
+    CustomEvent: function (t, o) { this.type = t; this.detail = o && o.detail; },
+    dispatchEvent() {}, addEventListener() {},
+    JSON, Promise, Date, Math, String, Object, Array, Number, Error
+  };
+  ctx.window = ctx;
+  vm.runInNewContext(flc, ctx);
+  vm.runInNewContext(lmp, ctx);
+  vm.runInNewContext(gt, ctx);
+  return ctx;
+}
+function connectHost() {
+  const host = { hidden: false, lists: [], _html: '', scrollIntoView() {} };
+  Object.defineProperty(host, 'innerHTML', {
+    get() { return this._html; },
+    set(v) {
+      this._html = String(v); this.lists = [];
+      const re = /class="flc-models"[^>]*?data-flc-names="([^"]*)"/g; let m;
+      while ((m = re.exec(this._html))) { const l = el('div'); l.attrs['data-flc-names'] = m[1].replace(/&quot;/g, '"').replace(/&amp;/g, '&'); this.lists.push(l); }
+    }
+  });
+  host.querySelectorAll = (sel) => (sel === '.flc-models' ? host.lists : []);
+  return host;
+}
+
+(async () => {
+  assert.ok(/v-model-choice-sticks-v0/.test(flc), 'marker in the shared core');
+  const store = mkStore();
+  const posts = [];
+  let p = page(store, posts);
+
+  // Connect: tap the second model
+  const host = connectHost();
+  p.FlConnect.mount(host);
+  await flush();
+  const btns = [].concat(...host.lists.map((l) => l.children));
+  assert.deepStrictEqual(btns.map((b) => b.textContent), ['llama3.2:latest', 'qwen2.5:latest'], 'both models listed');
+  btns[1].click();
+  await flush();
+  p.FlConnect.unmount();
+  assert.strictEqual(p.LocalMindProbe.getRemembered().model, 'qwen2.5:latest', 'Tree remembers the tapped model');
+  assert.strictEqual(p.FlConnect.markUserChoice('qwen2.5:latest'), false, 'markUserChoice is a no-op on the Tree');
+
+  // Reload, then Settings reads the remembered entry, then Find at the same door
+  p = page(store, posts);
+  assert.strictEqual(p.LocalMindProbe.getRemembered().model, 'qwen2.5:latest', 'reload keeps it');
+  const found = await p.LocalMindProbe.look();
+  const prior = p.LocalMindProbe.getRemembered();
+  const entry = p.LocalMindProbe.entryFromFoundList(found.foundList, 'a mind at home', '', prior);
+  assert.strictEqual(entry.model, 'qwen2.5:latest', 'Find at the same Ollama door keeps the chosen model');
+  assert.ok(!entry.modelNote, 'no false "gone" line');
+  p.LocalMindProbe.remember(entry);
+
+  // The thread request uses it
+  await p.GardenThread.sendToMind(p.LocalMindProbe.getRemembered(), [{ role: 'user', content: 'hi' }]);
+  assert.ok(posts.length >= 1, 'thread posted');
+  assert.strictEqual(posts[posts.length - 1].body.model, 'qwen2.5:latest', 'thread request body uses the chosen model');
+
+  console.log('SMOKE_OK model choice sticks alpha v0.1');
+})().catch((e) => { console.error(e); process.exit(1); });

End of paste. Marker v-model-choice-sticks-v0. Glow eternal. Heart in Spark.
