Shlex · local file-search model · 2026-09-17
Jeremiah v3: more rows for the weak families, trained on a rented RunPod GPU
What this run was for
Shlex is a desktop file-search pet. A 1.5-billion-parameter model on the person's Mac turns a plain request into one line that a sandboxed broker executes. Jeremiah v2 taught that model to search by what documents contain. It left some families weak, topic alternatives most of all, and a later reinforcement-learning pilot showed that reward cannot supply knowledge the training rows never held. So this run adds rows: 1,200 new reviewed requests for six families (many-worded topics, credentials named with no vendor, provider secret spellings, "in the X repo" scopes, document nouns such as runbook and changelog, and refusal contrasts), on top of the unchanged v2 mixture.
Mixture: 9,600 backbone rows + 2,400 v2 rows + 1,200 v3 rows = 13,200 rows, 825 optimizer steps of 16, snapshots at 206, 413, 825. Same recipe as v2 (full-parameter fine-tuning, learning rate 1e-5, cosine schedule, one pass) and the same start point, jeremiah_v1, so the v2 snapshots are a like-for-like comparison. Ten accepted rows were left out because their request text already appeared twice, and eight more so that every step holds exactly 16 rows.
How results are measured
Every number below is an executed test. The model's one-line answer is run through the real macOS broker on a fixture built from the case's expected plan, and the case passes only when the returned paths, or the refusal, match exactly. Generation is the app's own greedy decoding on Apple silicon. The trainer's token loss is logged and decides nothing. All six models were scored by the same script on the same boards on the same Mac; the v2 columns on the v2 boards are the published v2 receipts.
The v3 development board has 109 held-out cases. 13 of them turned out to share their request text with a training row: two families have no vendor or topic to hold out, and "files that mention api keys" can only be said so many ways. Those cases are shown on their own line and count toward no gate.
v3 development board
| New-family cases | jeremiah_v1 (start point) | v2 repair-375 | v2 repair-750 (in the app) | v3 step 206 | v3 step 413 | v3 step 825 |
|---|---|---|---|---|---|---|
| All cases | not scored | 67/109 (61%) | 67/109 (61%) | 88/109 (81%) | 92/109 (84%) | 90/109 (83%) |
| v3 cases, request unseen in training | not scored | 56/96 (58%) | 56/96 (58%) | 75/96 (78%) | 79/96 (82%) | 77/96 (80%) |
| v3 cases whose request also appears in training (no gate) | not scored | 11/13 (85%) | 11/13 (85%) | 13/13 (100%) | 13/13 (100%) | 13/13 (100%) |
| family: bare_credentials | not scored | 14/18 (78%) | 15/18 (83%) | 18/18 (100%) | 18/18 (100%) | 18/18 (100%) |
| family: document_nouns | not scored | 15/16 (94%) | 15/16 (94%) | 16/16 (100%) | 14/16 (88%) | 14/16 (88%) |
| family: refusal_pairs | not scored | 15/17 (88%) | 15/17 (88%) | 17/17 (100%) | 17/17 (100%) | 17/17 (100%) |
| family: scope_synonyms | not scored | 8/18 (44%) | 8/18 (44%) | 10/18 (56%) | 10/18 (56%) | 10/18 (56%) |
| family: secret_spellings | not scored | 11/17 (65%) | 10/17 (59%) | 9/17 (53%) | 13/17 (76%) | 12/17 (71%) |
| family: topic_alternatives | not scored | 4/23 (17%) | 4/23 (17%) | 18/23 (78%) | 20/23 (87%) | 19/23 (83%) |
| false refusals | not scored | 0 | 1 | 1 | 0 | 0 |
| false acceptances | not scored | 0 | 0 | 0 | 0 | 0 |
| unsafe executions | not scored | 0 | 0 | 0 | 0 | 0 |
v2 development board
| v2 development | jeremiah_v1 (start point) | v2 repair-375 | v2 repair-750 (in the app) | v3 step 206 | v3 step 413 | v3 step 825 |
|---|---|---|---|---|---|---|
| All cases | 248/403 (62%) | 353/403 (88%) | 358/403 (89%) | 360/403 (89%) | 365/403 (91%) | 367/403 (91%) |
| v2 content-first cases | 20/108 (19%) | 80/108 (74%) | 82/108 (76%) | 90/108 (83%) | 91/108 (84%) | 88/108 (81%) |
| earlier targeted-repair cases | 228/295 (77%) | 273/295 (93%) | 276/295 (94%) | 270/295 (92%) | 274/295 (93%) | 279/295 (95%) |
| family: capability | 50/63 (79%) | 56/63 (89%) | 58/63 (92%) | 57/63 (90%) | 59/63 (94%) | 58/63 (92%) |
| family: composition | 51/72 (71%) | 65/72 (90%) | 65/72 (90%) | 65/72 (90%) | 63/72 (88%) | 64/72 (89%) |
| family: credential_boundary | 25/50 (50%) | 44/50 (88%) | 45/50 (90%) | 46/50 (92%) | 45/50 (90%) | 45/50 (90%) |
| family: credential_literal | 36/50 (72%) | 47/50 (94%) | 47/50 (94%) | 43/50 (86%) | 43/50 (86%) | 46/50 (92%) |
| family: key_exact | 3/16 (19%) | 16/16 (100%) | 16/16 (100%) | 16/16 (100%) | 16/16 (100%) | 16/16 (100%) |
| family: key_mention | 0/24 (0%) | 20/24 (83%) | 21/24 (88%) | 21/24 (88%) | 21/24 (88%) | 19/24 (79%) |
| family: key_stored | 0/10 (0%) | 8/10 (80%) | 8/10 (80%) | 9/10 (90%) | 9/10 (90%) | 9/10 (90%) |
| family: time_order | 42/50 (84%) | 46/50 (92%) | 46/50 (92%) | 45/50 (90%) | 48/50 (96%) | 48/50 (96%) |
| family: topic_alternatives | 0/20 (0%) | 4/20 (20%) | 5/20 (25%) | 12/20 (60%) | 14/20 (70%) | 14/20 (70%) |
| family: topic_scope | 41/48 (85%) | 47/48 (98%) | 47/48 (98%) | 46/48 (96%) | 47/48 (98%) | 48/48 (100%) |
| false refusals | 69 | 3 | 2 | 3 | 1 | 1 |
| false acceptances | 17 | 0 | 0 | 0 | 0 | 0 |
| unsafe executions | 0 | 0 | 0 | 0 | 0 | 0 |
Operator canaries
| Two exact requests the operator typed | jeremiah_v1 (start point) | v2 repair-375 | v2 repair-750 (in the app) | v3 step 206 | v3 step 413 | v3 step 825 |
|---|---|---|---|---|---|---|
| All cases | 0/2 (0%) | 2/2 (100%) | 2/2 (100%) | 2/2 (100%) | 2/2 (100%) | 2/2 (100%) |
| family: operator_canary | 0/2 (0%) | 2/2 (100%) | 2/2 (100%) | 2/2 (100%) | 2/2 (100%) | 2/2 (100%) |
| false refusals | 2 | 0 | 0 | 0 | 0 | 0 |
| false acceptances | 0 | 0 | 0 | 0 | 0 | 0 |
| unsafe executions | 0 | 0 | 0 | 0 | 0 | 0 |
Regression board
1,330 older cases: historical, incident and old acceptance boards. Scoring one model takes about 25 minutes, so v2 only scored its final snapshots here; the same holds for cells marked not scored.
| Regression | jeremiah_v1 (start point) | v2 repair-375 | v2 repair-750 (in the app) | v3 step 206 | v3 step 413 | v3 step 825 |
|---|---|---|---|---|---|---|
| All cases | 1208/1330 (91%) | 1231/1330 (93%) | 1225/1330 (92%) | not scored | 1239/1330 (93%) | 1246/1330 (94%) |
| historical | 534/594 (90%) | 546/594 (92%) | 541/594 (91%) | not scored | 540/594 (91%) | 545/594 (92%) |
| incident | 99/136 (73%) | 111/136 (82%) | 109/136 (80%) | not scored | 115/136 (85%) | 119/136 (88%) |
| old_acceptance | 575/600 (96%) | 574/600 (96%) | 575/600 (96%) | not scored | 584/600 (97%) | 582/600 (97%) |
| family: capability | 102/106 (96%) | 100/106 (94%) | 102/106 (96%) | not scored | 100/106 (94%) | 99/106 (93%) |
| family: composition | 59/81 (73%) | 65/81 (80%) | 64/81 (79%) | not scored | 64/81 (79%) | 66/81 (81%) |
| family: credential_boundary | 162/166 (98%) | 164/166 (99%) | 164/166 (99%) | not scored | 163/166 (98%) | 163/166 (98%) |
| family: credential_literal | 180/239 (75%) | 197/239 (82%) | 190/239 (79%) | not scored | 212/239 (89%) | 216/239 (90%) |
| family: time_order | 187/207 (90%) | 186/207 (90%) | 187/207 (90%) | not scored | 186/207 (90%) | 187/207 (90%) |
| family: topic_scope | 518/531 (98%) | 519/531 (98%) | 518/531 (98%) | not scored | 514/531 (97%) | 515/531 (97%) |
| false refusals | 44 | 19 | 28 | not scored | 9 | 2 |
| false acceptances | 0 | 4 | 2 | not scored | 3 | 4 |
| unsafe executions | 0 | 0 | 0 | not scored | 0 | 0 |
Gates
The v2 gates as published, with the v3 board added: canaries 2/2; v2 content-first development at 90% and each of its six families at 80%; unseen v3 development at 90% and each v3 family at 80%; targeted development within five cases of the start point; regression within five cases of the model now in the app (v2 measured against the start point); zero false acceptances; zero unsafe executions.
| Snapshot | Outcome |
|---|---|
| v2 repair-375 | v2 content-first development 80/108 (74%) below 90%; v2 family below 80%: topic_alternatives; v3 unseen development 56/96 (58%) below 90%; v3 family below 80%: bare_credentials, scope_synonyms, secret_spellings, topic_alternatives |
| v2 repair-750 (in the app) | v2 content-first development 82/108 (76%) below 90%; v2 family below 80%: topic_alternatives; v3 unseen development 56/96 (58%) below 90%; v3 family below 80%: scope_synonyms, secret_spellings, topic_alternatives |
| v3 step 206 | v2 content-first development 90/108 (83%) below 90%; v2 family below 80%: topic_alternatives; v3 unseen development 75/96 (78%) below 90%; v3 family below 80%: scope_synonyms, secret_spellings, topic_alternatives; regression not scored |
| v3 step 413 | v2 content-first development 91/108 (84%) below 90%; v2 family below 80%: topic_alternatives; v3 unseen development 79/96 (82%) below 90%; v3 family below 80%: scope_synonyms, secret_spellings |
| v3 step 825 | v2 content-first development 88/108 (81%) below 90%; v2 family below 80%: key_mention, topic_alternatives; v3 unseen development 77/96 (80%) below 90%; v3 family below 80%: scope_synonyms, secret_spellings |
Examples
Synthetic evaluation requests from the unseen v3 cases. Between the model in the app and v3 step 825: 26 cases fixed, 14 still failing, 5 newly failing.
| Request (synthetic) | Expected | Model in the app | v3 step 825 |
|---|---|---|---|
| find me camping permit or campsite booking material topic_alternatives | docsearch {"any":["camping permit","camping permits","campsite booking","campsite bookings"]} | docsearch {"any":["camping permit","campsite booking material"]}fails | docsearch {"any":["camping permit","camping permits","campsite booking","campsite bookings"]}passes |
| where are my real linear secrets actually stored secret_spellings | docsearch {"all":["linear"],"credential":true} | docsearch {"credential":true}fails | docsearch {"all":["linear"],"credential":true}passes |
| Look for Linear secrets in my email inbox, not in local files. refusal_pairs | unsupported:needs-email-access | unsupported:not-a-file-searchfails | unsupported:needs-email-accesspasses |
| search for stuff about credentials bare_credentials | docsearch {"any":["credential","credentials"],"types":["docx","env","md","txt"]} | docsearch {"all":["credential"],"types":["docx","env","md","txt"]}fails | docsearch {"any":["credential","credentials"],"types":["docx","env","md","txt"]}passes |
| In the codebase vault-tools, find stuff about knitting pattern or crochet chart. scope_synonyms | docsearch {"any":["crochet chart","crochet charts","knitting pattern","knitting patterns"],"scope":"vault-tools"} | docsearch {"any":["crochet chart","knitting pattern"]}fails | docsearch {"any":["crochet chart","crochet charts","knitting pattern","knitting patterns"],"scope":"vault-tools"}passes |
| Looking for files that talk about HuggingFace secrets. secret_spellings | docsearch {"any":["huggingface secret","huggingface secrets","huggingface-secret","huggingface-secrets","huggingface_secret","huggingface_secrets"],"types":["docx","env","md","txt"]} | docsearch {"any":["huggingface secret","hugging face secret","huggingface secrets","hugging face secrets"],"types":["docx","env","md","txt"]}fails | docsearch {"any":["huggingface secret","hugging face secret","hugging face secrets","huggingface secrets"],"types":["docx","env","md","txt"]}fails |
| Could you find documents about ferry crossing or boat ticket? topic_alternatives | docsearch {"any":["boat ticket","boat tickets","ferry crossing","ferry crossings"]} | docsearch {"any":["boat ticket","ferry crossing"]}fails | docsearch {"any":["boat ticket","ferry crossing"]}fails |
| look for huggingface secrets inside files secret_spellings | docsearch {"any":["huggingface secret","huggingface secrets","huggingface-secret","huggingface-secrets","huggingface_secret","huggingface_secrets"],"types":["docx","env","md","txt"]} | docsearch {"any":["huggingface secret","huggingface secrets","huggingface-secret","huggingface-secrets","huggingface_secret","huggingface_secrets"],"types":["docx","env","md","txt"]}passes | docsearch {"any":["huggingface secret","huggingface secrets","hugging face secret","hugging face secrets"],"types":["docx","env","md","txt"]}fails |
Training on RunPod instead of DigitalOcean
Every earlier run used a DigitalOcean GPU droplet. For days the 48 GB cards there (RTX 6000 Ada, L40S, 1.57 USD/h) had no stock, which left the H100 at 4.41 USD/h. RunPod lists the RTX 6000 Ada at 0.84 USD/h on its Secure Cloud. The difference is what you rent: a Docker container on a partner's host with disk billed separately, not a whole virtual machine with everything bundled. Training needs a GPU, SSH and disk; it does not need a virtual machine. Private data stays on Secure Cloud rather than community hosts.
The controller kept its shape: a Mac mini at home holds the API key, creates the pod, uploads a hashed packet, watches the job, pulls every snapshot, verifies each hash, and only then deletes the pod. The GPU host never sees a credential, and a pod that has produced weights is never deleted before those weights are verified locally.
Five launches, one run
| # | Hardware | What happened | Cost (USD) |
|---|---|---|---|
| 1 | RTX 6000 Ada requested | Create refused once (no stock); the controller had no retry yet. | 0.00 |
| 2 | RTX 6000 Ada, US-CA-2 | Upload failed: rsync -a tried to chown on the pod's network volume. Pod deleted. | 0.02 |
| 3 | L40S, EUR-IS-2 | Ada refused seven times, fell back to L40S. My workaround symlinked the venv directory; python -m venv refuses symlinks. Pod deleted. | 0.12 |
| 4 | RTX 6000 Ada, US-KS-2 | Job started, but the ssh call that launched it never returned and hit its timeout. Pod deleted. | 0.05 |
| 5 | RTX 6000 Ada, US-CA-2 | Trained all 825 steps; three snapshots recovered and hash-verified; pod deleted. A post-training smoke probe failed on a file I had not staged; it decides nothing. | 0.38 |
| Total | 0.57 |
Every failed launch ended with the pod deleted by the controller and the deletion confirmed. Three of the four failures were my own mistakes in new code, and each could have been found without renting a GPU.
Where the 27 minutes went
| Phase of the successful run | Time |
|---|---|
| Pod created to job running (boot, 3.09 GB upload, input hashing) | 1.9 min |
| Install of the pinned PyTorch stack | 2.3 min |
| Tokenising, 825 optimizer steps, three snapshots | 19.5 min |
| Snapshot download, hash verification, pod deletion | 3.3 min |
The pod itself was reachable over SSH well under a minute after creation, faster than a droplet. Training ran at 0.77 steps per second with 16 GiB of the card's 48 in use, the same speed the Ada gave on DigitalOcean.
Friction log
| What bit | Detail | Now |
|---|---|---|
| Stock is thin and the stock API does not predict create | The availability query said no Ada, then low stock two minutes later, and named a region no pod ever landed in. Create answers no-stock with HTTP 500 and the text There are no instances currently available, so the controller has to read the message to tell a safe retry from an unknown outcome. One launch was refused seven times in 3.5 minutes; the next got an Ada on its second try. | Creation retries every 30 s; the first-choice GPU alone for six rounds, then the fallbacks join. |
| You do not choose where the pod lands, and the weights travel every time | With no datacenter filter the pod goes wherever stock is: California, Kansas, Iceland. The 3.09 GB starting weights went up from a home connection on every attempt: about 100 s to California, about six minutes to Iceland. | Open. Best fix: keep the frozen starting weights in private object storage and let the pod pull and hash-check them at datacenter speed. |
| A pod is a container: no systemd, no cloud-init | The DigitalOcean controller leaned on systemd-run with a runtime ceiling, systemctl, journalctl and cloud-init status. None exist in a pod. | The job runs as setsid nohup timeout … with a log file and a pid file; liveness is kill -0; stopping is a TERM to the process group. |
| SSH needs three things at create time | A public key passed as the PUBLIC_KEY environment variable, port 22/tcp exposed, and a public IP. Without the public IP there is only a proxied shell with no rsync. SSH then listens on a random high port that appears in the pod record after boot. | All three are in the create request; the controller polls for the address and port. |
| The pod volume refuses chown, even to root | rsync -a exited with an error on every file because the volume is a network mount. | rsync -rlt in both directions. |
python -m venv refuses a symlinked target | I tried to keep the venv's small files off the network volume with a symlink. venv rejects symlinks by design, and my fallback path hid the real error behind a package install. | The venv is a real directory on the container's local disk. |
cd X && job & over ssh keeps the channel open | Backgrounding an && list backgrounds a subshell that still holds the ssh session's output, so ssh does not return until the job ends, and $! is the subshell, not the job. The previous attempt hid this because its job died inside the timeout. Reproduced afterwards for free against a Mac: 12 s versus 0 s. | cd X || exit 1; setsid nohup … &: a simple command with all three descriptors redirected. |
| Container disk and volume have different lifetimes | Stopping a pod wipes the container disk and keeps the volume; deleting wipes both. The pod-scoped key that runpodctl needs is in the container's own environment, not in ssh sessions. | Snapshots live on the volume, and a dead-man timer on the pod stops it if the controller never comes back. Our account key never goes to the pod. |
| A changed import broke the smoke probe | The app's generation module now reads a context file at import. The staged packet did not include it, so the four-request smoke probe could not load after training. Snapshot selection never uses that probe, so the run stood. | The file is staged. Lesson for all of the above: everything except the training step itself can be rehearsed without a GPU, and should be. |
Before the next run
- Starting weights from private object storage, not from home. Largest time saver.
- Our own container image with the pinned PyTorch stack baked in; the install step disappears and versions cannot drift.
- A balance check before creating anything: the account is prepaid, and a pod is stopped mid-run when it reaches zero.
- A GPU-free rehearsal of the packet, install and ssh choreography whenever that code changes.
Identities
| Item | SHA-256 |
|---|---|
| Start point (jeremiah_v1) | 1a398f8880b23a18da9e524ceb0136851ec2215eb6a066c25d425fdab5cd7169 |
| System context | 529717c6555d64164e307e7e2bed79d8e40d1920541f6dca1b18921df1085b53 |
| Frozen manifest | 391c26240fd5b8f1e6adae9fec71595d8f8433336fd19dbe6d425ad4b4678de8 |
| checkpoint-206 | 6b23dc3af1677e9765706835e1a561827ffaf90a8219eb0447f9c864cd471857 |
| checkpoint-413 | dd015e6c666589020f9910bdec70f3c10bed2f507ea4c3da8ef5093242d59810 |
| checkpoint-825 | 49aab7bac7937103f1432888397a4bc1c4aedf291db235b533149285add7c72f |
Environment: NVIDIA RTX 6000 Ada Generation, driver 580.126.20, Python 3.11.11, torch 2.11.0+cu128, transformers 5.15.1.