AI research and infra, data, performance, robotics. I send fixes upstream for what I run into: Kubernetes, LLM inference, data tooling, coding agents, ROS 2, OpenCV. Go · Rust · Python · C++ · TypeScript · Write-ups at spicyshark.co · Research at spicyshark.co/research
| Project | Merged change | Result | Story |
|---|---|---|---|
| kube-router | #2192 network-policy sync stops copying the iptables table once per pod | 320x faster, 99.8% less allocation (benchmark) | post |
| k3s | #14801 --disable-etcd on an existing server removes its etcd member |
no stale member left in the cluster | |
| k3s | #14811 tunnel proxy stops leaking a goroutine when both directions of a connection fail | 1,029 leaked goroutines → 0 (10,000-run test) | post |
| kine | #800 compaction retries on MariaDB error 1020, so kine runs on MariaDB 11.8 | 44 failed compactions → 0 in 45 s of concurrent writes | |
| cri-tools (crictl) | #2212 make verify checks docs and vendor drift in a temp dir instead of rewriting the tree |
tree byte-identical after every run | post |
| external-dns | #6805 TXT registry computes only the ownership record name when checking for missing records, instead of rendering and encrypting every record's labels | Records(), which runs every reconcile: 512 ms → 23 ms per call with TXT encryption on, 96% less allocation (1k records, in-memory benchmark) |
post |
| redis-py | #4434 asyncio client breaks the reference cycle on each error reply | 1,000 leaked exceptions → 0 (GC off) | post |
| sql_exporter | #1115 log context trims its leading comma again, so failed queries stop logging a false "Invalid context log item" warning | 1 false warning per failed query → 0 (test) | post |
| sql_exporter | #1116 concurrent /metrics?jobs[]= scrapes stop sharing one job filter, so each response holds only its own job's series |
20 of 20 mixed-job test runs → 0, go test -race clean |
post |
| sql_exporter | #1117 a collectors: glob that overlaps a collector already named no longer fails startup with unknown collector |
README example config: -config.check error → passes, collector loaded once |
post |
| sql_exporter | #1118 a JSON secret with a number or boolean beside the DSN, such as the numeric port in AWS database secrets, now resolves its key instead of handing the whole JSON to the driver |
secret with numeric port: target open error → DSN resolved (test) |
post |
| sql_exporter | #1119 a reload that switches a single target: config to jobs: is now refused with the scrape mode error, as the reverse switch already was, instead of reporting success and serving the old target |
/reload single target → jobs: 200, no error → 500 with the error (test) |
post |
| sql_exporter | #1120 a reload that fails no longer stores the config it could not apply, so /config and the jobs[] filter keep matching the targets that still serve |
after a failed reload, /metrics?jobs[]=a: 400 invalid job name → 200 |
post |
| sql_exporter | #1121 a query that fails under min_interval is no longer cached, so the next scrape runs it again instead of replaying the error until the interval ends |
min_interval: 1h, table created after a failed scrape: m missing and scrape_errors_total rising on every scrape → m 1 on the next scrape |
post |
| UniFi Poller | #1110 unpoller_device_stations exports one series per station type again, with station_type and tag in the right labels, so the Users and Guests dashboard panels show data |
UAP with 3 user and 2 guest stations: one series {station_type="", tag="guest"} 3 → user 3 and guest 2 |
post |
| UniFi Poller | #1113 unpoller_device_temperature_celsius and unpoller_device_storage carry each sensor's and each disk's own labels, so a gateway's sensors no longer overwrite each other's labels |
UXG with sensors Local 41 and CPU 62 and no disk: usually {temp_area="", temp_type="CPU", tag="cpu"} twice → one series per sensor with its own area and type |
post |
| UniFi Poller | #1114 unpoller_device_stations exports a USG's own mobile and handheld counts, so the USG Insights Mobile and Handheld panels no longer show the user and guest counts |
USG with 41 user, 0 guest, 2 mobile and 8 handheld stations: {station_type="mobile"} 41 and {station_type="handheld"} 0 → mobile 2 and handheld 8 |
post |
| Radicale (CardDAV) | #2285 a CardDAV prop-filter with no test, or test="anyof", returns a card that matches any one of its conditions, as RFC 6352 says, instead of only a card that matches all of them |
card with NICKNAME:test, prop-filter with no test and text-match test and text-match a: empty multistatus → card returned; test="allof" still excludes it |
post |
| filelock (file locks) | #856 acquire() resolves the lock path before it raises the hold count, so a FileNotFoundError from a removed working directory leaves the count unchanged |
relative-path lock, working directory removed, acquire() raises, then with lock: ends: lock_counter 1 and is_locked True → 0 and False (test, four lock types; the maintainer added the nested-acquire test) |
post |
| tox (test environments) | #4174 positional factor labels such as {factor:0} resolve per env_base table, so each table's environments get their own labels instead of the last table's |
sync-oci-pw description Sync to → Sync oci to pw (test, failed 3 of 3 runs before the fix) |
post |
| Jedi (Python autocompletion and refactoring) | #2122 Script.extract_function leaves out the return line and the assignment when the extracted lines define no name, so the call site is a plain g(a) instead of the syntax error = g(a) |
two print calls extracted from f: = g(a) and a bare return → g(a) and no return (repro; the new test failed 3 of 3 runs before the fix) |
post |
| Kamaji (Kubernetes control planes) | #1360 removing the whole extraArgs block from a TenantControlPlane now resets the flags it added to the kube-apiserver container, instead of leaving them in place on every later pass |
resetKubeAPIServerFlags with the block removed and --audit-log-path on the container: args kept → args reset (test, failed 3 of 3 runs before the fix) |
post |
| wcmatch (Python glob matching) | #245 an inverse extended glob followed by an extended group that holds another, such as !(a)@(b|*(c)), translates to a valid regular expression instead of raising re.error |
!(2)_@(foo|*(bar)) on 1_foo with EXTMATCH: re.error → True (test, five rows) |
post |
| wcmatch (Python glob matching) | #246 a star directly before an extended star group, such as a**(b), is read as * followed by the group *(b), so the pattern matches abb instead of only names that end in the literal (b) |
fnmatch.fnmatch('abb', 'a**(b)', flags=fnmatch.EXTMATCH) False → True; glob.globmatch('b', '**(a|b)b', flags=glob.GLOBSTAR | glob.EXTGLOB) False → True (test, eight rows, eight failed before the fix) |
post |
| wcmatch (Python glob matching) | #247 glob.glob and glob.iglob with IGNORECASE return a path once, not twice, when two patterns find it, and return both a and A where the file system holds both |
glob.glob(['A*', '*b'], flags=glob.IGNORECASE) with a file Ab: ['Ab', 'Ab'] → ['Ab'] (test, one row, failed in three test classes before the fix) |
post |
| goja (JavaScript engine in Go) | #803 Array.from throws RangeError: Invalid array length for an array-like longer than 2**32-1 when this is Array or not a constructor, as Node does, instead of looping once per index |
Array.from({length: 2**32}): no return in any practical time → RangeError: Invalid array length (test, three calls, failed 3 of 3 runs before the fix) |
post |
| Kamaji (Kubernetes control planes) | #1363 a missing apiserver.crt in the API server certificate Secret now makes Kamaji regenerate the certificate, instead of panicking in the log line of the names check |
the API server certificate handler with a stored Secret that holds only apiserver.key: nil pointer panic → new certificate written (test; 34 of 35 specs passed and the new one failed before the fix, all 35 pass after) |
post |
| Kamaji (Kubernetes control planes) | #1365 a failed DataStore lookup in the TenantControlPlane reconcile is now returned, so it is retried with backoff, instead of being logged and reported as success | Reconcile with Spec.DataStore naming a DataStore that does not exist: nil error returned → not-found error returned (test) |
post |
| goja (JavaScript engine in Go) | #805 TypedArray.from accepts a string or number source, as Node does, instead of throwing TypeError: Value is not an object |
Uint8Array.from("123"): TypeError: Value is not an object: 123 → [1, 2, 3] (test, five assertions, failed 3 of 3 runs before the fix) |
post |
| goja (JavaScript engine in Go) | #806 RegExp.prototype.test calls exec, as Node does, instead of throwing TypeError for a plain object with exec and ignoring an exec that was replaced |
RegExp.prototype.test.call({exec() { return null; }}, "a"): TypeError → false (test, 14 assertions, failed 3 of 3 runs before the fix) |
post |
| Prefect (workflow orchestration in Python) | #23442 Task.with_options, Flow.with_options and MaterializingTask.with_options copy the hook lists, so a hook added to the copy stays off the original instead of being added to both |
original task with one on_failure hook, with_options copy, then on_failure(alert) on the copy: the original's on_failure_hooks ['log_failure', 'alert'] → ['log_failure'] (test, 13 rows over tasks, flows and @materialize tasks, all failed before the fix) |
post |
| goja (JavaScript engine in Go) | #807 RegExp.prototype[Symbol.replace] (non-global) and [Symbol.split] call exec, as Node does, instead of panicking on a nil pointer when exec is missing |
RegExp.prototype[Symbol.replace].call({}, "a", "b"): Go nil pointer panic → TypeError (test, 9 assertions, failed 3 of 3 runs before the fix) |
post |
| Project | Merged change | Result | Story |
|---|---|---|---|
| pgx | #2689 interval text encoding no longer overflows on the minimum microseconds value, so the text scans back | --2562047788:00:-54.-775808 → -2562047788:00:54.775808, same as PostgreSQL 18 |
|
| arrow-rs | #11628 CSV reader returns an error instead of panicking when a dictionary column holds more distinct values than its key type can index | 129 values with Int8 keys: panic → DictionaryKeyOverflowError |
post |
| arrow-rs | #11630 C Data Interface export of a Field keeps a map's keys-sorted flag, so a sorted map, also inside a struct, list, union or Schema, imports as sorted |
Map(_, true) in a Field, exported and imported back: Map(_, false) → Map(_, true) |
post |
| sqlglot | #8662 ClickHouse PASTE JOIN parses as a join instead of PASTE becoming the left table's alias |
SELECT * FROM foo PASTE JOIN bla: read as foo AS PASTE, bla, a cross join → kept as PASTE JOIN (test) |
|
| yfinance | #3001 a start or end date whose midnight falls in a daylight-saving gap (Cairo, Santiago, Sao Paulo before 2019) shifts forward to 01:00 instead of raising | Sao Paulo 2018-11-04: ValueError nonexistent time → parsed as 01:00 (test) |
|
| ezdxf (DXF files) | #1433 export_json_tags() escapes backslashes and double quotes in string values, so its JSON parses and an MTEXT with a \P line break loads back unchanged |
MTEXT first\Psecond, json.loads of the export: JSONDecodeError: Invalid \escape → parses, and load_json_tags() returns the same text (test, both formats) |
post |
| gojq (jq in Go) | #322 a slice with a negative fractional start, such as .[-1.5:], rounds the start down as jq does, instead of truncating it toward zero, in array slices, string slices and slice assignment |
.[-1.5:] on [0,1,2]: [2] → [1,2]; .[-1.5:] = ["x"]: [0,1,"x"] → [0,"x"] (test, array and string cases, failed before the fix) |
post |
| OjG (Go JSONPath) | #243 the in filter operator compares arrays and objects by value, as == and != do, so $[?@.a in @.b] returns the row instead of panicking |
[{"a":[1],"b":[[1]]}]: comparing uncomparable type []interface {} panic → row returned; (@ in [1]) with 1.0: false → true (test, six rows) |
post |
| OjG (Go JSONPath) | #246 a parenthesized path in a filter, (@.a), is an existence check like @.a, so $[?(@.a) && (@.b)] returns the row that has both fields instead of nothing |
[{"a":1},{"b":2},{"a":1,"b":2}]: $[?(@.a) && (@.b)] [] → [{"a":1,"b":2}]; $[?!(@.a)] all three rows → [{"b":2}] (test, five rows, four failed before the fix) |
post |
| OjG (Go JSONPath) | #248 ! in a filter applies to one term, as in RFC 9535, so $[?!@.a && @.b] returns the row that has b and not a instead of every row the whole !(@.a && @.b) lets through |
[{"a":1},{"b":2},{"a":1,"b":2},{"c":3}]: $[?!@.a && @.b] [{"a":1},{"b":2},{"c":3}] → [{"b":2}]; $[?!@.a || @.b] [{"c":3}] → [{"b":2},{"a":1,"b":2},{"c":3}] (test, six rows, both tests failed before the fix) |
post |
| soupsieve (CSS selectors for Beautiful Soup) | #320 :nth-last-child(an+b of S) returns the right elements when the first sibling checked does not match S, so li:nth-last-child(1 of .x) returns the last .x item instead of the one before it |
<ul><li id="0"></li><li id="1" class="x"></li><li id="2" class="x"></li></ul>: li:nth-last-child(1 of .x) ['1'] → ['2'] (test, three selectors, the cached run failed before the fix) |
post |
| soupsieve (CSS selectors for Beautiful Soup) | #322 :in-range and :out-of-range accept a week input with a week-year of 10000 or more instead of raising ValueError, so a value of 10000-W20 between a min of 10000-W01 and a max of 10000-W52 is in range |
<input id="a" type="week" min="10000-W01" max="10000-W52" value="10000-W20">: :in-range ValueError: year 10000 is out of range → ['a'] (test, two rows in each of the two week tests, both failed before the fix) |
post |
| PyMC (probabilistic programming in Python) | #8503 VonMises.logp uses the scaled i0e instead of log(i0(kappa)), so the log-density of a point inside the support is finite for a large kappa instead of -inf |
pm.logp(pm.VonMises.dist(mu=0.0, kappa=100.0), 0.0).eval(): -inf → 1.382390229428057 (SciPy 1.382390243648071; test, two cases, both failed before the fix) |
post |
| OjG (Go JSONPath) | #250 length() of a string counts characters, as RFC 9535 does, instead of bytes, so $[?length(@)==2] returns "жж" and "☺☺" as well as "ab" |
["a","ab","жж","☺☺","😀"]: $[?length(@)==2] ["ab"] → ["ab","жж","☺☺"]; $[?length(@)==1] ["a"] → ["a","😀"] (test, one row, the length was 10 before the fix) |
post |
| OjG (Go JSONPath) | #251 <= and >= in a filter match equal operands of any type, as RFC 9535 defines them, so $[?@.a<=null] and $[?@.a>=true] return {"a":null} and {"a":true} instead of nothing |
[{"a":null},{"a":true},{"a":false}]: $[?@.a<=null] [] → [{"a":null}]; $[?@.a>=true] [] → [{"a":true}] (test, fifteen rows, the ten match rows failed before the fix) |
post |
| OjG (Go JSONPath) | #252 a negative number right after an operator in a filter parses, so $[?@.a==-1] reads as $[?(@.a == -1)] instead of failing |
$[?@.a==-1]: '==-' is not a valid operation error → parses as $[?(@.a == -1)] (test, eleven rows that failed before the fix and four error rows that pass either way) |
post |
| OjG (Go JSONPath) | #253 \/ and an escaped UTF-16 pair in a quoted name or string parse as one character, so $["\/"] reads as $['/'] and $["\ud83d\ude00"] finds the key for U+1F600 |
$["\/"]: 0x2f (/) is not a valid escaped character error → parses as $['/']; $["\ud83d\ude00"] on {"😀":1}: [] → [1] (test, eleven rows that failed before the fix and six that pass either way) |
post |
| OjG (Go JSONPath) | #254 an escaped UTF-16 pair in a JSON string decodes as one character in the oj, gen and sen parsers and the oj and sen tokenizers, so "\ud83d\ude00" reads as U+1F600 instead of two U+FFFD |
"\ud83d\ude00": "\ufffd\ufffd" → "\U0001f600" (oj parser test, five rows that failed before the fix and eight that pass either way) |
post |
| OjG (Go JSONPath) | #256 a tab, a line feed or a carriage return counts as blank space wherever a space already did, so $[?\n@.a] reads as $[?(@.a)] instead of failing |
"$[?\n@.a]" in a Go string: '' is not a value or function error → parses as $[?(@.a)] (test, eight rows that failed before the fix and two error rows that pass either way) |
post |
| OjG (Go JSONPath) | #255 match() tests the whole string when its pattern has a top-level |, as RFC 9535 does, so $[?match(@, 'ab|cd')] returns "ab" and "cd" and no longer "abx" and "xcd" |
$[?match(@, 'ab|cd')] over ["ab","cd","abx","xcd"]: all four returned → "ab" and "cd" only (test, three rows that failed before the fix and nine that pass either way) |
post |
| NumPyro (probabilistic programming on JAX) | #2376 the supports of Uniform, LogUniform and DiscreteUniform are computed from low and high instead of stored as a second copy, so low and high are carried once and an updated distribution reports the updated support (a refactor) |
Uniform leaves 4 → 2, LogUniform 8 → 4; after a gradient step on Uniform(1.0, 3.0): low 1.5, support.lower_bound 1.0 → 1.5, support(1.2) True → False (test, two cases, both failed before the change) |
post |
| NumPyro (probabilistic programming on JAX) | #2363 Levy.support is derived from loc, so a Levy that crosses jit, vmap or lax.map keeps its support instead of getting None |
jax.jit(lambda: dist.Levy(0.0, 1.0, validate_args=True))(): support is None and log_prob(2.0) raises AssertionError → GreaterThan(lower_bound=0.0) and -2.2086592 (SciPy -2.2086593040445908; test, four cases, all failed before the fix) |
post |
| Project | Merged change | Result | Story |
|---|---|---|---|
| AIBrix (vLLM) | #3126 Hugging Face LoRA adapters download into the adapter directory | first and repeat loads hand the engine the same path | post |
| AIBrix (vLLM) | #3130 runtime sidecar accepts the hf:// adapter URLs the webhook already allows |
hf:// adapters load instead of failing with "Unsupported URL scheme" |
post |
| AIBrix (vLLM) | #3132 per-model HTTPRoute matches /v1/images/generations |
image requests with no routing strategy reach the model instead of finding no route | post |
| AIBrix (vLLM) | #3131 runtime sidecar loads adapters from a mounted local path (/...) that the webhook already allows |
local-path adapters load with the sidecar on, under the same name check as other sources | post |
| AIBrix (vLLM) | #3133 model-router custom paths reach the HTTPRoute once each; the dedup set was never written | repeated or default paths no longer count toward the 64-match route limit | post |
| AIBrix (vLLM) | #3136 ModelAdapter EndpointSlices declare the address family of their Pods instead of always IPv4 | IPv6 Pods: EndpointSlice rejected, adapter Service without endpoints → endpoints created | post |
| AIBrix (vLLM) | #3137 POST /CreateUser for an existing user returns the stored limits instead of echoing the request |
user stored at 50 RPM, repeated create with 100: response 100 → 50 | post |
| AIBrix (vLLM) | #3147 the runtime serves aibrix:prompt_tokens_total, aibrix:generation_tokens_total and aibrix:request_success_total, because the six counter rules are keyed by the family name the parser returns, without _total |
vllm:prompt_tokens_total 12.0 through the collector: no aibrix: line → aibrix:prompt_tokens_total 12.0 (one test row per rule) |
post |
| AIBrix (vLLM) | #3146 the PodSet deletes a Failed or Succeeded pod so the next reconcile can create its replacement, because the pod keeps its index-derived name and the create returns AlreadyExists |
Failed or Succeeded pods at indexes 1 and 2: both stay, creates skipped → both deleted with an event each, both back on the next reconcile (test with a Failed and a Succeeded subtest, both failed before the fix) | post |
| AIBrix (vLLM) | #3169 a RayClusterFleet with an omitted, empty or partly set strategy gets a 25% RollingUpdate where the controller reads it, because the CRD default fills only an absent key |
omitted or {}: no ReplicaSet; only a type: nil pointer panic; empty rollingUpdate: "nil value for IntOrString" → one ReplicaSet each (the 4 new specs failed before the fix) |
post |
| OME | #1231 a computed environment override replaces a ConfigMap or Secret source instead of leaving it beside the new value | engine Deployment no longer rejected for value plus valueFrom |
post |
| OME | #1232 the manager registers the BenchmarkJob controller and its validating webhook again, after a refactor dropped them while the chart still declared the webhook | BenchmarkJob create: failed calling webhook → admitted and reconciled into a Job; new test checks every declared webhook path is served |
post |
| OME | #1234 runtime env overrides that switch between value and valueFrom (or between sources) no longer merge into an entry holding both |
3 of 4 merge cases rejected by Kubernetes validation → 0 | post |
| Xinference | #5726 async reasoning streams keep chunks that carry no text, so a reply with no text still ends with its finish_reason and usage |
empty reply on vLLM, SGLang or MLX: body only data: [DONE] → stop and usage chunks |
post |
| Xinference | #5731 a non-stream reasoning reply that ends inside the thinking block no longer starts reasoning_content with the start tag, so it matches the stream |
non-stream reply with finish_reason: "length" and no end tag: reasoning_content "<think>\nThe user asks" → "\nThe user asks" |
post |
| Xinference | #5733 a non-stream reasoning reply with text before the start tag no longer starts content with a stray >: content is cut at the end tag, not at an index counted from the start of the reply |
reply with a newline before <think>: content ">\n\nHello" → "\n\nHello"; with nothing after the end tag, ">" → "" |
post |
| Xinference | #5746 a streamed reasoning reply that starts with a whitespace-only chunk no longer gets the start tag inserted ahead of that whitespace, so a <think> the model writes next stays out of reasoning_content |
stream chunks ["\n", "<think>", "a", "</think>", "b"]: reasoning_content "\n\n<think>a" → "a" |
post |
| Xinference | #5751 a non-stream reasoning reply that has no <think> before </think> keeps a <think> in the answer as content, so the reply no longer fails or returns the answer's block as reasoning_content |
reply a</think>Use the <think> tag: IndexError (HTTP 500) → reasoning_content "a"; reply a</think>b<think>c</think>d: reasoning_content "c" → "a" |
post |
| Xinference | #5756 a streamed reasoning reply that has already ended its reasoning keeps a later </think> in the answer as content, so it is no longer read as the end of the reasoning a second time |
stream chunks ["a", "</think>", "b", "</think>", "c"]: content "bc" → "b</think>c"; chunks ["a</think>b", "c</think>d"]: reasoning_content "\nac" → "\na" |
post |
| Project | Merged change |
|---|---|
| oh-my-codex | #3791 HUD caches the git branch label until HEAD, refs or config change · all 4 merged |
| oh-my-claudecode | #4373 hooks start file-context injectors only for the tools they act on · all 7 merged |
| oh-my-openagent | #10133 git diff stats read NUL-separated, so renamed and quoted paths keep their counts · all 2 merged |
| Project | What | Result | Story |
|---|---|---|---|
| OpenCV (calib3d) | #30316 validateDisparity skips out-of-range disparities instead of indexing past the row buffers |
fixes a crash reported in April 2024 (#25367); two earlier fixes were closed | post |
| slam_toolbox (ROS 2) | #905 node removal stops copying the pose graph's vertex map and edge list | 84x faster per removal on a 19,870-vertex map (benchmark) | |
| mediamtx (camera streaming) | #6359 fMP4 recordings of a VP9 track store the frame height instead of repeating the width | 1920x804 stream: init segment Height: 1920 → Height: 804 |
post |
| chess-sim2real-benchmark · dataset | render generator and 20,000-image sim-to-real benchmark | a colour-convention fix took per-square accuracy from 54.4 to 85.2 | post |
| Model | Method | Result |
|---|---|---|
| Qwen3.8-27B-OBLITERATED-DYN-GGUF | GGUF quants of OBLITERATUS/Qwen3.8-27B-OBLITERATED with a quant type per tensor group, chosen by measured KL-divergence sensitivity inside a fixed file-size budget | same size as the upstream Q4_K_M (16.81 GB): mean KL divergence from BF16 0.0348 → 0.0195, same top token 93.84% → 95.33%; CPU prompt processing 0.85x |
