What changed, and why
One environment variable, chained on the keep-alive PR (same file, same dict). Without it, concurrent requests to the Mac Studio's Ollama queue serially behind a single slot: when ynab-agent's poll tick fans out several category enrichments at once, the second waits for the first, the third for the second. OLLAMA_NUM_PARALLEL=4 gives the loaded model four slots that batch instead.
The one block that changed
The variable lands at the end of the same environment dict, under the keep-alive entry this PR chains on.
KV-cache tuning, keep-alive, and the new four-slot setting.
infra/k8s/tailscale/macstudio/com.zo.ollama.plist · 68 lines
infra/k8s/tailscale/macstudio/com.zo.ollama.plist
⋯ 32 lines hidden (lines 1–32)
1<?xml version="1.0" encoding="UTF-8"?>
2<!-- LaunchAgent that runs Ollama on the Mac Studio so it survives reboots.
3 Reference copy of ~/Library/LaunchAgents/com.zo.ollama.plist. Install:
5 cp infra/k8s/tailscale/macstudio/com.zo.ollama.plist ~/Library/LaunchAgents/
6 launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.zo.ollama.plist
7 brew services stop ollama # avoid the formula's LaunchAgent fighting this one
9 We use a dedicated agent (not `brew services`) because the Homebrew ollama
10 formula hardcodes its service env block, so brew regenerates the plist on
11 every upgrade and would wipe any added vars. This agent preserves the
12 formula's perf tuning AND survives `brew upgrade ollama`. -->
13<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
14<plist version="1.0">
15<dict>
16 <key>Label</key>
17 <string>com.zo.ollama</string>
18 <key>ProgramArguments</key>
19 <array>
20 <string>/opt/homebrew/opt/ollama/bin/ollama</string>
21 <string>serve</string>
22 </array>
23 <key>EnvironmentVariables</key>
24 <dict>
25 <!-- Loopback bind: `tailscale serve` dials 127.0.0.1:11434 and owns the
26 tailnet IP on :11434, so Ollama must NOT bind 0.0.0.0 (would collide
27 with serve, and is pointless — the macOS firewall blocks direct
28 non-loopback hits regardless). The cluster's Host header is rewritten
29 to `localhost` by an in-cluster proxy so Ollama's DNS-rebind check
30 accepts it. Local `ollama` CLI keeps working on the default port. -->
31 <key>OLLAMA_HOST</key>
32 <string>127.0.0.1:11434</string>
33 <!-- Preserved from the Homebrew ollama formula's service block. -->
34 <key>OLLAMA_FLASH_ATTENTION</key>
35 <string>1</string>
36 <key>OLLAMA_KV_CACHE_TYPE</key>
37 <string>q8_0</string>
38 <!-- Never idle-unload a loaded model. The default unloads after 5
39 minutes, so a quiet stretch (ynab-agent's email cadence is bursty)
40 pays a multi-second cold load on the next call — the largest
41 single source of tail latency. -1 disables the timer; loading a
42 different model can still evict via the normal LRU path. -->
43 <key>OLLAMA_KEEP_ALIVE</key>
44 <string>-1</string>
45 <!-- Four request slots per loaded model so concurrent callers
46 (ynab-agent's poll tick fans out several W2 enrichments; froot
47 runs its own loops) batch instead of queueing serially. Each slot
48 carries its own KV cache — with q8_0 KV and the default context
49 this is a modest, bounded cost — and slots are reused by longest
50 shared prefix, so distinct call types each keep a warm prefix. -->
51 <key>OLLAMA_NUM_PARALLEL</key>
52 <string>4</string>
53 </dict>
⋯ 15 lines hidden (lines 54–68)
54 <key>RunAtLoad</key>
55 <true/>
56 <key>KeepAlive</key>
57 <true/>
58 <!-- Aqua = run in the logged-in GUI session so Metal/GPU is available. -->
59 <key>LimitLoadToSessionType</key>
60 <string>Aqua</string>
61 <key>StandardOutPath</key>
62 <string>/opt/homebrew/var/log/ollama.log</string>
63 <key>StandardErrorPath</key>
64 <string>/opt/homebrew/var/log/ollama.log</string>
65 <key>WorkingDirectory</key>
66 <string>/opt/homebrew/var</string>
67</dict>
68</plist>