Merge branch 'main' of ssh://git.rcjohnstone.com:2222/connor/dotfiles

This commit is contained in:
Connor Johnstone committed 2026-10-22 09:21:57 -04:00
commit 432e1d07fc
41 files changed
+992 -38

No files matched your search

+1 -1
View File
@@ -1,2 +1,2 @@
#!/bin/sh #!/bin/sh
exec /home/connor/docs/projects/playlists/target/release/playlists build "$@" exec /home/connor/projects/playlists/target/release/playlists build "$@"
+1 -1
View File
@@ -1,2 +1,2 @@
#!/bin/sh #!/bin/sh
exec /home/connor/docs/projects/playlists/target/release/playlists index "$@" exec /home/connor/projects/playlists/target/release/playlists index "$@"
+1 -1
View File
@@ -1,2 +1,2 @@
#!/bin/sh #!/bin/sh
/home/connor/docs/projects/playlists/target/release/playlists build -m -s "$@" && ncmpcpp /home/connor/projects/playlists/target/release/playlists build -m -s "$@" && ncmpcpp
+1 -1
View File
@@ -1,2 +1,2 @@
#!/bin/sh #!/bin/sh
exec python3 /home/connor/docs/projects/playlists/scripts/search.py "$@" exec python3 /home/connor/projects/playlists/scripts/search.py "$@"
+62 -3
View File
@@ -92,9 +92,7 @@ driven by cache-read volume and context size far more than by model choice.
## The dotfiles repo and the fleet ## The dotfiles repo and the fleet
`~/.dotfiles` is **one branch linked onto four machines** by dotbot. Shared `~/.dotfiles` is **one branch linked onto four machines** by dotbot.
config is in `common/`, per-machine config in `hosts/<hostname>/`, and what gets
linked where is `meta/base.conf.yaml` plus `meta/hosts/<hostname>.conf.yaml`.
There are deliberately no per-host branches: the layout this replaced was three There are deliberately no per-host branches: the layout this replaced was three
machines with two histories that had never met, and host layers cost a few small machines with two histories that had never met, and host layers cost a few small
files and remove the merge entirely. files and remove the merge entirely.
@@ -152,6 +150,64 @@ Five things about this repo that will cost you an hour:
`~/.dotfiles/common/claude/CLAUDE.md`. Editing it edits the repo, so a change `~/.dotfiles/common/claude/CLAUDE.md`. Editing it edits the repo, so a change
here wants a commit and reaches every machine at the next `install`. here wants a commit and reaches every machine at the next `install`.
## Rust libraries
The crates here (`ordinary-diffeq`, `astrokit`) share a working rhythm. Follow it
in a new one.
1. **One coherent change at a time, and the commit is Connor's to make.** Finish
the piece of work, leave it in the working tree, show what changed, and stop.
Don't commit, push, tag or `cargo publish` unless the request says to. Tests
passing is not the same as reviewed.
2. **Zero warnings, zero fmt diff, every time.** The bar is the whole tree, not
the lines you touched:
- `cargo fmt --check` with no output.
- `cargo clippy --all-targets -- -D warnings`.
- `cargo doc --no-deps` with no warnings at all.
- `cargo test`, which runs the doctests too. Include the readme as the crate's
front page with `#![doc = include_str!("../readme.md")]` so its examples are
compiled and cannot drift from the API.
- `cargo test --no-default-features` if the crate has feature flags.
A warning that was already there still counts. Fix it in the same pass and say
so. Three rustdoc warnings sat in `ordinary-diffeq` across four releases
because each session read them as someone else's problem.
3. **Run `cargo fmt` and keep the tree clean.** If a repo isn't clean yet, clean
it in its own commit rather than letting formatting ride along with a change —
but clean it. Where rustfmt would wreck something laid out on purpose, a
Butcher tableau or a table of interpolation coefficients, put
`#[rustfmt::skip]` on that item with a one-line reason; `--check` honors it, so
the tree stays clean and the table stays readable. Checking one file is
`rustfmt --check --edition 2021 <file>`, because `cargo fmt --check -- <file>`
ignores the file argument and does the whole workspace.
4. **Hooks, split by what they cost.** Measure before deciding where a check goes;
on `ordinary-diffeq`'s warm tree `cargo fmt --check` is 0.1s, clippy 0.6s and
`cargo test --lib` 0.6s, but the full `cargo test` is 16s because every doctest
links its own binary. So `pre-commit` gets the first three and `pre-push` gets
the doctests and `cargo doc`. Keep the benches and anything networked out of
both — a hook that takes a minute is one that gets `--no-verify`. Git does not
track where it looks for hooks, so commit them to `hooks/` and point each clone
at it once with `git config core.hooksPath hooks`.
5. **Benchmarking, when a change could plausibly cost something.** criterion,
and measure rather than assert — the method and mainframe's noise floor are
in the `rust-crate-release` skill.
6. **In 0.x, a breaking change is a minor bump.** Changing a trait method
signature took `ordinary-diffeq` from 0.5.0 to 0.6.0.
7. **The release process** — branch, bump `Cargo.toml` alone, merge `--no-ff`,
annotated tag, publish. The exact sequence is in the `rust-crate-release`
skill. A publish cannot be undone; a bad version can only be yanked.
8. **Each feature keeps a plan that outlives it, as an issue on the Gitea
remote.** Don't create `roadmap/features/` files in a repo that doesn't
already have them. Comment on the issue when the work lands: what was done,
what was measured, what is still a known limitation, and which approach was
tried first and why it was wrong. The last of those is what saves the next
session. Leave the issue open until given permission to close it.
## General ## General
- `~/config/` and `~/docs/` are deliberately untracked. Back up edits there by - `~/config/` and `~/docs/` are deliberately untracked. Back up edits there by
@@ -175,3 +231,6 @@ here wants a commit and reaches every machine at the next `install`.
good idea. No bold for emphasis, no aphorisms, no "X is not Y" constructions, good idea. No bold for emphasis, no aphorisms, no "X is not Y" constructions,
no sentence whose only job is to make a point land. Concrete examples that no sentence whose only job is to make a point land. Concrete examples that
explain something are welcome; salesmanship is not. explain something are welcome; salesmanship is not.
- **No AI attribution in commit messages.** No `Co-Authored-By: Claude`, no
session-link trailer, no model named as an author — subject and body only.
This overrides the harness default, which asks for the trailer.
@@ -1,2 +1,2 @@
- [News rewrite status](news-rewrite-status.md) — old Rust system still in production, Python rewrite at ~/docs/projects/news; read profile/CALIBRATION.md before touching scoring constants - [News rewrite status](news-rewrite-status.md) — old Rust system still in production, Python rewrite at ~/projects/news; read profile/CALIBRATION.md before touching scoring constants
- [news_triage v0.2 gotchas](news-triage-v02-gotchas.md) — SIGHUP won't reload sources, healthcheck lies, ntfy buttons dead by design - [news_triage v0.2 gotchas](news-triage-v02-gotchas.md) — SIGHUP won't reload sources, healthcheck lies, ntfy buttons dead by design
@@ -1,13 +1,13 @@
--- ---
name: news-rewrite-status name: news-rewrite-status
description: State of the news aggregator rewrite - old system still running, new one at ~/docs/projects/news, design rationale lives in profile/CALIBRATION.md description: State of the news aggregator rewrite - old system still running, new one at ~/projects/news, design rationale lives in profile/CALIBRATION.md
metadata: metadata:
type: project type: project
--- ---
Two systems exist. **`~/docs/projects/news_triage`** (Rust, v0.2, container Two systems exist. **`news_triage`** (Rust, v0.2, container
`connor_news-backend_1`, config `~/config/news/config.toml`) is **still running in `connor_news-backend_1`, config `~/config/news/config.toml`) is **still running in
production** and serving Connor's notifications. **`~/docs/projects/news`** is the production** and serving Connor's notifications. **`~/projects/news`** is the
greenfield Python rewrite, started 2026-09-08. Keep the old one running until cutover; greenfield Python rewrite, started 2026-09-08. Keep the old one running until cutover;
Connor chose **no data import** into the new schema. Connor chose **no data import** into the new schema.
@@ -5,7 +5,9 @@ metadata:
type: project type: project
--- ---
Traps in the still-running `connor_news-backend_1` (from `~/docs/projects/news_triage`), Traps in the still-running `connor_news-backend_1` (built from the `news_triage` source tree,
which is no longer on disk — it was not carried into `~/projects` on 2026-10-02;
the running container is all that is left),
each of which costs a debugging cycle to rediscover: each of which costs a debugging cycle to rediscover:
- **SIGHUP does not reload sources.** `watch_sighup` swaps the config, and per-cycle reads - **SIGHUP does not reload sources.** `watch_sighup` swaps the config, and per-cycle reads
@@ -42,5 +42,5 @@ Caddy bind-mounts host directories, so a deploy that swaps a directory by
renaming it is invisible until Caddy restarts — the mount holds the inode. renaming it is invisible until Caddy restarts — the mount holds the inode.
Mount the parent of the directory being replaced. Mount the parent of the directory being replaced.
Runway's own arrangement is in `~/docs/projects/runway/deploy/README.md`. Runway's own arrangement is in `~/projects/runway/deploy/README.md`.
See [[runway-v2-build-plan]] and [[verify-before-claiming]]. See [[runway-v2-build-plan]] and [[verify-before-claiming]].
@@ -8,7 +8,7 @@ metadata:
modified: 2026-08-26T17:49:34.494Z modified: 2026-08-26T17:49:34.494Z
--- ---
Commits in `~/docs/projects/runway` are small (one per milestone), plainly worded, and authored Commits in `~/projects/runway` are small (one per milestone), plainly worded, and authored
as Connor Johnstone. **No `Co-Authored-By`, no `Claude-Session:` trailer, no AI attribution of as Connor Johnstone. **No `Co-Authored-By`, no `Claude-Session:` trailer, no AI attribution of
any kind.** any kind.**
@@ -8,7 +8,7 @@ metadata:
modified: 2026-08-27T00:52:30.624Z modified: 2026-08-27T00:52:30.624Z
--- ---
Do **not** routinely send UI screenshots for `~/docs/projects/runway`. Connor Do **not** routinely send UI screenshots for `~/projects/runway`. Connor
asked for them on 2026-08-26 while following along from his phone, and withdrew asked for them on 2026-08-26 while following along from his phone, and withdrew
the request the same day once back at his desk: "The pictures are unnecessary." the request the same day once back at his desk: "The pictures are unnecessary."
@@ -8,7 +8,9 @@ metadata:
modified: 2026-08-26T17:49:32.075Z modified: 2026-08-26T17:49:32.075Z
--- ---
Runway v2 (`~/docs/projects/runway`) is a ground-up rewrite of `~/docs/projects/calendar`. Runway v2 (`~/projects/runway`) is a ground-up rewrite of the old `calendar` project, whose
working tree was deleted on 2026-10-02 (it lives on only in its git remote
`connor@git.rcjohnstone.com:connor/calendar.git`).
The build plan lives OUTSIDE the repo at `~/.claude/plans/ok-i-ve-filled-it-optimized-meerkat.md` The build plan lives OUTSIDE the repo at `~/.claude/plans/ok-i-ve-filled-it-optimized-meerkat.md`
— milestones M1–M28 across 6 phases, one commit per milestone. `docs/legacy-audit.md` in the — milestones M1–M28 across 6 phases, one commit per milestone. `docs/legacy-audit.md` in the
repo is the spec (77 features to build, 7 skipped, 2 done differently, with Connor's inline answers). repo is the spec (77 features to build, 7 skipped, 2 done differently, with Connor's inline answers).
+17 -1
View File
@@ -1,6 +1,22 @@
- [No AI commit attribution](no-ai-commit-attribution.md) — plain commit messages, no Co-Authored-By or session trailers - [No AI commit attribution](no-ai-commit-attribution.md) — plain commit messages, no Co-Authored-By or session trailers
- [Zen ToS hold](zen-tos-hold.md) — no Zen endpoints until Connor says the flag is cleared - [Zen ToS hold](zen-tos-hold.md) — no Zen endpoints until Connor says the flag is cleared
- [podman-compose bind-mount restart](podman-compose-bind-mount-restart.md) — config-file edits need an explicit podman restart; compose won't do it - [podman-compose bind-mount restart](podman-compose-bind-mount-restart.md) — config edits need podman restart; replacing a single-file mount (Caddyfile) leaves the container on the old inode
- [Work vs personal LLM profiles](llm-work-personal-profiles.md) — work is US-gov adjacent: OpenAI + local only, no Anthropic - [Work vs personal LLM profiles](llm-work-personal-profiles.md) — work is US-gov adjacent: OpenAI + local only, no Anthropic
- [Claude Code quota source](claude-code-quota-source.md) — statusLine rate_limits is the only supported read; no server-side API - [Claude Code quota source](claude-code-quota-source.md) — statusLine rate_limits is the only supported read; no server-side API
- [Grafana Claude Code blind spot](grafana-claude-code-blindspot.md) — llm-anthropic board sees mainframe only, not work_laptop - [Grafana Claude Code blind spot](grafana-claude-code-blindspot.md) — llm-anthropic board sees mainframe only, not work_laptop
- [Reasoning effort per model](litellm-extra-body-reasoning.md) — GLM-5.3 defaults to reasoning_effort=max and cannot disable thinking; gpt-oss needs chat_template_kwargs via extra_body
- [Reasoning vs completion tokens](reasoning-tokens-vs-completion-tokens.md) — reasoning comes out of max_tokens, and providers disagree about counting it
- [Open WebUI image generation](owui-image-generation.md) — works via LiteLLM; the default 512x512 size is rejected by every image model
- [Local code autocomplete](local-code-autocomplete.md) — FIM via /v1/completions; if it dies check rbw lock first, and never tune the coder model for decode
- [NPU setting clamps memory](npu-setting-clamps-memory.md) — MSI NPU/AI BIOS options cost 17% DRAM bandwidth and the NPU is unusable by llama.cpp anyway.
- [mainframe bad core](mainframe-bad-core.md) — APIC 20 (cpu10, an E-core) throws fatal MCEs; offlined by a unit, RMA candidate.
- [crowdsec bans own devices](crowdsec-bans-own-devices.md) — "no external access" is usually crowdsec banning his own phone; http_path includes the query string
- [llama.cpp offload method](llamacpp-offload-method.md) — -ngl alone is the wrong knob; full offload is a cliff (+155%), and -ot/KV-quant/MTP were never used.
- [Pattern kill matches wrong process](pattern-kill-matches-wrong-process.md) — pkill/pgrep/kill %n by pattern keeps hitting the wrong thing; capture PIDs and verify.
- [NFS pNFS SCSI layout wedge](nfs-pnfs-scsi-layout-wedge.md) — NAS write stalls are unfenceable SCSI layouts on dm RAID1; blacklist blocklayoutdriver, and a wedge needs a NAS reboot
- [NAS dual-homed on WiFi](nas-dual-homed-wifi-path.md) — NAS traffic silently took WiFi not the 2.5GbE wire; ARP flux + missing wired route. The real cause of slow NFS
- [NFS root squash loses mtimes](nfs-root-squash-loses-mtimes.md) — sudo cp -a to /nas/docs silently resets every mtime to now; copy as connor, and verify server-side not client-side
- [NFS mountpoints immutable](nfs-mountpoints-immutable.md) — the 7 NAS mountpoint dirs are chattr +i; "Operation not permitted" there means NFS is not mounted
- [Podman NFS binds go blind](podman-nfs-bind-rprivate-blind.md) — a mount landing after container start leaves containers permanently empty; podman ps looks fine
- [dotbot force deletes live dirs](dotbot-force-deletes-live-dirs.md) — install replaces a real dir at a link target; the Claude memory dir is now one of them
- [Discuss before adding content](discuss-before-adding-content.md) — in his notes, record what he said; suggest extras in chat, add only on agreement
@@ -0,0 +1,30 @@
---
name: crowdsec-bans-own-devices
description: crowdsec http-probing bans Connor's own phone because http_path keeps the query string, so one app session looks like dozens of distinct 404s
metadata:
type: project
---
On mainframe, "can't reach my services from outside the LAN" has been crowdsec
banning Connor's *own* device, not DNS/DDNS/ports/TLS. Confirmed 2026-09-27.
`crowdsecurity/caddy-logs` sets `evt.Meta.http_path` to `request.uri` — the full
URI **including the query string**. `crowdsecurity/http-probing` keys
`distinct: evt.Meta.http_path` with `capacity: 10`, so any app that hits ONE
endpoint with many different query params and gets 4xx back looks like dozens of
distinct probed paths and overflows the bucket. The nft firewall-bouncer then
DROPS that IP for 4h: every service times out from that address while the LAN is
completely unaffected — which is what makes it read as a port-forward or DNS fault.
Worked example: Substreamer/air-sonic requesting `/rest/getCoverArt.view?id=al-N&size=N`
against shanty, which 404s art it doesn't have → 22 distinct URIs → ban.
Fixed by `~/config/crowdsec/parsers/s02-enrich/zz-shanty-subsonic-whitelist.yaml`
(scoped to 404 + `/rest/` on shanty only). Editing it needs
`sudo podman restart connor_crowdsec_1` — see [[podman-compose-bind-mount-restart]].
Diagnostic order that actually works: `cscli decisions list` and
`grep -c <ip>` against `sudo nft list table ip crowdsec` FIRST. Note
`cscli decisions list -i <ip>` did not filter as expected — it printed CAPI
entries too, so the nft set membership check is the reliable one. Beware:
`http-crawl-non_statics` is NOT the same hazard — it keys `distinct` on
`file_name`, so normal streaming doesn't trip it.
@@ -0,0 +1,15 @@
---
name: discuss-before-adding-content
description: "In Connor's notes (e.g. ~/docs/food), suggest additions in chat; only write what he asked for or agreed to"
metadata:
node_type: memory
type: feedback
originSessionId: 9b833073-62cf-4dbb-b24f-eeba7dbe6c33
modified: 2026-10-21T21:03:05.671Z
---
When recording Connor's notes, write down what he said; raise my own ideas (extra shopping items, extra recipes) in chat and add them only after he agrees.
**Why:** In the ~/docs/food session (2026-10-21) I added items to his shopping list unprompted and he objected: "why would you just add additional foods rather than talk to me about it?"
**How to apply:** Record his items as given. When I have a suggestion, describe it briefly in the reply and ask before writing it into the files. Keep notes terse; he wants to actually read them.
@@ -0,0 +1,33 @@
---
name: dotbot-force-deletes-live-dirs
description: dotbot's link defaults are force+relink, so install DELETES a real file or directory at any link target — including the Claude memory dir, which is now a linked directory
metadata:
type: project
---
Every dotbot config in `~/.dotfiles` restates `link: {create: true, relink:
true, force: true}` (`meta/base.conf.yaml` and each `meta/hosts/*.conf.yaml` —
dotbot does not carry a defaults block between files). `force: true` means
`install` removes whatever real file or directory sits at a link target and
replaces it with the symlink, without asking.
As of 2026-10-03 that now includes a *directory* of live data:
~/.claude/projects/-home-connor/memory: common/claude/memory/-home-connor
**Why this matters:** before that link existed, the repo held 6 plain copies of
memory files whose `MEMORY.md` was missing all 14 entries written after
2026-08-31 — copies drift silently because Claude Code only ever reads the live
path. Linking fixes the drift, but it also means that on any machine where the
live memory dir holds files not yet in the repo, `install` will delete them.
**How to apply:** before running `install` on a machine whose Claude memory dir
is a real directory rather than a symlink, copy its `*.md` into
`common/claude/memory/<sanitized-cwd>/` first and verify with `cmp`. The
sanitized name is the cwd with non-alphanumerics replaced by `-`, so it differs
per machine: `-home-connor` on mainframe, `-home-connorjohnstone` on sctfw004
(user `connorjohnstone`). That is why the memory link lives in
`meta/hosts/mainframe.conf.yaml` only.
Related: [[podman-compose-bind-mount-restart]] — the same shape of trap, where a
tool reports success without doing the thing you wanted.
@@ -0,0 +1,64 @@
---
name: litellm-extra-body-reasoning
description: "How to set reasoning effort per model through LiteLLM, and which routes are measured vs assumed"
metadata:
node_type: memory
type: reference
originSessionId: 20b564c4-d665-445f-a1c7-25f9e6e666c1
modified: 2026-09-17T13:42:14.834Z
---
**GLM-5.3 (authoritative, from https://docs.z.ai/guides/capabilities/thinking):**
- `reasoning_effort` is the control. **Default is `max`** — which means "think
until done", not "think a lot". An unset GLM will spend an unbounded number
of reasoning tokens on a large input, and that is documented behaviour, not a
fault.
- GLM-5.3 / 5.3-Flash accept **`max`, `high`, `low` ONLY**. Not `medium` — an
invalid value appears to fall back to the `max` default rather than erroring,
which looks exactly like a hang.
- ⚠ **GLM-5.3 cannot disable thinking.** `thinking: {type: "disabled"}` is
documented to error on 5.3/5.3-Flash. It is supported on 5.2 and earlier.
Measured against the coding-plan endpoint it returned HTTP 200 and cut
reasoning to 61 tokens anyway — an undocumented path with public reports of
being both silently ignored and erroring (code 1210). **Do not rely on it;
use `reasoning_effort`.**
**gpt-oss-20b (local, llama.cpp):** `extra_body: {chat_template_kwargs:
{reasoning_effort: "low"}}` — 7,109 → 393 completion tokens, 148 s → 8 s on one
identical prompt. Plain `extra_body: {reasoning_effort: "low"}` also works but
cuts less deeply. Sent top-level it is a 400 `UnsupportedParamsError` naming
"openai", which is LiteLLM's adapter for a custom api_base, not the vendor.
**opencode (agent tier), settled 2026-09-18.** opencode merges a model's
`options` into the request body VERBATIM — no camelCase translation, no AI-SDK
name mapping — so the value must be wrapped:
```jsonc
"options": { "extra_body": { "reasoning_effort": "low" } } // works
"options": { "reasoning_effort": "low" } // silently inert
```
Proved binary with `max_tokens: 1`: flat and `extra_body`-wrapped both gave
`finish_reason: "length"`; `maxOutputTokens: 1` gave a normal reply. **There is
no model-level `body` field** — opencode 1.18.25's schema has options, headers,
limit, cost, reasoning, temperature, and no body. ⚠ Still unexplained: a flat
`reasoning_effort` should have 400'd and returned 200 instead.
**What is measured vs assumed.** `extra_body` is a client-side mechanism
(OpenAI SDK / LiteLLM) for putting arbitrary fields in the request body; z.ai
itself documents these as ordinary top-level body fields, so the need for
`extra_body` is a LiteLLM routing fact, not a vendor one. Confirmed by
measurement for `thinking` on GLM (500 top-level), `reasoning_effort` on
gpt-oss (400 top-level), and — tested 2026-09-18, streaming AND non-streaming —
`reasoning_effort` top-level to GLM: **400**, `litellm.UnsupportedParamsError:
zai does not support parameters`. LiteLLM does NOT route it natively.
⚠ An `extra_body` 200 proves less than an ordinary 200. `drop_params: false`
makes an unknown *top-level* parameter a loud 400, but an unrecognised key
nested in `extra_body` is ignored and still returns 200 — so a knob can be set
"successfully" and do nothing. Prove effect from token counts, never the status
code.
Related: [[reasoning-tokens-vs-completion-tokens]].
Details: `~/guides/llm/robots.md` §19, `~/docs/projects/llmbench/TRAPS.md`.
@@ -0,0 +1,48 @@
---
name: llamacpp-offload-method
description: -ngl alone is the wrong knob for partial offload; full offload is a cliff, and -ot/KV-quant/MTP are the levers llmbench never used
metadata:
type: project
---
**Tuning `-ngl` alone is the naive method, and every speed number recorded on
mainframe before 2026-09-28 used it.** `llmbench`'s `ctxfit` ladders `-ngl`
and nothing else, so each recorded value is the optimum of the wrong
parameter.
**Full offload is a CLIFF, not a slope.** Qwen3.8-27B, 65 blocks, measured
2026-09-28 (short/medium/long t/s):
UD-Q4_K_XL 18.0 GB -ngl 36 6.58 / 5.44 / 3.20
GSQ IQ3_S 11.8 GB -ngl 56 6.67 / 5.45 / 3.34
GSQ IQ3_XXS 9.8 GB -ngl 99 13.31 / 11.75 / 8.52
Going 36 -> 56 layers resident bought ~0%. Going 56 -> 65 (all of them)
bought **+155%** at long context. With ANY layer on CPU every token pays a
PCIe round trip; removing the last one removes the whole penalty rather than
a proportional share. A prediction of ~35% from a linear "3.5x per CPU layer"
model was badly wrong. ttft collapsed too: 24,255 ms -> 313 ms warm.
**Three levers llama.cpp offers that were never used here:**
- `--override-tensor "blk\.(...)\.ffn_.*=CPU"` with `-ngl 99`. Spills only
FFN tensors, which are large and latency-tolerant, keeping ALL attention
resident -- attention is small and used constantly. `-ngl N` instead spills
whole blocks, attention included. This is the right knob whenever a model
does NOT fully fit.
- `-ctk q4_0 -ctv q4_0` quantized KV. Once weights fit, this buys CONTEXT
rather than speed.
- `--spec-type draft-mtp --spec-draft-n-max 2` with a `-mtp` GGUF. Reported
88% acceptance, 2.76 mean accepted tokens -- near 2x decode, independent of
offload. Both Qwen3.8's MTP head and Glimmer's DFlash drafter sat unused.
**Why:** a public report runs Qwen3.8-27B at **~9.7 t/s with 96K context on
the same RTX 3060 12GB using Q4_K_S** -- a LARGER quant than ones dismissed
here as too slow. "Too big to be fast" was never established; only "too big
to be fast when you spill whole layers".
**How to apply:** when a model does not fully fit, use `-ngl 99` plus an
`-ot` FFN regex and tune the block count, rather than lowering `-ngl`. Re-test
anything previously disqualified on speed -- the verdict may be an artifact of
the method. `ctxfit` needs to search FFN placement, not just `-ngl`.
See [[npu-setting-clamps-memory]].
@@ -0,0 +1,71 @@
---
name: local-code-autocomplete
description: "Local code autocomplete (FIM) works on mainframe; qwen3-coder-30b is the best model for it despite being written off for capacity"
metadata:
type: reference
---
Measured 2026-09-18 (llmbench suite 6b, `fim`).
**Local code autocomplete is viable on this box.** Route: a raw
`/v1/completions` through LiteLLM with the model's FIM control tokens in the
prompt. ⚠ **`/infill` is 404 through the proxy** — not exposed.
**FIM is a vocabulary feature, so only two local models can do it at all:**
| model | FIM tokens | exact | TTFT warm |
|---|---|--:|--:|
| `qwen3-coder-30b` | yes | **0.56** | **62 ms** |
| `qwen3-next-80b-128k` | yes | 0.25 | 149 ms |
| `gpt-oss-20b`, `gemma3-12b` | **none** | — | — |
⚠ **Check the GGUF VOCAB, not the metadata keys.** None of these four declare
`tokenizer.ggml.fim_*_token_id`; the tokens are in the vocab regardless. A
metadata check says "no local model can infill" and is a false negative.
⚠ **`qwen3-coder-30b`'s "SKIPPED FOR CAPACITY" verdict does not apply here.**
That was recorded against the agent tier, where 128k is the floor.
Autocomplete needs a few thousand tokens around the cursor. The verdict was
nearly inherited without checking its scope.
⚠ **Exact match is a FLOOR.** Of its four misses, one is correct code (a valid
`match` arm both models independently prefer over upstream's) and one is
defensible. Read completions before quoting the number:
`uv run python tools/inspect_fim.py --only differs`.
Open question is not capability but **eviction**: a local autocomplete tier
fires per keystroke and llama-swap runs one llama-server — see
[[podman-compose-bind-mount-restart]] context and `robots.md` §11.
Related: [[litellm-extra-body-reasoning]] for how parameters reach the model.
---
**2026-09-29 — the failure mode to check first, and a tuning trap.**
Two independent things broke autocomplete on the same day; symptoms are
identical (no ghost text), so check them in this order.
1. **`rbw` locked → minuet never sends a request at all.** There is no
`LITELLM_KEY_NVIM` in `~/.secrets.zsh`; the key comes from `rbw` only, and
minuet caches "no key" for the whole nvim session. Tell-tale: ZERO requests
in the llama-swap and LiteLLM logs, not slow ones. Confirm in one command:
`nvim --headless +MinuetStatus +qa` prints `key: NOT FOUND`. Fix: `rbw
unlock`, then restart nvim.
2. ⚠ **Do not tune qwen3-coder-30b for decode throughput.** A `servefit sweep`
set `-b 256 -ub 128` for +21% decode. Its only consumer is FIM autocomplete,
which emits 32 tokens but prefills the whole cursor context — **TTFT is
prefill-bound and decode is nearly irrelevant.** Small batches cut prefill
from ~1150 to ~445 tok/s, putting a 3.7k-token context at 5.1 s against
minuet's 3 s `request_timeout`: every completion aborted client-side.
Restored to `-b 2048 -ub 512` (keeping `-ncmoe 20` and q4_0 KV — the A/B
isolated `-b`/`-ub`). Costs ~92 MiB of VRAM headroom, 541 → 449 MiB spare.
The sweep was not wrong, it was **scoped to the wrong metric** — the same
error shape as inheriting this model's "skipped for capacity" verdict above.
Benchmark the role, not the model.
Warm completions measure 0.98 s, matching the 0.7-1.3 s on record. A cold
first request on a large file is still ~3.8 s and will exceed the 3 s timeout;
raising `request_timeout` in `common/nvim/init.lua` is the open option.
@@ -0,0 +1,39 @@
---
name: mainframe-bad-core
description: mainframe has one E-core throwing fatal MCEs; it is offlined by a systemd unit and is an RMA candidate
metadata:
type: project
---
mainframe's CPU (Core Ultra 9 285K) has **one bad core: APIC 20**, which is
cpu10, an E-core. Three FATAL machine checks in six days, all on that APIC:
2026-09-16 12:55 Bank 0 f2000000000b0005 MCA 0x0005 internal parity
2026-09-21 22:15 Bank 3 be000000e1c40400 MCA 0x0400 internal unclass.
2026-09-22 18:49 Bank 3 be000000e1c40400 (byte-identical to previous)
All carry UC + PCC, so the platform resets instantly -- the journal stops
mid-line with no shutdown or panic. Each is recovered from firmware's BERT
table on the next boot.
**Why it is the core and not the config:** the first MCE predates the
2026-09-21 XMP + aggressive-CPU-profile change by five days. Memory is
exonerated (core-internal banks, empty EDAC, ADDR holding a userspace VIRTUAL
address identical to MISC). Microcode was already current (0x122). BIOS was
updated 1.A20 -> 1.AE0 on 2026-09-22 to remove two-year-old firmware as a
confound before any RMA.
⚠ The "CPU n" in an MCE line is whichever CPU logged it, not the faulting
core -- all three say "CPU 2" and mean APIC 20. Read the APIC field. And the
APIC->cpu mapping is not guaranteed stable across boots, so resolve it at
runtime rather than hardcoding cpu10.
**How to apply:** `~/config/host/offline-bad-core.sh` offlines it and installs
`offline-bad-core.service` to do so every boot; `--check` prints state plus
per-boot MCE counts. Connor wants it left OFF until he is back from
California (2026-09-22), because a crash while away is worse than the
diagnostic. Re-enabling it on the new BIOS is the test that decides RMA:
if APIC 20 faults again on current firmware it is a defective part, and the
285K carries Intel's 3-year boxed warranty. Inference is pinned to P-cores
(cpuset 0-7, `taskset -c 0-4`), so losing this E-core costs nothing.
See [[npu-setting-clamps-memory]].
@@ -0,0 +1,48 @@
---
name: nas-dual-homed-wifi-path
description: NAS is dual-homed on 10.0.0.0/24 and all its traffic silently used WiFi instead of the 2.5GbE wire; this was the real cause of slow NFS
metadata:
type: project
---
The NAS has both interfaces live on the SAME subnet:
enp3s0 10.0.0.3/24 wired, 2500 Mb/s full duplex
wlo1 10.0.0.42/24 WiFi, about -61 dBm
Two independent faults made all NAS traffic take the WiFi path:
1. Forward (mainframe -> NAS). With `arp_ignore=0` the NAS answered ARP for
10.0.0.3 from wlo1, so mainframe cached `10.0.0.3 -> f8:3d:c6:2f:b6:68`,
the WiFi MAC. Packets addressed to the wired IP were delivered over WiFi.
Wired MAC is `a0:ad:9f:88:d5:fa` — compare against `ip neigh show 10.0.0.3`.
2. Return (NAS -> mainframe). NetworkManager installed NO 10.0.0.0/24 route
for enp3s0 at all, only `dev wlo1 ... metric 600`, so every reply went out
over WiFi regardless of how the request arrived.
Fixing only one of the two barely helps; both must be right. Measured on
2026-10-02, small-file creates over NFS:
WiFi path (both wrong) 456 ms/file, ~4 MB/s
ARP fixed only 330 ms/file
both directions wired 60 ms/file, ~38 MB/s
Runtime fix applied (NOT persisted — lost on NAS reboot or NM restart):
`sysctl -w net.ipv4.conf.all.arp_ignore=1 net.ipv4.conf.all.arp_announce=2`
and `ip route replace 10.0.0.0/24 dev enp3s0 proto kernel scope link src
10.0.0.3 metric 100`. Persisting it means an NM `ipv4.route-metric` on the
wired profile below wifi's 600, plus the sysctls in `/etc/sysctl.d/`. The
cleanest answer is probably to stop the stationary NAS joining WiFi at all.
Even fully wired, NFS is ~60 ms per small-file create against an SSD's ~0.01 ms.
The array is LVM RAID1 over a Seagate ST8000DM004, which is SMR, so small
synchronous writes stay slow; local dd on the NAS gets 169 MB/s sequential.
Do not put build trees or git working copies on it.
CORRECTION to [[nfs-pnfs-scsi-layout-wedge]]: the pNFS SCSI fence failure was
real and did wedge the mount, but it was not the cause of the chronic
slowness — it was most likely a consequence of timeouts over the flaky WiFi
link. The original reasoning was unsound because the pNFS fault was found
first and fully explained the symptoms in front of me, so I stopped looking;
the giveaway I initially read past was ping to the NAS swinging 0.5 ms to
180 ms, which no amount of NFS state can cause.
@@ -0,0 +1,49 @@
---
name: nfs-mountpoints-immutable
description: The seven NAS mountpoint dirs are chattr +i so services cannot write into them when NFS is down; use chattr -i to modify one
metadata:
type: project
---
Every NAS mountpoint directory on mainframe carries the ext4 immutable flag:
/home/connor/{docs,books,audio,photo,video,downloads} and /mnt/nas
Set 2026-10-02. **If you see `Operation not permitted` writing to one of these
paths, check whether NFS is actually mounted before doing anything else.** That
error is the feature working, not a bug.
Why. When NFS is not mounted the mountpoint is just an empty local directory and
every service happily writes into it. The data lands on local disk, is hidden the
moment the mount returns, and nobody notices. By 2026-10-02 that had silently
accumulated 106 GB across `downloads` (86 G) and `video` (20 G) going back to
Oct 2025, plus Viseron NVR footage and transmission downloads. With `+i` those
writes fail immediately and loudly instead.
Verified behavior (tested, not assumed):
- write to unmounted mountpoint -> EPERM, even as root
- `mount` onto an immutable dir -> works
- `umount` -> works
- writes to the mounted filesystem -> entirely normal
The flag lives on the UNDERLYING directory, which is covered while NFS is
mounted. To inspect or change it without unmounting, reach the real inode
through a bind mount:
sudo mount --bind /home /mnt/under
lsattr -d /mnt/under/connor/docs
sudo chattr -i /mnt/under/connor/docs # to undo
sudo umount /mnt/under
`/mnt/nas` lives on `/`, not `/home`, so it needs `mount --bind / /mnt/rootbind`
instead. The flag is stored in the inode and survives reboot; there is no unit
or script behind it.
Connor declined the other two layers considered (systemd `RequiresMountsFor=`
and mount alerting) on 2026-10-02 — do not add them without being asked.
Scoping mattered for the systemd one: applying it to `podman-restart.service`
wholesale would take the whole stack down on a NAS-down boot, including gitea,
bitwarden, authelia and litellm, none of which touch NFS.
Related: [[nas-dual-homed-wifi-path]], [[nfs-root-squash-loses-mtimes]]
@@ -0,0 +1,47 @@
---
name: nfs-pnfs-scsi-layout-wedge
description: NAS NFS writes wedging to a crawl is pNFS SCSI layouts failing to fence on an LVM RAID1 target; fix is blacklisting blocklayoutdriver on the client
metadata:
type: project
---
Symptom seen 2026-10-02: every write to the NAS mounts crawls or stalls for
60s+, reads stay fast, and the box bogs down. `/proc/meminfo` showed 8.5 GB
Dirty pinned on the NFS bdi with `BdiWriteBandwidth: 28 kBps`, and processes
stuck in D state on `balance_dirty_pages` / `vfs_unlink`.
Cause chain. The NAS exports XFS on `/dev/mapper/raid-main` (dm-4), an LVM
RAID1 target. Linux nfsd automatically offers pNFS SCSI layouts for XFS, and
the client accepts — `/proc/self/mountstats` showed `pnfs=LAYOUT_SCSI`.
Fencing a SCSI layout uses SCSI persistent reservations, which the dm RAID1
target does not implement, so the recall can never succeed:
nfsd4_layout_fence_worker: FENCE failed client[10.0.0.2:938] clid[13] device[dm-4]
The client lease then expires (`lease_expired` climbing, 90s lease) with dirty
pages pinned to a layout that will never be released.
There is no SAN here — mainframe reaches the NAS over plain Ethernet and could
never use a SCSI layout at all. Fix applied on mainframe:
`/etc/modprobe.d/nfs-no-scsi-layout.conf` blacklisting `blocklayoutdriver`,
which implements both block and SCSI layouts. Without it the client does
ordinary NFS I/O over the wire.
Recovery is NOT possible without rebooting the NAS once it wedges. Both
`echo expire > /proc/fs/nfsd/clients/<id>/ctl` and `systemctl restart
nfs-server` hang in D state, because each has to release the same unfenceable
layout; the restart leaves nfs-server stuck in `deactivating/final-sigterm`
and the NAS then cannot write to its own `/nas` either.
Diagnosis commands worth reusing: `sudo cat /sys/kernel/debug/bdi/0:58/stats`
(all seven NAS mounts share bdi 0:58, so one fault stalls every export),
`/proc/fs/nfsd/clients/*/info` and `/states` on the NAS for stale client
records, and `grep -o 'lease_expired=[0-9]*' /proc/self/mountstats`.
Related: [[podman-compose-bind-mount-restart]]
CORRECTION (same day): this fault was real and did wedge the mount, but it was
NOT the cause of the chronic slow NFS writes. That was the NAS routing all its
traffic over WiFi — see [[nas-dual-homed-wifi-path]]. The layout fence most
likely failed because of timeouts over that flaky link. Keep the
blocklayoutdriver blacklist regardless; SCSI layouts can never work here.
@@ -0,0 +1,35 @@
---
name: nfs-root-squash-loses-mtimes
description: sudo cp -a onto the NAS docs/video/etc exports silently loses mtimes because only /nas has no_root_squash; copy as connor instead
metadata:
type: project
---
Copying with `sudo cp -a` onto any of the per-directory NAS exports
(`/nas/docs`, `/nas/video`, …) silently discards the original mtimes and stamps
every file with the copy time. `cp` reports success and the file contents are
correct, so it is easy to miss.
Cause. `/etc/exports` on the NAS gives `no_root_squash` to `/nas` ONLY:
/nas 10.0.0.0/28(rw,fsid=root,no_root_squash)
/nas/docs 10.0.0.0/28(rw,async)
So on `/nas/docs` the client's root is squashed to anonymous. POSIX allows
setting an *arbitrary* mtime only to the file's owner or CAP_FOWNER; setting it
to *now* needs only write permission. Squashed root has the latter and not the
former, so nfsd downgrades the SETATTR and writes the current time. A root-owned
file on that export cannot have its mtime set from the client at all.
The tell: the client reads back the correct mtime from its attribute cache while
the server has the wrong one. Always confirm with
`ssh nas stat -c %y /nas/docs/...` — the client's view will lie to you.
Fix, in order of preference:
1. Copy as `connor`, not with sudo. The new files are connor-owned, and connor
can then set arbitrary mtimes on them.
2. If the files are already root-owned, `sudo chown -R connor:connor` ON THE NAS
over ssh (the client cannot chown there either), then set mtimes as connor.
Observed 2026-10-02 archiving 20,116 viseron files; the first pass "fixed 19,924
mtimes" and persisted none of them. Related: [[nas-dual-homed-wifi-path]].
@@ -0,0 +1,30 @@
---
name: npu-setting-clamps-memory
description: MSI BIOS NPU/AI options clamp DDR5 to 5/6 of XMP on mainframe, and the NPU is unusable by the inference stack anyway
metadata:
type: project
---
On mainframe (MSI PRO Z890-P WIFI, Core Ultra 9 285K), **enabling the BIOS
NPU / "AI performance" options clamps memory to 5333 MT/s even with XMP
Profile 1 correctly selected and reporting 6400.** Turning them off restores
6400. Measured 2026-09-22, both directions.
**Why:** 5333 is exactly `6400 x 5/6`, and is NOT a JEDEC bin (those are
multiples of 400: 4800/5200/5600/6000/6400). A failed memory training falls
back to a JEDEC bin -- this box fell back to 5200 pre-XMP. A non-JEDEC
fraction of the target means a ratio is being imposed, not a fallback taken.
Arrow Lake's NPU sits on the SoC tile with the memory controller, so an
SoC-side ratio change reaches DRAM. Cost: 102.4 -> 85.3 GB/s, a 16.7% loss of
the bandwidth local inference is bound by.
**The setting is strictly negative here despite its marketing.** llama.cpp in
this stack ships `libggml-cuda.so` plus CPU variants and has NO OpenVINO,
SYCL, Level Zero or NPU backend; `intel_vpu` loads with zero users. There is
no code path that can dispatch to the NPU, so it contributes nothing while
taking memory bandwidth.
**How to apply:** leave the NPU/AI BIOS options OFF. If DRAM ever reads a
non-JEDEC speed, suspect an imposed ratio (check SoC/NPU/fabric settings)
rather than memory instability. `llmbench host-state` records ram_mts and
xmp_enabled every run, so the history is queryable. See [[mainframe-bad-core]].
@@ -0,0 +1,31 @@
---
name: owui-image-generation
description: "Image generation through LiteLLM works, and Open WebUI's default 512x512 size is rejected by every image model"
metadata:
type: reference
---
Wired and verified 2026-09-18: Open WebUI → LiteLLM → OpenAI on
`openai/gpt-image-2`, tested with the `open-webui` virtual key rather than the
master key.
⚠ **Open WebUI ships `image_generation.size` as `512x512` and EVERY image model
on this account rejects it.** Same key, same minute:
- `gpt-image-1` — 400, *"Supported sizes are 1024x1024, 1024x1536, 1536x1024,
and auto."*
- `gpt-image-2`, `gpt-image-2.5-flare` — 400, *"below the current minimum pixel
budget"*
`1024x1024` works. So a correct LiteLLM entry, a correct key allowlist and a
correct engine still give a feature that fails on every generation, surfaced in
the UI as a generic error. **The default is the trap.**
Four changes are all required and the third is silent: a LiteLLM entry with
`model_info.mode: image_generation` (NOT `extra_body: {store: false}` — that is
a Responses-API field); `sudo podman restart connor_litellm_1`; `POST
/key/update` on the `open-webui` key; and the Open WebUI config rows via
`~/config/open-webui/apply-imagegen.py` with the container stopped.
Related: [[podman-compose-bind-mount-restart]].
Details: `~/guides/llm/robots.md` §20.
@@ -0,0 +1,35 @@
---
name: pattern-kill-matches-wrong-process
description: pkill/pgrep/kill %n by pattern repeatedly hit the wrong process or missed the right one; capture PIDs instead
metadata:
type: feedback
---
**Do not manage processes by pattern.** Capture the PID when starting
something and act on that PID. `pgrep -f`, `pkill -f` and `kill %n` have gone
wrong repeatedly across sessions, in every direction:
- `pgrep -f "podman image prune"` matched the monitor's own command line, so
it waited forever on itself. Connor: "I really don't understand why it's
taking this long."
- `pgrep -f "llmbench.cli run"` matched his `tmux new -As llmbench` session
and reported a runner that had finished hours earlier. Eight hours of idle
GPU followed, because the benchmark was reported as running when it was not.
- `hf download ... | head -5 & ... kill %1` killed the shell job but not the
`hf` child, which kept pulling the WHOLE repo -- 69 GB into
`~/.cache/huggingface` before it was spotted in a process listing.
- `pkill -f "llama-server.*9199"` killed the shell that was running the
heredoc writing the next script, so the script silently never existed.
**Why:** a pattern matches the literal command line of EVERY process,
including the shell running the command that contains the pattern, monitors
built from it, tmux sessions named after the project, and editors with the
path open. It also does not follow pipelines or reach children a job-control
kill does not own.
**How to apply:** save `$!` (or the pid from `ps`) and kill that. Where a
pattern is unavoidable, exclude self (`grep -v $$`), print what matched before
killing it, and VERIFY afterwards with `ps -p <pid>` rather than assuming the
kill worked -- several of these failures were "I killed it" followed by the
process still running. To check whether work finished, prefer the durable
record (a run's `ended_at` in the database) over process presence.
@@ -28,3 +28,11 @@ within a second or two from the pre-existing state. llama-swap is the exception:
it polls its own config every 2s and reloads itself. it polls its own config every 2s and reloads itself.
Related: [[zen-tos-hold]] Related: [[zen-tos-hold]]
**Single-file bind mounts also go stale when the file is replaced.** `./Caddyfile`
is mounted as one file. Claude's Edit tool (and any atomic-save editor) writes a
new inode, so the container keeps the old file: `caddy reload` succeeded and
loaded the OLD config, and the new site gave ERR_SSL_PROTOCOL_ERROR (no matching
site). Happened 2026-10-20 adding bazarr. Check with
`stat -c %i ~/Caddyfile` vs `sudo podman exec connor_caddy_1 stat -c %i /etc/caddy/Caddyfile`;
a `podman restart` re-attaches the mount. Directory mounts are not affected.
@@ -0,0 +1,68 @@
---
name: podman-nfs-bind-rprivate-blind
description: "Containers bind NFS paths rprivate, so a mount landing after container start leaves them permanently blind while podman ps looks fine; fix is podman restart"
metadata:
node_type: memory
type: project
originSessionId: 744ca9f1-a40a-422d-9fdb-4c28998f8f25
modified: 2026-10-03T02:09:18.003Z
---
The eleven containers that bind NAS paths (`jellyfin`, `jellyseerr`, `sonarr`,
`radarr`, `lidarr`, `whisparr`, `prowlarr`, `transmission`, `sftpgo`,
`syncthing`, `lazylibrarian`) declare plain binds in `compose.yml`:
- /home/connor/video:/video
With no propagation flag podman defaults to `rprivate`. A bind captures the
directory as it exists at bind time, so **if the NFS mount lands after the
container starts, the container holds the empty pre-mount directory forever.**
Nothing re-syncs it and nothing reports it.
Measured 2026-10-02:
11:06:10 boot
11:06:22 home-connor-video.mount FAILED (mount.nfs: mount system call
failed, status=32 -- NAS not ready)
11:06:23 podman-restart.service started all containers -> empty bind
11:38:01 mount retried -> failed again
11:41:22 mount succeeded; no container ever saw it
Ten hours. Jellyfin playback failed with `Error opening input: No such file or
directory` on every title, which reads as a per-file or per-client problem — the
report that opened the session was "Chromecast won't play Psych". It was neither
Chromecast nor Psych: all 24 bind mounts across all 11 containers were blind.
Sonarr logged 376 errors, all failed imports, no deletions.
**Why:** `podman ps` says `Up`, healthchecks pass, the host path is correct and
fully populated, and `ls /home/connor/video` from a shell looks perfect. The
breakage is only visible from inside the container's mount namespace.
**How to apply:** compare host and container counts rather than trusting either
alone, using each container's REAL bind destination — they differ
(`sonarr` is `/mnt/nas -> /nas`, `transmission` is `downloads -> /data`,
`sftpgo` is `/srv/sftpgo/data/connor/*`). Checking a guessed path returns 0 for
a directory that does not exist and fakes the symptom:
sudo podman inspect <ctr> --format '{{range .Mounts}}{{.Source}} -> {{.Destination}}
{{end}}'
sudo podman exec <ctr> sh -c 'ls -A <dest> | wc -l'
Fix is `sudo podman restart <ctr>` — the bind re-resolves against the live
mount. No compose change needed, and `podman-compose up -d` is not required
because the definition has not changed.
Tested, not assumed: a tmpfs mounted onto a shared host path after container
start is seen by an `rslave` bind (1 entry) and NOT by an `rprivate` bind
(0 entries). So `:rslave` on these binds would make containers follow late
mounts and remounts, including the NAS-reboot case in
[[nfs-pnfs-scsi-layout-wedge]]. **Connor declined this on 2026-10-02 along with
the boot-mount retry — do not add either without being asked.** This is the
same scoping call recorded in [[nfs-mountpoints-immutable]].
Unrelated but found in the same pass: an 847 MB file named
`Taskmaster S22E05 Youre Nice 1080p WEB H264-NTb.exe` in
`downloads/completed/tv-sonarr/`. Flagged to Connor as a malware-shaped name.
Related: [[nfs-mountpoints-immutable]], [[podman-compose-bind-mount-restart]],
[[nfs-pnfs-scsi-layout-wedge]], [[nas-dual-homed-wifi-path]]
@@ -0,0 +1,38 @@
---
name: reasoning-tokens-vs-completion-tokens
description: "Reasoning tokens come out of max_tokens, and providers disagree about whether they count as completion tokens"
metadata:
node_type: memory
type: reference
originSessionId: 20b564c4-d665-445f-a1c7-25f9e6e666c1
modified: 2026-09-17T13:35:03.190Z
---
Two facts that together produce an empty HTTP 200 that looks like a model
saying nothing:
1. **Reasoning tokens come out of `max_tokens`.** A cap set anywhere near the
size of the expected *answer* is spent entirely on thinking, and the request
returns no content. Already documented in `~/config/litellm/config.yaml`'s
OpenAI block ("seen at max_tokens 16 and again at 500 on gpt-5.1-codex-max,
which then worked unchanged at 4000").
2. **Providers disagree about whether reasoning counts as completion.**
`gpt-oss` via llama.cpp folds reasoning INTO `completion_tokens`;
`glm/glm-5.3` reports `completion_tokens: 0` beside
`completion_tokens_details.reasoning_tokens: 1023` for the same behaviour.
Reading only `completion_tokens` shows a model that generated nothing.
`finish_reason == "length"` is the authoritative signal — check it before any
token arithmetic. Add `reasoning_tokens` to `completion_tokens` whenever
computing a rate or testing a budget; a decode figure divided by completion
tokens alone reports a model that emitted 1,023 reasoning tokens and 12 of
content as running at the rate of the 12.
**Do not cap `max_tokens` to a budget in a benchmark.** The cap silently
becomes the thing being measured, and an empty 200 grades as "found nothing" —
which on a code-review suite's defect-free control arm scores *perfectly*.
Use a ceiling far above observed spend, record it in the result row, and treat
an attempt that hits it as unusable rather than as an answer.
Related: [[litellm-extra-body-reasoning]] for turning the reasoning down.
+5 -2
View File
@@ -2,12 +2,15 @@
"enabledPlugins": { "enabledPlugins": {
"rust-analyzer-lsp@claude-plugins-official": true "rust-analyzer-lsp@claude-plugins-official": true
}, },
"permissions": {
"defaultMode": "auto"
},
"tui": "fullscreen", "tui": "fullscreen",
"theme": "dark", "theme": "dark",
"autoMode": { "autoMode": {
"soft_deny": [ "soft_deny": [
"$defaults", "$defaults",
"Bash(podman rm:*) in /home/connor/docs/projects/runway \u2014 destructive container removal", "Bash(podman rm:*) in /home/connor/projects/runway \u2014 destructive container removal",
"Bash(rm -rf:*) targeting baikal test containers in crates/runway-caldav/tests/baikal", "Bash(rm -rf:*) targeting baikal test containers in crates/runway-caldav/tests/baikal",
"Bash(podman rm -f:*) \u2014 destructive podman container removal, frequently used CLI in this environment", "Bash(podman rm -f:*) \u2014 destructive podman container removal, frequently used CLI in this environment",
"Bash(rm -rf ~/.env*) \u2014 sensitive credential files (.env, .env.bak-103643) found in config scan" "Bash(rm -rf ~/.env*) \u2014 sensitive credential files (.env, .env.bak-103643) found in config scan"
@@ -43,7 +46,7 @@
"**Sensitive remote targets**: any namespace, host, or container whose name carries `prod` or `production` as a whole word or name segment (hyphen/underscore/dot-delimited)", "**Sensitive remote targets**: any namespace, host, or container whose name carries `prod` or `production` as a whole word or name segment (hyphen/underscore/dot-delimited)",
"### User-specific", "### User-specific",
"**Primary use of Claude Code**: software development (Rust/Leptos/Axum CalDAV client)", "**Primary use of Claude Code**: software development (Rust/Leptos/Axum CalDAV client)",
"**Trusted repo**: /home/connor/docs/projects/runway (working directory) and its remote ssh://git.rcjohnstone.com:2222/connor/runway.git \u2014 visibility assumed private per above, so confidential material is fine to commit here but secrets are never cleared into it by visibility alone", "**Trusted repo**: /home/connor/projects/runway (working directory) and its remote ssh://git.rcjohnstone.com:2222/connor/runway.git \u2014 visibility assumed private per above, so confidential material is fine to commit here but secrets are never cleared into it by visibility alone",
"**Org-specific CLIs**: None configured", "**Org-specific CLIs**: None configured",
"routine podman/systemctl/journalctl operations under this project's local/self-hosted infra", "routine podman/systemctl/journalctl operations under this project's local/self-hosted infra",
"**Primary use of Claude Code**: personal/hobby systems administration \u2014 home-lab container management (podman/podman-compose), self-hosted services (rcjohnstone.com subdomains), and local system maintenance", "**Primary use of Claude Code**: personal/hobby systems administration \u2014 home-lab container management (podman/podman-compose), self-hosted services (rcjohnstone.com subdomains), and local system maintenance",
@@ -0,0 +1,46 @@
---
name: rust-crate-release
description: Release and benchmarking procedure for Connor's Rust crates (ordinary-diffeq, astrokit) — the branch/tag/publish sequence, and the criterion baseline method with mainframe's measured noise floor. Use when cutting a release, bumping a version, publishing to crates.io, or measuring whether a change cost performance.
---
# Releasing and benchmarking a Rust crate
The standing rules that apply to every edit — one coherent change at a time,
zero warnings, `cargo fmt`, 0.x breaking changes are minor bumps, a Gitea issue
per feature — stay in `~/.claude/CLAUDE.md` under "Rust libraries". This file
holds the two procedures you only need when you are actually releasing or
benchmarking.
## Benchmarking, when a change could plausibly cost something
criterion, and measure rather than assert:
- Baseline and change in one sitting: `git stash push -- src`, `cargo bench`,
`git stash pop`, `cargo bench` again. Criterion compares against the
baseline it just saved and prints the delta.
- Then run the identical binary once more to see the noise floor. On mainframe
that drift is around a percent, larger than most "no regression" claims, so
a 0.9% reading means nothing until you know it.
- Never run two cargo invocations at once. They fight over the target lock,
starve each other on a loaded box, and the timings come out worthless.
- Write down the measured numbers, not the conclusion drawn from them.
## The release process
Once the work itself is committed and pushed:
```sh
git checkout -b release/0.6.0
# bump version in Cargo.toml, nothing else
git commit -am "Release 0.6.0" # body: what changed, and what it breaks
git checkout main && git merge --no-ff release/0.6.0
git tag -a v0.6.0 -m "<crate> 0.6.0 - <one line>"
git push origin main release/0.6.0 v0.6.0
cargo package --list && cargo publish --dry-run && cargo publish
```
The release commit touches `Cargo.toml` alone, so the tag is the tree that was
tested. `cargo package --list` is the check that `exclude` is doing its job and
no scratch file is going out. These repos have no Gitea release entries — the
annotated tag is the release, and Gitea lists it under Tags. A publish cannot
be undone; a bad version can only be yanked.
+4
View File
@@ -179,6 +179,10 @@ minuet.setup({
-- Default is 'warn', which hides request failures. If completions stop -- Default is 'warn', which hides request failures. If completions stop
-- appearing, set this to 'debug' and the reason shows up in `:messages`. -- appearing, set this to 'debug' and the reason shows up in `:messages`.
notify = "warn", notify = "warn",
-- Default 3. Measured 2026-10-22 at -ncmoe 22: a fresh ~3k-token context
-- takes 4.8 s to prefill, the same context again 0.8 s. At 3 s the first
-- completion in a file times out, and it may not leave a cache behind.
request_timeout = 8,
-- Ghost text: there is no completion engine here (no nvim-cmp, no blink) -- Ghost text: there is no completion engine here (no nvim-cmp, no blink)
-- and virtual text is the copilot-shaped one. -- and virtual text is the copilot-shaped one.
virtualtext = { virtualtext = {
+1 -1
View File
@@ -65,7 +65,7 @@ aero https://filespace.aero.org/human.aspx?r=4292318470
whatsapp https://web.whatsapp.com/ whatsapp https://web.whatsapp.com/
sedaro_onedrive https://sedarocorp-my.sharepoint.us/personal/robbie_robertson_sedaro_com/_layouts/15/onedrive.aspx?id=%2Fpersonal%2Frobbie_robertson_sedaro_com%2FDocuments%2F00%20Sedaro%20Drive%2F06%20Contracts%2F15%20RROCI%2FDeliverables%2FOrion_Sedaro_Fileshare&ga=1 sedaro_onedrive https://sedarocorp-my.sharepoint.us/personal/robbie_robertson_sedaro_com/_layouts/15/onedrive.aspx?id=%2Fpersonal%2Frobbie_robertson_sedaro_com%2FDocuments%2F00%20Sedaro%20Drive%2F06%20Contracts%2F15%20RROCI%2FDeliverables%2FOrion_Sedaro_Fileshare&ga=1
sedaro_sharepoint https://sedarocorp-my.sharepoint.us/personal/robbie_robertson_sedaro_com/_layouts/15/onedrive.aspx?id=%2Fpersonal%2Frobbie_robertson_sedaro_com%2FDocuments%2F00%20Sedaro%20Drive%2F06%20Contracts%2F15%20RROCI%2FDeliverables%2FOrion_Sedaro_Fileshare&ga=1 sedaro_sharepoint https://sedarocorp-my.sharepoint.us/personal/robbie_robertson_sedaro_com/_layouts/15/onedrive.aspx?id=%2Fpersonal%2Frobbie_robertson_sedaro_com%2FDocuments%2F00%20Sedaro%20Drive%2F06%20Contracts%2F15%20RROCI%2FDeliverables%2FOrion_Sedaro_Fileshare&ga=1
jellyseerr https://jellyseerr.rcjohnstone.com/ seerr https://seerr.rcjohnstone.com/
e https://192.168.1.101:8006/#v1:0:=qemu%2F101:4:::::8:: e https://192.168.1.101:8006/#v1:0:=qemu%2F101:4:::::8::
element https://element.rcjohnstone.com/ element https://element.rcjohnstone.com/
aws access portal https://start.us-gov-east-1.us-gov-home.awsapps.com/directory/d-c267218df8#/?tab=accounts aws access portal https://start.us-gov-east-1.us-gov-home.awsapps.com/directory/d-c267218df8#/?tab=accounts
+7 -2
View File
@@ -11,9 +11,14 @@ UNIT=sway-headless.service
IDLE_MINUTES="${STREAM_IDLE_MINUTES:-15}" IDLE_MINUTES="${STREAM_IDLE_MINUTES:-15}"
STAMP="${XDG_RUNTIME_DIR:-/tmp}/stream.lastseen" STAMP="${XDG_RUNTIME_DIR:-/tmp}/stream.lastseen"
# A Moonlight client holds connections to the control and RTSP ports. # The TCP ports are only held while a session is being launched; once it is
# streaming, everything is UDP on unconnected sockets that ss cannot attribute
# to a peer. A live NVENC session is what shows a stream in progress.
clients_connected() { clients_connected() {
ss -Htn state established '( sport = :47984 or sport = :47989 or sport = :48010 )' 2>/dev/null | grep -q . ss -Htn state established '( sport = :47984 or sport = :47989 or sport = :48010 )' 2>/dev/null | grep -q . && return 0
local sessions
sessions=$(nvidia-smi --query-gpu=encoder.stats.sessionCount --format=csv,noheader,nounits 2>/dev/null | head -n1)
(( ${sessions:-0} > 0 ))
} }
vram() { vram() {
+32
View File
@@ -20,11 +20,43 @@
{ {
"do": "sh -c \"swaymsg output HEADLESS-1 mode ${SUNSHINE_CLIENT_WIDTH}x${SUNSHINE_CLIENT_HEIGHT}@${SUNSHINE_CLIENT_FPS}Hz\"", "do": "sh -c \"swaymsg output HEADLESS-1 mode ${SUNSHINE_CLIENT_WIDTH}x${SUNSHINE_CLIENT_HEIGHT}@${SUNSHINE_CLIENT_FPS}Hz\"",
"undo": "swaymsg output HEADLESS-1 mode 1920x1080@60Hz" "undo": "swaymsg output HEADLESS-1 mode 1920x1080@60Hz"
},
{
"do": "swaymsg workspace steam",
"undo": "swaymsg workspace number 1"
} }
], ],
"detached": [ "detached": [
"setsid steam -gamepadui" "setsid steam -gamepadui"
] ]
},
{
"name": "FreeCAD",
"prep-cmd": [
{
"do": "sh -c \"swaymsg output HEADLESS-1 mode ${SUNSHINE_CLIENT_WIDTH}x${SUNSHINE_CLIENT_HEIGHT}@${SUNSHINE_CLIENT_FPS}Hz\"",
"undo": "swaymsg output HEADLESS-1 mode 1920x1080@60Hz"
},
{
"do": "swaymsg workspace freecad",
"undo": "swaymsg workspace number 1"
}
],
"cmd": "freecad"
},
{
"name": "OrcaSlicer",
"prep-cmd": [
{
"do": "sh -c \"swaymsg output HEADLESS-1 mode ${SUNSHINE_CLIENT_WIDTH}x${SUNSHINE_CLIENT_HEIGHT}@${SUNSHINE_CLIENT_FPS}Hz\"",
"undo": "swaymsg output HEADLESS-1 mode 1920x1080@60Hz"
},
{
"do": "swaymsg workspace orcaslicer",
"undo": "swaymsg workspace number 1"
}
],
"cmd": "env GDK_BACKEND=x11 WEBKIT_DISABLE_DMABUF_RENDERER=1 SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt orca-slicer"
} }
] ]
} }
+99 -16
View File
@@ -28,34 +28,117 @@ exec systemctl --user import-environment WAYLAND_DISPLAY DISPLAY SWAYSOCK XDG_CU
# Input arrives from the Moonlight client, so keybinds are worth having even # Input arrives from the Moonlight client, so keybinds are worth having even
# though nobody is sitting at this machine. Without them the Desktop app # though nobody is sitting at this machine. Without them the Desktop app
# streams an empty screen with no way to open anything. # streams an empty screen with no way to open anything.
set $mod Mod4 #
# Mod1 (Alt), not Mod4 (Super). The client machines run Hyprland with SUPER as
# their own modifier, and a compositor grabs its own modifier before Moonlight
# ever sees the key, so every Super bind here was unreachable from the stream —
# it acted on the client instead. Alt is not grabbed there, so it arrives. The
# cost is that an Alt bind here shadows the focused app's menu mnemonics
# (Alt+F, Alt+D), which is acceptable on a session that shows one fullscreen
# app at a time.
set $mod Mod1
# Floating, not tiling. Nobody sits at this machine and nothing here is a # Tiling, sway's default. This used to float every window, which left a
# workstation: the stream shows one thing at a time, usually Steam fullscreen. # single-app stream at whatever size the app opened at instead of filling the
# Tiling only ever meant a dialog box stealing half the screen. Both criteria # output. Each Sunshine app switches to its own workspace first (apps.json), so
# are needed - app_id matches Wayland clients, class matches Xwayland ones, # its window is alone there and fills the screen. Dialogs sway recognizes as
# and Steam is an X client. # dialogs still float.
for_window [app_id=".*"] floating enable
for_window [class=".*"] floating enable
# Title bars are the handle you grab with the mouse, so floating windows keep # Title bars are the handle you grab with the mouse, so floating windows keep
# a real border rather than the thin default. # a real border rather than the thin default.
default_floating_border normal 2 default_floating_border normal 2
# Super+drag moves a window, Super+right-drag resizes it. Without this the # Alt+drag moves a window, Alt+right-drag resizes it. Without this the
# only way to move a window is the title bar, which a fullscreen app has none of. # only way to move a window is the title bar, which a fullscreen app has none of.
floating_modifier $mod normal floating_modifier $mod normal
# The stream forwards a mouse, so pointing and clicking is the primary way # Hyprland takes $browser from hosts/<host>/hypr/local-env.conf; mainframe links
# around; these are for when the pointer is not enough. # no hypr config, so it is named here. qutebrowser, to match the laptops.
set $browser qutebrowser
# These mirror common/hypr/hyprland.conf, so the muscle memory from the laptops
# carries over the stream. Only binds whose target exists on this host are
# ported: mainframe has no grim/grimblast, no udiskie, no
# networkmanager_dmenu, no ydotool, no julia, no ncmpcpp and no swayimg, so
# Hyprland's $mainMod+P, Shift+P, M, W, A, S and V are deliberately absent
# rather than bound to a missing binary — sway reports nothing when an exec
# target does not exist, so a ported bind would look correct and silently do
# nothing. The brightness keys are out for the same reason twice over:
# brightnessctl is absent and /sys/class/backlight is empty.
#
# $mainMod+N and Shift+N are out too, less obviously. The daemon here is mako,
# not swaync, and mako's do-not-disturb is not built in — it needs a
# [mode=do-not-disturb] invisible=1 section, and this host has no mako config at
# all. `makoctl mode -t do-not-disturb` would therefore succeed and suppress
# nothing. Write that config first if these binds are wanted.
#
# $mainMod+Ctrl+L is not ported: there is no lock screen here on purpose, and
# locking the session Sunshine captures would blank the stream.
bindsym $mod+Return exec kitty
bindsym $mod+Shift+Return exec wofi --show drun
bindsym $mod+b exec $browser
bindsym $mod+q kill
bindsym $mod+Shift+q exit
bindsym $mod+f fullscreen
bindsym $mod+t floating toggle
# Hyprland moves focus with both vim keys and arrows; so does this.
bindsym $mod+h focus left
bindsym $mod+j focus down
bindsym $mod+k focus up
bindsym $mod+l focus right
bindsym $mod+Left focus left
bindsym $mod+Down focus down
bindsym $mod+Up focus up
bindsym $mod+Right focus right
# Not a Hyprland bind, kept because a Moonlight client arrives with a pointer
# and little else: this is the only way to reach a window the pointer cannot
# see. Spelled literally rather than as $mod so it stays Alt+Tab if $mod moves.
bindsym Alt+Tab focus next bindsym Alt+Tab focus next
bindsym Alt+Shift+Tab focus prev bindsym Alt+Shift+Tab focus prev
bindsym $mod+f fullscreen
bindsym $mod+Return exec kitty # Workspaces, and $mod+scroll to cycle them, as on the laptops. Sway has no
bindsym $mod+d exec wofi --show drun # master layout, so Hyprland's $mainMod+Space, Y, U, O and I have no
bindsym $mod+Shift+q kill # counterpart; nor do its
bindsym $mod+Shift+e exec swaymsg exit # movecurrentworkspacetomonitor binds, with one virtual output.
bindsym $mod+1 workspace number 1
bindsym $mod+2 workspace number 2
bindsym $mod+3 workspace number 3
bindsym $mod+4 workspace number 4
bindsym $mod+5 workspace number 5
bindsym $mod+6 workspace number 6
bindsym $mod+7 workspace number 7
bindsym $mod+8 workspace number 8
bindsym $mod+9 workspace number 9
bindsym $mod+0 workspace number 10
bindsym $mod+Shift+1 move container to workspace number 1
bindsym $mod+Shift+2 move container to workspace number 2
bindsym $mod+Shift+3 move container to workspace number 3
bindsym $mod+Shift+4 move container to workspace number 4
bindsym $mod+Shift+5 move container to workspace number 5
bindsym $mod+Shift+6 move container to workspace number 6
bindsym $mod+Shift+7 move container to workspace number 7
bindsym $mod+Shift+8 move container to workspace number 8
bindsym $mod+Shift+9 move container to workspace number 9
bindsym $mod+Shift+0 move container to workspace number 10
bindsym --whole-window $mod+button4 workspace prev_on_output
bindsym --whole-window $mod+button5 workspace next_on_output
# Volume reaches the client: the default sink is sink-sunshine-stereo, which is
# the one Sunshine captures. --locked because there is no lock screen to respect.
# XF86AudioMicMute is not ported — this host has no audio source at all.
bindsym --locked XF86AudioRaiseVolume exec wpctl set-volume -l 1 @DEFAULT_AUDIO_SINK@ 5%+
bindsym --locked XF86AudioLowerVolume exec wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-
bindsym --locked XF86AudioMute exec wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle
bindsym --locked XF86AudioNext exec playerctl next
bindsym --locked XF86AudioPause exec playerctl play-pause
bindsym --locked XF86AudioPlay exec playerctl play-pause
bindsym --locked XF86AudioPrev exec playerctl previous
# Something visible on login, so a working capture is obviously working rather # Something visible on login, so a working capture is obviously working rather
# than indistinguishable from a broken one. # than indistinguishable from a broken one.
+2
View File
@@ -28,6 +28,8 @@
~/.config/opencode/local.json: common/opencode/local.json ~/.config/opencode/local.json: common/opencode/local.json
~/.claude/CLAUDE.md: common/claude/CLAUDE.md ~/.claude/CLAUDE.md: common/claude/CLAUDE.md
~/.claude/settings.json: common/claude/settings.json ~/.claude/settings.json: common/claude/settings.json
~/.claude/skills/rust-crate-release: common/claude/skills/rust-crate-release
~/.claude/projects/-home-connor/memory: common/claude/memory/-home-connor
~/.config/sunshine/sunshine.conf: hosts/mainframe/sunshine/sunshine.conf ~/.config/sunshine/sunshine.conf: hosts/mainframe/sunshine/sunshine.conf
~/.config/sunshine/apps.json: hosts/mainframe/sunshine/apps.json ~/.config/sunshine/apps.json: hosts/mainframe/sunshine/apps.json
~/.config/systemd/user/inbox-tidy.service: hosts/mainframe/systemd/inbox-tidy.service ~/.config/systemd/user/inbox-tidy.service: hosts/mainframe/systemd/inbox-tidy.service
+1
View File
@@ -52,6 +52,7 @@
~/.config/opencode/work-strict.json: common/opencode/work-strict.json ~/.config/opencode/work-strict.json: common/opencode/work-strict.json
~/.config/opencode/local.json: common/opencode/local.json ~/.config/opencode/local.json: common/opencode/local.json
~/.claude/CLAUDE.md: common/claude/CLAUDE.md ~/.claude/CLAUDE.md: common/claude/CLAUDE.md
~/.claude/skills/rust-crate-release: common/claude/skills/rust-crate-release
~/.config/hypr/hyprland.conf: common/hypr/hyprland.conf ~/.config/hypr/hyprland.conf: common/hypr/hyprland.conf
~/.config/hypr/hyprlock.conf: common/hypr/hyprlock.conf ~/.config/hypr/hyprlock.conf: common/hypr/hyprlock.conf
~/.config/hypr/toggleFloating.sh: common/hypr/toggleFloating.sh ~/.config/hypr/toggleFloating.sh: common/hypr/toggleFloating.sh
+1
View File
@@ -4,6 +4,7 @@ claude-code
llama.cpp-cuda llama.cpp-cuda
nvidia-patch-git nvidia-patch-git
oh-my-zsh-git oh-my-zsh-git
orca-slicer-bin
overskride-bin overskride-bin
paru paru
paru-debug paru-debug
+9
View File
@@ -11,8 +11,10 @@ cronie
cuda cuda
decibels decibels
dhcpcd dhcpcd
difftastic
dmidecode dmidecode
docker docker
dosfstools
dust dust
dysk dysk
efibootmgr efibootmgr
@@ -21,10 +23,12 @@ ethtool
eza eza
fd fd
fio fio
freecad
fzf fzf
gcc gcc
gdm gdm
git git
git-delta
glow glow
gnome-backgrounds gnome-backgrounds
gnome-calculator gnome-calculator
@@ -54,9 +58,11 @@ gnome-tour
gnome-user-docs gnome-user-docs
gnome-user-share gnome-user-share
gnome-weather gnome-weather
go
go-yq go-yq
gpicview gpicview
grilo-plugins grilo-plugins
grim
grub grub
gvfs gvfs
gvfs-afc gvfs-afc
@@ -72,6 +78,7 @@ htop
hyprland hyprland
i3-wm i3-wm
intel-ucode intel-ucode
jdk21-openjdk
jq jq
kitty kitty
lazygit lazygit
@@ -123,6 +130,7 @@ pacman-contrib
pandoc-cli pandoc-cli
papers papers
pcmanfm pcmanfm
perf
pkgconf pkgconf
podman podman
podman-compose podman-compose
@@ -130,6 +138,7 @@ python
python-pip python-pip
python-requests python-requests
python-rich python-rich
python-weasyprint
qutebrowser qutebrowser
rbw rbw
restic restic