Compare commits
77
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
206c9acb46 | ||
|
|
53025a6be5 | ||
|
|
dc88d6f961 | ||
|
|
f2d8aa4f9e | ||
|
|
7506970f9d | ||
|
|
c736456413 | ||
|
|
441519c1f0 | ||
|
|
90cd6bb766 | ||
|
|
757fa0106b | ||
|
|
a8e74e7266 | ||
|
|
f859230ff6 | ||
|
|
6569290398 | ||
|
|
d83531e3fe | ||
|
|
41f577c883 | ||
|
|
32bea490c4 | ||
|
|
b698b0e35e | ||
|
|
0d41c3d61d | ||
|
|
ea2cfad847 | ||
|
|
6f0aa7d72e | ||
|
|
7d394e74b4 | ||
|
|
10cd18ec57 | ||
|
|
e4721a6713 | ||
|
|
4648ba7f3f | ||
|
|
2d71c603cb | ||
|
|
388058d134 | ||
|
|
979382e5cc | ||
|
|
47d4e0e8f9 | ||
|
|
a071d71e99 | ||
|
|
38236f35f7 | ||
|
|
d73deb61e2 | ||
|
|
68d6889100 | ||
|
|
5a3f1a77ac | ||
|
|
0b27a68c33 | ||
|
|
0b72e9a427 | ||
|
|
b04a634ec2 | ||
|
|
240324a18c | ||
|
|
9d172eff9c | ||
|
|
ddebfb63bb | ||
|
|
b7fd64b241 | ||
|
|
414b4cd578 | ||
|
|
b289aa59d4 | ||
|
|
175bbdf451 | ||
|
|
531ead5775 | ||
|
|
abee84ddad | ||
|
|
9df5c50fda | ||
|
|
702847cefd | ||
|
|
ed3a43bff1 | ||
|
|
cd3fe7fd03 | ||
|
|
218357cb66 | ||
|
|
31d6924a27 | ||
|
|
feda9fe668 | ||
|
|
eae9dea2b0 | ||
|
|
bc597edec1 | ||
|
|
5f560f33c1 | ||
|
|
461f0b8a6e | ||
|
|
02b65989ca | ||
|
|
42eaf9e1a7 | ||
|
|
f51f4dd1aa | ||
|
|
0e2cf13e3b | ||
|
|
1d1059f7b3 | ||
|
|
b7089ef570 | ||
|
|
dc4de763f3 | ||
|
|
9d284106ac | ||
|
|
ac2c5c519a | ||
|
|
f1402ca89c | ||
|
|
4d515f7e81 | ||
|
|
f7281eee42 | ||
|
|
ecb81dd8b8 | ||
|
|
1b1a296ec5 | ||
|
|
9a3d364832 | ||
|
|
e529dc5386 | ||
|
|
9faf7e0652 | ||
|
|
017404aa51 | ||
|
|
c0d840a8bb | ||
|
|
f3360a28f1 | ||
|
|
dcdf84719c | ||
|
|
8c3dc6bfc9 |
No files matched your search
+1
-1
@@ -85,7 +85,7 @@ compaction:
|
||||
- handoff
|
||||
- remote
|
||||
idleThresholdTokens: 100000
|
||||
thresholdPercent: 95
|
||||
thresholdPercent: 85
|
||||
idleEnabled: true
|
||||
thresholdTokens: -1
|
||||
dropUseless: false
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
name: agenttrash-purger-invocation
|
||||
description: "Invoke the .AGENTTRASH purger (scripts/purge-agenttrash.sh) to clear staged deletions, and diagnose its guaranteed exit 1 on hosts whose find is uutils (Rust) rather than GNU."
|
||||
---
|
||||
|
||||
# Purging .AGENTTRASH — invocation and the uutils-find trap
|
||||
|
||||
`scripts/purge-agenttrash.sh` (serverAppConfigs, merged in PR #79 `fad87aa`) is the tool form of the fleet's deletion policy: files land in `.AGENTTRASH`, and an approved purge runs this instead of being improvised per session.
|
||||
|
||||
## Invoke
|
||||
|
||||
```sh
|
||||
sh scripts/purge-agenttrash.sh # plan only — prints, deletes nothing
|
||||
sh scripts/purge-agenttrash.sh --execute # perform the plan
|
||||
sh scripts/purge-agenttrash.sh --execute /some/root # scope to one root (default: $HOME)
|
||||
```
|
||||
|
||||
- `--quiet` silences per-line output; errors and the summary still print. `--log PATH` overrides the default `~/.AGENTTRASH-cleanup.log` (one dated line per run — written even for a dry run, so pass a scratch `--log` when you only want to look).
|
||||
- Every printable line is fixed at plan time in `$HOME/.agenttrash-purge.lock/plan`, so dry-run and `--execute` output cannot drift. Exit `0` ok · `1` any refusal or failure · `2` usage.
|
||||
- Rule: a root's own `.AGENTTRASH` is *emptied* (the directory survives); deeper `.AGENTTRASH` trees are *removed*; symlinked or foreign-owned candidates are refused, never followed; `/` is refused outright.
|
||||
|
||||
## The trap: uutils find makes it exit 1 on the dev box
|
||||
|
||||
On the workstation, `sh scripts/purge-agenttrash.sh` reports `refused /home/blank (find failed)` and exits 1 **even though the plan is correct** — and an `--execute` there acts on the recorded plan while still reporting failure.
|
||||
|
||||
- Cause: `find --version` reports `find 0.8.0` (Rust **uutils**, errors as `Error: <path>: Permission denied (os error 13)`), not GNU findutils. It exits 1 whenever any directory under the root is unreadable — and `~/.local/share/containers/storage/**` (podman overlay layers, mode 0700 dirs) is unreadable. `collect_root` turns any nonzero find status into a root refusal.
|
||||
- Proved by: `find /home/blank -xdev -name .AGENTTRASH >/dev/null 2>&1; echo $?` → `1`, while `find /home/blank/.omp -xdev ...` → `0`.
|
||||
- Fix, if it ever comes up (don't invent it): treat unreadable-directory errors as warnings, or accept find's partial output and reserve refusal for a hard find failure. Note `bash -n` is not the gate — shellcheck / `sh -n` is.
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
name: bats-line-scoped-assert-traps
|
||||
description: "When writing or reviewing bats tests over multi-line command output, catch glued-glob adjacency that hops lines, env-exec of shell functions (127), and file-wise runs that hide full-suite reds."
|
||||
---
|
||||
|
||||
Three traps that produced false-green bats runs; check each when writing or reviewing tests that assert over multi-line `$output`.
|
||||
|
||||
## 1. `[[ "$output" == *"a b"* ]]` can hop lines
|
||||
`$output` is multiline; a glob `*url auth=Bearer key*` matches across newline boundaries, and inserting a new column into a per-line log format silently breaks *old* adjacency pins elsewhere in the file.
|
||||
|
||||
Pin per-line facts with a line-scoped grep instead:
|
||||
|
||||
```bash
|
||||
grep -Eq '/audio/speech .*auth=Bearer dk-speech' <<<"$output"
|
||||
```
|
||||
|
||||
`grep` is line-scoped by construction: the assertion can only match within one CALL/log line. When a fixture adds a column to its log lines, grep the test suite for glued globs over those lines first (`grep -n '\[\[ "\$output" == \*' tests/`).
|
||||
|
||||
## 2. `run env PATH=... my_shell_function args` is always 127
|
||||
`env` execs an external binary; bats test-file functions are invisible to it, and `BW01` (127) does not by itself fail the test — a following `[ "$status" -eq 0 ]` does, or worse, nothing checks it.
|
||||
|
||||
Inherit and extend PATH around the function call instead:
|
||||
|
||||
```bash
|
||||
export PATH="$WORK/bothbin:$PATH"
|
||||
DV_VAR=x my_function scenario
|
||||
```
|
||||
|
||||
If a restricted PATH is truly required, write an executable wrapper script into that bin (`exec bun "$DRIVER" "$@"` with an absolute interpreter path).
|
||||
|
||||
## 3. File-wise green ≠ suite green
|
||||
A single file (`bats tests/behavior.bats`) can pass while `bats tests/` is red (order-dependent state, shared fixtures, or simply a stale dist pin file in another file). Always run the whole gate — `bats tests/` plus unit runner (`bun test`) — on the exact final tree before opening a PR, and grep the log, never tail it:
|
||||
|
||||
```bash
|
||||
bats tests/ > /tmp/gate.log 2>&1; echo "EXIT $?"; grep "^not ok" /tmp/gate.log; grep -c "^ok" /tmp/gate.log
|
||||
```
|
||||
|
||||
## Verify a red is red for the stated reason
|
||||
When a freshly-added test fails immediately and inexplicably fast (seconds), suspect harness-level breakage (127, missing exec) rather than product behavior — inspect with a temporary `printf '%s\n' "$output" > /tmp/dbg.txt` dump inside the test, delete it after, and reproduce the actual line format before fixing assertions.
|
||||
@@ -1,47 +1,48 @@
|
||||
---
|
||||
name: browser-screenshot-disk-and-cookies
|
||||
description: Save omp managed-browser screenshots to real files (tab.screenshot path is silently ignored) and reset the shared Chromium cookie jar between persona captures.
|
||||
description: "Save omp managed-browser screenshots to real files (tab.screenshot path is silently ignored), prove/clear cookie state (clearCookies is domain-scoped), and get embedded-image commits past the secret gate."
|
||||
---
|
||||
|
||||
# Screenshots to disk with the omp `browser` eval helper
|
||||
# omp managed browser from Eval: screenshots, cookies, page reads
|
||||
|
||||
## Traps (both cost a full re-capture to discover)
|
||||
1. `tab.screenshot({ path })` **ignores `path` silently** — the option is not in
|
||||
its schema (`{selector, fullPage, silent, annotate, format, quality, ifChanged, threshold}`).
|
||||
The image attaches inline to the tool result and **nothing is written to disk**;
|
||||
`ls` shows an empty dir. Inline images may also be dropped from transcript
|
||||
archives later, so "attached to chat" is not "saved".
|
||||
2. The shared managed Chromium keeps **cookies across tabs and turns** (profile
|
||||
persists after `tab.close()`). A second capture script that expects the
|
||||
anonymous view lands signed-in and `waitForSelector("testid/…")` fails fast.
|
||||
Friction-earned 2026-10-21 (cookie-state proof on a local stack), same day (static HTML guide verification — four wasted API calls from signature guesses), re-earned same day on the franksign screenshot-walkthrough campaign (19 embedded PNGs, two cookie traps, one secret-gate false positive).
|
||||
|
||||
## Procedure
|
||||
Write files via `tab.run`, which exposes the raw Puppeteer `page` — its
|
||||
`screenshot({path})` does write:
|
||||
## Screenshots to real files
|
||||
- `tab.screenshot({ path })` returns the image to the model but the `path` option is **silently ignored** on the managed-browser path — no file appears on disk.
|
||||
- Working recipe: re-capture raw and decode base64 to disk:
|
||||
```js
|
||||
await tab.run(async ({ page }, p) => {
|
||||
const r = await page.screenshot({ encoding: "base64", type: "png" });
|
||||
await Bun.write(p, Buffer.from(typeof r === "string" ? r : r.data, "base64"));
|
||||
}, { args: ["/tmp/x.png"] });
|
||||
```
|
||||
This puppeteer variant returns the base64 **string directly**, not `{data}` — handle both or you write `undefined`.
|
||||
- `computer.window("Firefox").screenshot()` fails (`captureWindow: unavailable` under niri) — don't go that way.
|
||||
- Downscale before embedding: `magick "$f" -resize 1000x "$f"` keeps dark-theme UI PNGs at 20–80 kB each.
|
||||
|
||||
```js
|
||||
const dir = "/tmp/<session-dir>";
|
||||
const tab = await browser.open({ name: "caps", url });
|
||||
const shot = (file) =>
|
||||
tab.run(async ({ page }, f) => { await page.screenshot({ path: f }); return f; },
|
||||
{ args: [`${dir}/${file}.png`] });
|
||||
## Cookie state (corrected 2026-10-21 — the old claim was wrong)
|
||||
- Cookies DO cross `browser.open` calls: the headless browser is one shared process with one jar (a stale persona session from an earlier campaign hijacked an "anonymous" walkthrough tab).
|
||||
- `tab.clearCookies()` is **domain-scoped to the current page origin** — running it on `about:blank` clears nothing and silently leaves the previous session live (a "signer" tab showed the admin's dashboard). Correct sequence: `goto(app-origin)` → `clearCookies()` → `reload()`, then verify with `page.cookies()`, not with page text.
|
||||
- Cookie proof (ground truth):
|
||||
```js
|
||||
const cs = await page.cookies()
|
||||
return cs.map(c => `${c.name}=${c.value.slice(0,12)}… dom=${c.domain} exp=${c.expires}`).join("\n")
|
||||
```
|
||||
|
||||
await tab.clearCookies(); // ALWAYS before an anonymous capture
|
||||
await tab.goto(url);
|
||||
await tab.waitForSelector("testid/first-state");
|
||||
await shot("1-anonymous");
|
||||
// ...drive states with direct helpers: click/fill("testid/<id>", ...) ...
|
||||
await shot("2-next-state");
|
||||
await tab.close();
|
||||
```
|
||||
## Committing screenshots: the secret-gate false positive
|
||||
- Data-URI PNGs in a diff WILL trip shape-based secret gates (`ASIA[A-Z0-9]{16}` etc. appear as random substrings in base64 streams, worse with case-insensitive grep).
|
||||
- Prove noise, don't assume: run the gate's own pattern with `grep -oEi` over `git diff --cached` and eyeball the matches (image-byte substrings, not credentials; confirm screenshots show passwords masked).
|
||||
- Then use the gate's sanctioned override with the evidence in the commit body: `ALLOW_SECRETS=1 git commit -m "... ALLOW_SECRETS: the N hits are ASIA-shape coincidences inside PNG base64 ..."`. Never `--no-verify`.
|
||||
|
||||
Verify with `ls -la <dir> && file <dir>/*.png` — do not trust silent success.
|
||||
## API signatures (guesses cost calls)
|
||||
- `browser.open({ url, name })` — options object; `browser.open(url)` → TypeError.
|
||||
- `tab.wait(ms)` does not exist → `await Bun.sleep(ms)`.
|
||||
- `tab.run(fnOrCode)` executes in an isolated **Node/Puppeteer realm** (`{ tab, page, browser }`, plus `args` as trailing params), NOT the page — `document is not defined` means you wanted `tab.evaluate(exprString)` (page-global expression; no top-level `return`; wrap an IIFE when you need one).
|
||||
- `tab.text("svg")` etc. are direct page helpers — prefer them over evaluate for plain reads.
|
||||
- For app UIs with labelled forms, `role/textbox[name="Exact label *"]` beats `label=...`: visible labels often include a required-marker the source selector lacks.
|
||||
|
||||
## Notes
|
||||
- Selectors accept `testid/<id>` handler; prefer it over CSS on `[data-testid]`.
|
||||
- `tab.run(fn, { args })`: functions receive `{tab, page, browser, wait, assert}`
|
||||
first, then each arg; a bare code string runs in an **isolated runtime, not the
|
||||
page** — `document` is undefined there. Use direct helpers or `tab.evaluate`
|
||||
(page-global expression; top-level `return` invalid) for page access.
|
||||
- Byte-identical PNG sizes between two frames = same view; fine as proof of a
|
||||
round-trip (state A → B → A).
|
||||
## Eval cell output
|
||||
- `log(...)` inside a JS eval cell can be swallowed (printed nothing across several runs); **`return` from the cell always shows**. Put every value you need to see into the return string.
|
||||
|
||||
## Static page verification loop
|
||||
Serve with `python3 -m http.server <port> -d <dir>` spawned via `Bun.spawn`, open, `tab.evaluate` anchor cross-check (`hrefs` vs `ids`), `img` census (`complete && naturalWidth > 0`), screenshots, `tab.close()`, `proc.kill(9)`. If a rendered label looks wrong in a low-res screenshot, read the real DOM text before "fixing" markup — `<id>` renders as `<id>` and misreads as `</id>` at thumbnail size.
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
name: correct-stale-todo-before-next-turn
|
||||
description: "Recover from marking a todo done on an inferred outcome instead of the verified one: rm the wrong entry, re-append with the true state, and state the correction before continuing."
|
||||
---
|
||||
|
||||
## When this applies
|
||||
|
||||
You reported a task as finished (or told the user an outcome) from the *expected* result of a script or plan, while the evidence you had already fetched in the same turn says otherwise. Common shape: a chain script documents `rm -f FILE` "on success", you read the docs, announce the file gone — and the same output also contains `ls -l FILE` (file present) plus the gate line `... kept (recordings incomplete)`.
|
||||
|
||||
Never let the script's intent outrank the captured output. An error you noticed yourself is fixed mechanically, not narrated later: the tool record and the transcript are both verifiable, so a wrong `done` is corrected on the next turn, in the same message as the next real action.
|
||||
|
||||
## Procedure
|
||||
|
||||
1. **Re-read your own claim against the evidence** you already have (the exact `ls -l`, `df`, log tail line, rc file). Quote the contradicting line to yourself before deciding.
|
||||
2. `todo op:rm task:<the wrong entry, exact string>` — do not re-append over a `done` item, and do not edit it in place with a new phase.
|
||||
3. `todo op:append` the same work with its **true** state in the text, so the list itself carries the reason: `Purge X (still on disk: the chain's gate kept it because <evidence>; <script> removes it when <condition>)`.
|
||||
4. State the correction in one line to the user, naming the evidence that was already in your own message. Do not restate the whole prior turn and do not re-explain the plan.
|
||||
5. Only then continue with the next real action — pair the correction with a tool call, never with a bare message.
|
||||
|
||||
## Traps this prevents
|
||||
|
||||
- A `done` item silently becomes the record of an action nobody took; the next agent trusts the list instead of the host.
|
||||
- "Documented behaviour" substituted for "observed behaviour": the script's comment says what happens on success, not whether success happened. Read the gate line, the rc file, or the `ls` before claiming the effect.
|
||||
- Stacking the next mistake on top: a purge or follow-up step launched because the predecessor was believed complete.
|
||||
@@ -0,0 +1,64 @@
|
||||
---
|
||||
name: deploy-target-dirty-blob-audit
|
||||
description: "When scripts/deploy.sh on serverAppConfigs aborts with \"Your local changes to the following files would be overwritten by merge\", prove whether the target's dirty file is already-merged content before restoring it, then deploy from the main checkout."
|
||||
---
|
||||
|
||||
# Deploy target dirty-file audit
|
||||
|
||||
`deploy.sh <host>` pushes the local branch, then ssh-pulls on the target. The pull is
|
||||
`git pull --ff-only origin main` on the TARGET checkout, so any uncommitted edit there
|
||||
aborts the whole deploy with:
|
||||
|
||||
```
|
||||
error: Your local changes to the following files would be overwritten by merge:
|
||||
scripts/run-qwen38-bf16.sh
|
||||
Aborting
|
||||
```
|
||||
|
||||
Nothing is dispatched, so the service is untouched (no wasted outage) — but the deploy
|
||||
cannot proceed until that path is reconciled. This happens when an agent edited the file
|
||||
directly on the host (the repo's "never edit on the remote" law) and committed the same
|
||||
bytes later from a worktree, leaving the host's HEAD behind its own working tree.
|
||||
|
||||
## 1. Classify before touching anything
|
||||
|
||||
```bash
|
||||
ssh <host> 'cd <checkout> && git status --porcelain' # every dirty path
|
||||
ssh <host> 'cd <checkout> && git hash-object <file>
|
||||
&& git rev-parse <target_sha>:<file>' # compare blobs
|
||||
```
|
||||
|
||||
- Hash equal to the blob the pull wants (`origin/main:<file>`, or a commit main contains)
|
||||
→ the edit is already merged; restoring is lossless, no trash copy needed.
|
||||
- Hash matches neither HEAD nor the incoming blob → real unmerged content: `cp` it to
|
||||
`.AGENTTRASH/YYYYMMDD-topic/` first, then decide.
|
||||
- `D` entries under `.AGENTTRASH/` (deletions, not modifications) do not block the pull
|
||||
unless those paths changed between HEAD and main. Leave them — only Frank purges trash.
|
||||
|
||||
## 2. Restore, then deploy from the MAIN checkout
|
||||
|
||||
```bash
|
||||
ssh <host> 'cd <checkout> && git checkout -- <file>'
|
||||
cd ~/Projects/serverAppConfig && ./scripts/deploy.sh <host> --service <svc>
|
||||
```
|
||||
|
||||
`deploy.sh` refuses with `FAIL: deploy.sh must run from the main branch.` when run from a
|
||||
feature worktree — use the host's `~/Projects/serverAppConfig` main checkout, and pull it
|
||||
`--ff-only` first so `[1/2] Pushing main to origin` reports `Everything up-to-date`.
|
||||
A missing server-side path (`/home/dank/models`) plus an `rm -f` in the chain means the
|
||||
`.rad` was already retired; confirm before relaunching anything that needs it.
|
||||
|
||||
## 3. Long deploys never fit an async bash slot
|
||||
|
||||
An LLM-service deploy waits up to 20 min for model load plus the Playwright gate, but an
|
||||
async `bash` call is killed at 600 s. Launch it as a named service instead, then watch the
|
||||
pid with a deadline-free watcher:
|
||||
|
||||
```bash
|
||||
bash command="cd ~/Projects/serverAppConfig && ./scripts/deploy.sh <host> --service <svc>" \
|
||||
name=deploy-<host> ready.log="Deployment plan"
|
||||
# then: while kill -0 <pid> 2>/dev/null; do sleep 20; done (timeout 0, async)
|
||||
```
|
||||
|
||||
`ready.log="Deployment plan"` matches the plan banner, so readiness reports the deploy
|
||||
started rather than finished; the real result arrives when the watcher completes.
|
||||
@@ -1,11 +1,11 @@
|
||||
---
|
||||
name: gitea-pr-api-cycle
|
||||
description: "\"Open/update/comment/merge PRs and issues on the Dankclaw Gitea API (192.168.0.143:3000) with the token-safe sequence (POST /pulls rejects Basic with 'token is required'), numeric label ids, the form-encoded merge field, the async 405 'try again later' merge trap, the empty-token-returns-404 trap, and post-merge live-checkout fast-forward plus the plugin-package-name upgrade gotcha.\""
|
||||
description: "\"Open/update/merge PRs and issues on the Dankclaw Gitea API (192.168.0.143:3000) with the token-safe sequence (POST /pulls rejects Basic with 'token is required'), numeric label ids, the form-encoded merge field, the async 405 'try again later' merge trap, the empty-token-returns-404 trap, merged-branch deletion via the branches endpoint, and post-merge live-checkout fast-forward plus the plugin-package-name upgrade gotcha.\""
|
||||
---
|
||||
|
||||
# Gitea PR/issue cycle — Dankclaw API
|
||||
|
||||
Friction-earned 2026-10-18 (PRs #15–#20, issue #18), re-earned 2026-10-19 on `dank/dankvoice` PRs #4–#6 (the merge payload claim was wrong; an empty token masqueraded as a missing repo), and re-earned 2026-10-20 on `dank/command-center` PRs #55…#57 (the 405 recalculation trap). Re-earned again 2026-10-20 on `dank/dankvoice` #25 (merge gate loop worked; suite verdict delivered as an issue comment). Re-earned 2026-10-21 on `dank/command-center` issue #58 / PR #59 (issue labels are numeric ids; the `<type>/<issue#>-<slug>` branch name has to be right *before* the first push). Same day on `dank/serverAppConfigs` #64: the Basic-vs-token split below cost two wasted POSTs.
|
||||
Friction-earned 2026-10-18 (PRs #15–#20, issue #18), re-earned 2026-10-19 on `dank/dankvoice` PRs #4–#6 (the merge payload claim was wrong; an empty token masqueraded as a missing repo), and re-earned 2026-10-20 on `dank/command-center` PRs #55…#57 (the 405 recalculation trap). Re-earned again 2026-10-20 on `dank/dankvoice` #25 (merge gate loop worked; suite verdict delivered as an issue comment). Re-earned 2026-10-21 on `dank/command-center` issue #58 / PR #59 (issue labels are numeric ids; the `<type>/<issue#>-<slug>` branch name has to be right *before* the first push). Same day on `dank/serverAppConfigs` #64: the Basic-vs-token split below cost two wasted POSTs. Re-earned 2026-10-21 on `dank/franksign` #30 (branch-delete endpoint guess cost one 404; docs bullet went stale under the merge).
|
||||
|
||||
## Where
|
||||
- API base: `http://192.168.0.143:3000/api/v1/repos/dank/<repo>/{pulls,issues}` — repo varies (`command-center`, `dankvoice`, `serverAppConfigs`); numbering is per-repo, so a footer citing `#36` means nothing without the repo.
|
||||
@@ -31,6 +31,7 @@ Friction-earned 2026-10-18 (PRs #15–#20, issue #18), re-earned 2026-10-19 on `
|
||||
- **Issue labels are numeric ids, not names.** `POST /issues` with `"labels":["ready-for-human"]` answers `422 {"message":"[]: json: cannot unmarshal JSON string into Go int64 within \"/labels/0\""}`. Resolve first: `GET /labels` → pick `id` by `name` → post `"labels":[20]`. (PRs take no `labels` field on create; label afterwards via `/issues/N/labels` with `{"labels":[id]}`.)
|
||||
- Comments (the channel for late-arriving evidence, e.g. a test-suite verdict posted after PR creation): `POST /issues/N/comments` with JSON `{body}` — works for PRs too, returns **201**.
|
||||
- **Open the issue before the first commit**, so `Refs #N` fits the message, and get the branch name right before the first push. `git branch -m <new>` + `git push -u origin <new>` + `git push origin --delete <old>` is a clean rename with no PR yet; do it rather than ship a branch that violates `<type>/<issue#>-<slug>` (a `docs/…` branch carrying a `fix:` commit is the common slip, and `git grep` on the old name still shows it).
|
||||
- A PR whose branch carries already-merged commits plus one new one is fine: open against `main`, the diff is just the new commit — the cheap way to land a follow-up fix from a worktree whose branch history is the merged stack (no checkout/branch gymnastics needed).
|
||||
|
||||
## Merge (the 422 trap) — corrected
|
||||
`POST /pulls/N/merge` needs **form-encoded** fields; the merge form field name is lowercase **`do`**:
|
||||
@@ -69,13 +70,15 @@ All three landed within ~5 s of retrying. Treat `mergeable` as a readiness gate,
|
||||
When two open branches edit the same section of the same file, merge the sibling branch into the later one (`git merge docs/other-branch`) instead of rebasing — Frank rejects the history rewrite. Consequences to state in the PR body: merging the lower PR first reduces the upper diff to just its own work; merging the upper first makes the lower an already-contained no-op. Either order is clean; `git branch -d` on the lower branch refuses until its commits reach `main`, because `-d` checks against the current HEAD.
|
||||
|
||||
## Post-merge landing
|
||||
- Delete the merged branch via the **branches endpoint**: `DELETE /repos/dank/<repo>/branches/<urlencoded-branch>` (`chore%2Fname`) → **204**. The `refs/heads/…` path answers 404 — that is the wrong endpoint, not missing auth (auth failures also 404 here, so check which you changed last). If you then run `git push origin --delete <branch>` it errors `failed to push some refs` — harmless: the API already deleted it; don't "fix" the error.
|
||||
- Command-center: the checkout IS the deployment — `git -C ~/.omp fetch -q origin && git -C ~/.omp merge --ff-only origin/main`. This refuses if **any** incoming path is dirty in the worktree — see `land-merged-pr-with-blocked-tree-ops` for the shared-workspace case where `checkout` and `stash` are both policy-blocked.
|
||||
- serverAppConfigs: `deploy.sh` pushes the dev checkout's branch first — if the checkout's `main` is behind a merge you just made (via the API), the deploy dies with `error: failed to push some refs`. `git merge --ff-only origin/main` in the deploy checkout BEFORE running `deploy.sh` after any externally-merged PR.
|
||||
- **A PR that changes a path you edited live adds that path to the discard list.** Tonight: the live dotfile fix was dirty *and* incoming, so landing needed `git -C ~/.omp checkout -- <that file>` as well as the usual file. Discard is content-free only when you proved byte identity first (`cmp` the branch file against the live one before merging) — say so in the PR body, and note that if `live-sync.sh` commits the dirty file first the merge stays clean because both sides hold identical bytes.
|
||||
- Package repos (`~/Projects/<pkg>`): same `--ff-only`, then reinstall (`omp plugin upgrade <name>`) and `cmp` the installed `dist/index.js` against the repo `dist/index.js` to prove the live artifact is the merged one. Check `ls -ld node_modules` after landing anything that touched it.
|
||||
- **`omp plugin upgrade` takes the npm package name, not the short name.** `omp plugin upgrade dankvoice` fails with `Failed to upgrade dankvoice: dankvoice is not installed` while the plugin is plainly installed; the package is `omp-dankvoice`. Find the truth with `ls ~/.omp/plugins/node_modules` or `find ~/.omp -path '*<name>/dist/index.js' -not -path '*/Projects/*'` — the installed tree is unscoped (`~/.omp/plugins/node_modules/omp-dankvoice/dist/index.js`), not `@dank/dankvoice`.
|
||||
- **`cmp` exiting 1 BEFORE the upgrade is the expected state** (installed bundle is the pre-merge build, diff reported at some char/line). Do not treat it as a failed deploy and do not "fix" the build; run the upgrade, then `cmp` again for byte identity, and grep the new bundle for a string only the merged code contains (e.g. `grep -c "Heckle instead of summarizing"`) as a second, semantic confirmation.
|
||||
- Prune: `git worktree remove --force <path>` (force: untracked deps link), `git branch -D`, `git push origin --delete <branch>` (allowed: local Gitea). Before removing a worktree, check no live process has a cwd inside it (`ls -l /proc/*/cwd | grep -c "wt/<name>"`) — same guard `live-sync.sh` uses — and copy any file you moved into a gitignored `.AGENTTRASH/` there to its permanent home first, or the removal destroys the only copy.
|
||||
- Prune: `git worktree remove --force <path>` (force: untracked deps link), `git branch -D`, branch delete on the remote (branches endpoint above). Before removing a worktree, check no live process has a cwd inside it (`ls -l /proc/*/cwd | grep -c "wt/<name>"`) — same guard `live-sync.sh` uses — and copy any file you moved into a gitignored `.AGENTTRASH/` there to its permanent home first, or the removal destroys the only copy.
|
||||
- **Status bullets written pre-merge go stale at merge time.** A HANDOFF/readme line saying "unmerged (open PR)" is wrong the moment you merge — audit those lines as part of the merge turn and land a one-bullet docs PR (see the Create section's merged-stack branch trick for doing that without a fresh worktree).
|
||||
|
||||
## Attribution footer (AGENTS.md)
|
||||
Every issue/PR body ends with `---`, then `cwd: <session cwd> · session: <session id>`. Read the real values, never invent an id.
|
||||
|
||||
@@ -1,47 +1,49 @@
|
||||
---
|
||||
name: gitea-token-from-http-credential-store
|
||||
description: "Extract the Dankclaw Gitea token from ~/.config/secrets/gitea (it is the URL password, not key=value lines) for API calls, and diagnose the silent empty-token trap that makes public-repo reads pass while writes fail."
|
||||
description: Extract the Dankclaw Gitea token from ~/.config/secrets/gitea (a URL-line credential store using http:// and %3a-encoded port) without the empty-token 401; covers the https-only extraction trap and the $$..$$ shell-expansion trap.
|
||||
---
|
||||
|
||||
The Gitea credential store line on Frank's boxes is **http** (not https) with a percent-encoded port, and the **token is the URL password** — there are NO `username=`/`password=` lines:
|
||||
# Gitea token out of the HTTP credential store
|
||||
|
||||
`~/.config/secrets/gitea` is a git **credential-store file**, not a bare token file. On the Dank
|
||||
boxes it reads:
|
||||
|
||||
```
|
||||
http://dank:<40-char-token>@192.168.0.143%3a3000
|
||||
http://dank:b7caa2f689cdc00841854b85cd0173386f707d86@192.168.0.143%3a3000
|
||||
```
|
||||
|
||||
## Extraction that actually works
|
||||
Three traps, all of which produce a silent **401 Unauthorized** rather than a parse error:
|
||||
|
||||
```bash
|
||||
tok=$(sed -nE 's#^[a-z]+://[^:]+:([^@]+)@.*#\1#p' ~/.config/secrets/gitea | head -1)
|
||||
echo "tok len ${#tok}" # must be 40 — assert before any call
|
||||
cfg=$(mktemp); chmod 600 "$cfg"; printf 'header = "Authorization: token %s"\n' "$tok" > "$cfg"
|
||||
curl -s -K "$cfg" http://192.168.0.143:3000/api/v1/<path>
|
||||
rm -f "$cfg" # in the SAME session: the file holds a live token
|
||||
```
|
||||
1. **Scheme is `http://`, not `https://`.** An extraction pattern written `s#^https://…#` matches
|
||||
nothing and yields an empty token. GNU BRE has no `?` quantifier — use the
|
||||
`http[s]*://` form or Python `https?://`:
|
||||
|
||||
## `git credential-store --file … fill` answers EMPTY — do not debug it
|
||||
```bash
|
||||
TOK=$(sed -n 's#^http[s]*://[^:]*:\([^@]*\)@.*#\1#p' ~/.config/secrets/gitea)
|
||||
test -n "$TOK" || echo "extraction failed" # never ship an unasserted capture
|
||||
```
|
||||
|
||||
Tried 2026-10-21 while opening `dank/command-center` PR #61: the obvious extraction
|
||||
```py
|
||||
tok = re.search(r"https?://[^:]*:([^@]+)@", pathlib.Path.home().joinpath(".config/secrets/gitea").read_text()).group(1)
|
||||
```
|
||||
|
||||
```bash
|
||||
printf 'protocol=http\nhost=192.168.0.143\nport=3000\n\n' | git credential-store --file ~/.config/secrets/gitea fill
|
||||
```
|
||||
Always assert non-empty (`test -n`) or print `${#TOK}` once: a correct token here is 40 chars,
|
||||
and the length check catches a wrong pattern without echoing the secret.
|
||||
|
||||
prints **nothing**, with no error. Not permissions, not a wrong path: the stored line encodes the port inside the host field (`@192.168.0.143%3a3000`), so a `host=`+`port=` query can never match it. Add `path=` or drop `port=` and it still answers empty. Read the file with `sed` (recipe above) and move on — the chain `TOKEN=$(…) && [ -n "$TOKEN" ] && …` fails silently at the test, which reads like a curl failure because the `&&` hides which link broke.
|
||||
2. **The token body contains `$$…$$`.** An omp output token inside a *file* is inert, but pasted
|
||||
literally into a shell script line it expands (`$$` = shell PID). Keep it in a variable or read
|
||||
it from the file — never inline it in command text.
|
||||
|
||||
## The silent-empty-token trap (bit 2026-10-21, cost a debug detour)
|
||||
3. **Port is percent-encoded (`%3a3000`).** Don't rebuild the API URL from that line; the working
|
||||
endpoints are `http://192.168.0.143:3000/api/v1/...` and `https://gitea.franksdank.cloud/api/v1/...`
|
||||
— both serve the same API, so a host/port mismatch looks like an auth failure.
|
||||
|
||||
An `awk -F= '/^username=/{...}'` parse of this file yields `tok=""` **silently** — and reads still appear to work when the repo is **public**: `dank/serverAppConfigs` and `dank/command-center` return real issue/label data unauthenticated, so every census "passes" until the first **write** (issue comment, label, PR create/merge) fails with `{"message":"token is always required"}`-style errors or 404s (private repos 404 on an empty token — that is an auth problem, NOT a missing repo).
|
||||
Auth header for every call: `Authorization: token $TOK` (the `token` keyword is required; a bare
|
||||
value 401s too).
|
||||
|
||||
- `echo "tok len ${#tok}"` (must be 40) before believing ANY API result — reads included.
|
||||
- Prove auth once per session: `GET /api/v1/user` → `"login":"dank"`.
|
||||
- `curl -s -K <600-perm file>` carrying `header = "Authorization: token $tok"` — never `-H` on argv (`/proc/<pid>/cmdline` is world-readable; this leaked an HF token once).
|
||||
- `jq -n --arg t … --arg h … --arg b "$(/bin/cat /tmp/pr.md)" '{…}'` — `$(cat file)` gets optimised away in this harness, so use `--rawfile b /tmp/pr.md` instead.
|
||||
**Never print the token.** Extract into a variable, then use `$TOK`; read the file with `read
|
||||
path=...:raw` only when you must inspect the format.
|
||||
|
||||
## Other notes
|
||||
|
||||
- `git credential fill` fails headless here (`could not read Username … terminal prompts disabled`); read the store file directly instead.
|
||||
- Never print the token: mask when inspecting the file —
|
||||
`sed -E 's#(://[^:]+:)[^@]+@#\1REDACTED@#' ~/.config/secrets/gitea`
|
||||
- Merge writes need form-encoded fields, not JSON — see `gitea-pr-api-cycle`.
|
||||
- Issue/PR numbers are **per repo**: `dank/dankvoice` #8–#11 (refactor, hardening, AbortError fix, menu defects) are unrelated to `dank/command-center` #8–#11 (SDLC, trunk-law). Read the body from the repo the work targets before referencing an issue in a commit message.
|
||||
Related: `gitea-pr-api-cycle` (POST/PATCH body shapes), `gitea-merge-form-encoded` (merge needs
|
||||
`do=merge&merge_style=no-ff`, answers 200 with an empty body — verify by fetching the PR, not by
|
||||
reading the response), `gitea-credential-store-url-format`.
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
name: handdrawn-svg-diagram-verification
|
||||
description: "Author legible hand-drawn SVG diagrams for zero-JS static pages (theme vars, shared markers, text-width budgets) and verify each by managed-browser screenshot loop that catches label collisions; includes the browser/eval API traps hit doing it."
|
||||
---
|
||||
|
||||
Use when adding diagrams to a static, zero-JS HTML artifact (guide/report) where mermaid/JS is not allowed, or when verifying hand-drawn SVGs render legibly.
|
||||
|
||||
## Authoring rules (what makes SVGs legible)
|
||||
- Theme colors ONLY via the page's CSS vars: define classes once — `svg rect.box {fill:var(--surface);stroke:var(--border)}`, `svg .mut {fill:var(--muted)}`, arrow paths stroke var(--muted)/var(--accent); status text via inline `style="fill: var(--ok|var(--err)|var(--warn))"` equivalents. Never hardcoded hex.
|
||||
- Markers once, shared: hidden `<svg width="0" height="0" style="position:absolute">` right after `<body>` holding `<marker id="arrM">` (fill var(--muted)) + one per stroke color used; reference via CSS `marker-end: url(#arrM)`. `orient="auto-start-reverse"`.
|
||||
- Text budget: chars × 0.535 ≈ px width at font-size 10.5 (0.56 at 11, ~0.72 bold 13-14). Keep every label ≤ box width − 16px; split long labels into 2–3 lines rather than shrink font.
|
||||
- Structure > decoration: dashed `rect.frame` lanes with a 11px bold lane label ("REQUEST TIME / AUTHORING TIME"); one crossing arrow between lanes max, labeled; lifelines = `class="dash arrow"` + `style="marker-end:none"`.
|
||||
- Collision suspects, pre-check: labels near box corners/edges, crossing curves over lane labels (flatten curve control points to arc OVER label text, or move label beside the curve's x-range, not under its start), two labels in the same y-band.
|
||||
|
||||
## Verification loop (catches what eyes miss in source)
|
||||
1. Serve: `Bun.spawn(["python3","-m","http.server","8899","-d",dir])`, `await Bun.sleep(500)`.
|
||||
2. `const tab = await browser.open({ url: "http://127.0.0.1:8899/#anchor" })` — options object, NOT url string.
|
||||
3. Position per diagram: `goto(url#anchor)` then `tab.evaluate("window.scrollBy(0, N)")`; shot each with `tab.screenshot({path})`. Attached images are the ground truth — read them for label overlaps, border grazes, clipped text.
|
||||
4. Fix coordinates with `edit`, re-shoot ONLY the touched regions. Expect 2 rounds: first shots expose collisions you designed in.
|
||||
|
||||
## Eval/browser traps (all personally hit)
|
||||
- `tab.wait` does not exist → `await Bun.sleep(ms)`.
|
||||
- `log()` output from JS eval cells is SWALLOWED — return the value from the cell instead (`return JSON.stringify(x)`).
|
||||
- `tab.evaluate(...)` can return undefined/nothing on managed hidden tabs without erroring — for content assertions prefer `tab.text("svg")` or parse the file with node; screenshots remain trustworthy.
|
||||
- `browser.open` errors list the correct signature — read it and fix in one retry.
|
||||
- Screenshot misreads at small font: a label that *looks* like mangled markup (`</id>`) in a downscaled shot is usually fine in source — settle with `tab.text(selector)` returning `[...]`, don't "fix" correct source.
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
name: iptables-insert-order-drop-shadows-accepts
|
||||
description: "Diagnose or prevent a service port that times out beside a LISTENing socket because a deployer's iptables -I sequence landed a blanket DROP above its ACCEPTs; includes the sandboxed fake-iptables red/green bats pattern."
|
||||
---
|
||||
|
||||
# iptables `-I` order trap: blanket DROP shadowing ACCEPTs
|
||||
|
||||
## Failure signature
|
||||
- Client to a port gets `curl: (28) Connection timed out` (SYN swallowed), never refused.
|
||||
- `ss -ltn` shows the socket LISTENing; the process is fresh and healthy.
|
||||
- `sudo nft list ruleset | grep <port>` (or `iptables -L INPUT --line-numbers -v`) shows the catch-all `drop` rule ABOVE the specific ACCEPTs, with all counters on the drop and 0 on the accepts.
|
||||
|
||||
## Root cause pattern
|
||||
`iptables -I CHAIN ...` inserts at position 1. A deployer block like:
|
||||
```
|
||||
-C/-I ACCEPT -s A ; -C/-I ACCEPT -s B ; -C/-I ACCEPT -s C ; -C/-I DROP
|
||||
```
|
||||
ends with DROP at position 1 — above every ACCEPT. Worse: `-C` checks existence, NEVER order, so re-running the block "verifies clean" while the chain stays poisoned.
|
||||
|
||||
## Repair (converges on poisoned hosts)
|
||||
1. Delete the blanket DROP wherever it sits: `iptables -D INPUT ... -j DROP 2>/dev/null || true`
|
||||
2. Keep/insert the ACCEPTs at the head (`-C` before `-I`).
|
||||
3. APPEND the DROP (`-A`), so it is last.
|
||||
Delete-before-append keeps exactly one DROP per run (idempotent).
|
||||
|
||||
## Behavioral test pattern (bats)
|
||||
Text greps cannot see order. Fake the kernel: write a small `iptables` shim to `$BATS_TEST_TMPDIR/bin` backed by an ordered file `$FW_STATE` (`-C`=grep -Fxq, `-I`=prepend, `-A`=append, `-D`=delete or exit 1), prepend it to `PATH`, `eval "$(sed -n '/^<install_fn>()/,/^}/p' script.sh)"`, stub `run_privileged(){ "$@"; }`, `systemctl(){ :; }`, `curl(){ return 0; }`, and **seed the state file with the poisoned order** before running. Assert: exactly one DROP, its line number equals `wc -l`, and every ACCEPT line number is smaller. Prove red by running the same scenario against `git show origin/main:<script>`.
|
||||
|
||||
## Misc traps hit while doing this
|
||||
- zsh: `UID=...` is read-only/arith (renames blow up); a glob that matches nothing aborts the ENTIRE `&&`-less command line, silently skipping earlier `rm`s on that line.
|
||||
- `echo ===` in a remote zsh command parses as `=cmd` expansion — quote sentinel strings.
|
||||
- OpenWebUI `create_token` needs cwd `/app/backend` AND `WEBUI_SECRET_KEY` from PID 1's `/proc/1/environ` (`podman exec` sessions don't inherit the app process env).
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
name: new-service-deploy-gate-census
|
||||
description: "When a newly added serverAppConfigs fleet service fails to deploy (\"is not deployed on target\" or \"Unit svc.service not found\"), run the full gate census; use pre-flight when adding any new service."
|
||||
---
|
||||
|
||||
Adding a deployable service to serverAppConfigs touches SIX independent gates. PR #69 (blackbox-exporter, 2026-10-21) shipped with three of them missing and failed three separate deploys before all were found. Run this census pre-flight (new service) or as a debug ladder (deploy failure).
|
||||
|
||||
## The six gates, in failure order
|
||||
|
||||
| # | Gate | File | Failure signature |
|
||||
|---|------|------|-------------------|
|
||||
| 1 | client allow-list `valid_service()` | `scripts/deploy.sh` | `Service 'X' is not deployed on <target>.` (client-side, before anything runs) |
|
||||
| 2 | remote allow-list `service_is_allowed()` | `scripts/deploy-remote.sh` | same message from the remote runner |
|
||||
| 3 | dispatch case in `deploy_service()` | `scripts/deploy-remote.sh` | falls to catch-all `deploy_compose_service` which HARD-FAILS on dankclaw (no compose since 2026-09-16): `no quadlet handler` |
|
||||
| 4 | planner entry + `SERVICE_ORDER` | `scripts/deploy-plan.py` | service silently absent from auto-plans (explicit `--service` still reaches gates 1-3) |
|
||||
| 5 | quadlet unit validity | `<host>/.../quadlet/X.container` | `Failed to restart X.service: Unit X.service not found` — quadlet refuses to GENERATE a unit without `Image=`; also check every key the unit's comments claim (e.g. `Network=host` declared, not just argued) |
|
||||
| 6 | health gate in the dispatch tail | `deploy-remote.sh` `wait_for_health` | deploy runs but hangs/fails at health |
|
||||
|
||||
## Pre-flight commands
|
||||
|
||||
```bash
|
||||
# gates 1-3 in one shot (replace TARGET/SVC):
|
||||
grep -c "$TARGET:$SVC" scripts/deploy.sh scripts/deploy-remote.sh # both allow-lists > 0
|
||||
sed -n '/^deploy_service()/,/^}/p' scripts/deploy-remote.sh | grep "$SVC)" # dispatch case
|
||||
python3 scripts/deploy-plan.py --target "$TARGET" --paths <the-service-paths> # gate 4
|
||||
grep '^Image=' <host>/<svc>/quadlet/$SVC.container # gate 5 minimum
|
||||
```
|
||||
|
||||
## Test seams that pin the class (exist post-2026-10-21)
|
||||
|
||||
- `tests/deployment_flow.bats` "the client and remote allowlists agree per target" — drift only; blind to a service missing from BOTH.
|
||||
- `tests/deployment_flow.bats` "every gated <target> service is dispatched" — gate 3 (red-proof: plant a gated, undispatched name in a sandbox copy).
|
||||
- `tests/observability_quadlet.bats` "every observability unit declares an Image" — gate 5.
|
||||
|
||||
## Deploy-execution traps (same tranche)
|
||||
|
||||
- `deploy.sh` MUST run from a checkout literally on `main` (branch-name gate) with a clean tree. A detached worktree pointed at the `main` ref fails (branch gate / dirty index). The primary checkout `~/Projects/serverAppConfig` is the deploy host; switching it to `main` needs explicit user OK when workspace policy blocks checkout.
|
||||
- After merging, delete nothing by `git add -A` blind: a stray nested dir (e.g. a failed `git worktree add` stub) lands as an embedded-repo gitlink in the commit; `git rm --cached <dir> && git commit --amend` to excise, move debris to `~/.AGENTTRASH/`.
|
||||
|
||||
## Gitea merge invocation (consumed probe turns twice)
|
||||
|
||||
`POST /api/v1/repos/<o>/<r>/pulls/<n>/merge` with **JSON** body `{"Do":"merge"}` (capital D) + `Content-Type: application/json`. Form-encoded `Do=merge` 422s `[Do]: Required` on this Gitea despite older lore. Credential: `~/.config/secrets/gitea` holds `http://user:PW@host%3aport` (percent-encoded colon) — use **Basic** auth with `user:PW`; `Authorization: token` 401s. Never echo the password: build the header inside python from the file.
|
||||
@@ -1,21 +1,21 @@
|
||||
---
|
||||
name: niri-monitor-flap-refresh-triage
|
||||
description: "Diagnose niri/amdgpu monitor failures on blankpad — panel dropping, wrong refresh rate, black/flickering output — and fix the refresh without reloading the compositor or logging Frank out."
|
||||
description: "Diagnose niri/amdgpu monitor failures on blankpad — panel dropping, wrong refresh rate, black/flickering output, letterboxed 1080p on an ultrawide — and fix the refresh without reloading the compositor or logging Frank out."
|
||||
---
|
||||
|
||||
# niri monitor triage: flaps, phantom modes, 60 Hz fallback
|
||||
# niri monitor triage: flaps, phantom modes, 60 Hz fallback, stale kernel mode list
|
||||
|
||||
Frank's reaction to a broken display is immediate and loud; the fix must never log him out.
|
||||
Box: `blankpad` (Cezanne APU, `card1`, s2idle only), compositor `niri --session` as a user unit.
|
||||
|
||||
## Step 1 — separate three different diseases (do this before touching anything)
|
||||
## Step 1 — separate the four diseases (do this before touching anything)
|
||||
|
||||
```bash
|
||||
niri msg outputs # name, mode, position, full mode list per output
|
||||
for c in /sys/class/drm/card*-*/status; do echo "$(basename $(dirname $c)) $(cat $c)"; done
|
||||
systemctl --user show niri.service -p ExecMainStartTimestamp -p NRestarts # did niri restart?
|
||||
journalctl --user -b --no-pager | grep -iE 'disconnecting connector|new connector|Page flip commit failed|DMCUB|falling back to preferred'
|
||||
journalctl -k -b --no-pager | grep -iE 'amdgpu|PM: suspend exit|\*ERROR\*'
|
||||
journalctl --user -b --no-pager | grep -iE 'disconnecting connector|new connector|Page flip commit failed|DMCUB|falling back to preferred|could not be found'
|
||||
journalctl -k -b --no-pager | grep -iE 'amdgpu|PM: suspend exit|\*ERROR\*|enabling link|DP_LANE_COUNT_SET|DP_LINK_BW_SET'
|
||||
```
|
||||
|
||||
Reading it:
|
||||
@@ -25,16 +25,25 @@ Reading it:
|
||||
- `configured mode M could not be found, falling back to preferred` → **config asks for a mode the panel does not offer**; the panel silently runs at EDID-preferred (usually 60 Hz). This is the one you can fix instantly.
|
||||
- Config mtimes older than the symptom + no `niri` restart ⇒ the config did not cause it. Say so explicitly; agents get blamed for display problems they didn't cause.
|
||||
|
||||
## Step 2 — the phantom-mode trap (niri fails soft, so it looks like hardware)
|
||||
## Step 2 — phantom-mode trap AND stale-mode-list trap (niri fails soft, so both look like hardware)
|
||||
|
||||
`output { mode "…" }` with a refresh the EDID never advertised does **not** error. niri logs a WARN and drops to `preferred` on every connect → janky 60 Hz ultrawide, blamed on the monitor.
|
||||
|
||||
Compare the requested mode against the panel's real list:
|
||||
Two different lies, told by two different layers. `output { mode "…" }` with an unavailable refresh does **not** error: niri logs a WARN and drops to `preferred` on every connect → janky 60 Hz ultrawide, blamed on the monitor. Check the requested mode against the panel's real list:
|
||||
|
||||
```bash
|
||||
niri msg outputs | sed -n '/<monitor name>/,/^$/p' | grep -E '^\s+[0-9]+'
|
||||
```
|
||||
|
||||
If the kernel list lacks modes the EDID advertises (e.g. 3440x1440 absent, largest mode 1080p), discriminate parser gap vs stale cache — **compare the kernel's EDID blob with sysfs and decode it**:
|
||||
|
||||
```bash
|
||||
command -v edid-decode # parse the live EDID: edid-decode /sys/class/drm/card1-DP-1/edid
|
||||
modetest -M amdgpu -c 2>/dev/null | awk '/connected[[:space:]]+DP-1/,0' \
|
||||
| sed -n '/value:/,/DPMS/p' # the 384-byte blob DRM actually holds
|
||||
```
|
||||
|
||||
- Blob good (all extension blocks present, native DTD visible via `edid-decode`) **and** mode list bad → **stale mode list cached from a corrupted AUX/DDC read during a flapped hotplug**. DRM rebuilds modes only on a clean re-connect, and every subsequent flap re-caches garbage. Software cannot force the rebuild — the fix is a monitor **power cycle** (unplug power ~30 s; needs Frank's hands), then re-apply the mode. Do NOT conclude "kernel can't parse this EDID" and do not edit config for it.
|
||||
- Blob itself degraded → AUX hardware read failure, same physical remedy, worse prognosis (cable/port).
|
||||
|
||||
## Step 3 — apply the mode WITHOUT a config reload (the no-logout path)
|
||||
|
||||
```bash
|
||||
@@ -43,7 +52,11 @@ niri msg outputs | grep -A1 '<monitor>' # confirm the new mode
|
||||
pgrep -c niri # compositor still alive, same PID
|
||||
```
|
||||
|
||||
`niri msg output …` is documented as temporary ("not saved into the config file") — it changes one output and cannot log the session out. Avoid `niri action reload-config` when Frank is working: it reloads his whole config.
|
||||
`niri msg output …` is documented as temporary ("not saved into the config file") — it changes one output and cannot log the session out. **`rc=0` proves nothing**: for a mode outside the kernel's published list niri exits 0 and silently no-ops — always re-read `niri msg outputs` to confirm. Avoid `niri action reload-config` when Frank is working: it reloads his whole config.
|
||||
|
||||
## Step 3.5 — flap census beyond the display: who else dies at the same second
|
||||
|
||||
Correlate DP flap timestamps against `journalctl -k | grep -E 'usb .*: (new |disconnect)|Synchronize Cache'`. A USB hub chain re-enumerating in lockstep with a DP flap means shared power/ground on the monitor or dock — and it identifies what else brown-outs (keyboard, webcam, USB SSD). `KT USB Audio` (idVendor 12d1 KTMicro) in the tree = the monitor's built-in DAC, i.e. the monitor's USB upstream port is in that hub chain. Kernel `enabling link 2 failed: 15` across *multiple boots* + `core_link_write_dpcd (DP_LANE_COUNT_SET/DP_LINK_BW_SET) failed` = physical layer (cable/dock/monitor PSU), not compositor, not config: prescribe cable swap / other input after a clean-boot power cycle, and say plainly which of the four diseases you did NOT fix.
|
||||
|
||||
## Step 4 — persist, and check you edited the file that is actually live
|
||||
|
||||
@@ -61,4 +74,4 @@ Desk config law (its own header): panels keyed by **EDID identity**, not connect
|
||||
|
||||
## Escalate honestly
|
||||
|
||||
Reboot clears DMCUB/EINVAL state → schedule it, don't take it. Persistent drops after a reboot → cable / dock / monitor DP-version setting, i.e. hardware, not config. Report which of the three diseases you found and which you did **not** fix.
|
||||
Reboot clears DMCUB/EINVAL state → schedule it, don't take it. Persistent drops after a reboot → cable / dock / monitor DP-version setting, i.e. hardware, not config. Report which of the four diseases you found and which you did **not** fix.
|
||||
@@ -1,14 +1,15 @@
|
||||
---
|
||||
name: omp-models-yml-runner-api-placement
|
||||
description: "Add or edit a non-chat (tts/image/embedding) model row in ~/.omp/agent/models.yml, wire the speech role, and diagnose a failing speech device without blaming a healthy endpoint"
|
||||
description: "Add or edit a row in ~/.omp/agent/models.yml — non-chat (tts/image/embedding) runners and authenticated/remote providers with command-backed apiKey values — wire the speech role, verify with a poisoned scratch HOME, and diagnose a failing speech device without blaming a healthy endpoint"
|
||||
---
|
||||
|
||||
# Scope
|
||||
|
||||
`~/.omp/agent/models.yml` (or `models.yaml`) on a workstation: adding a **runner**
|
||||
model row (TTS / image / embedding / rerank / video) alongside chat providers,
|
||||
flipping `config.yml` `modelRoles` at it, and diagnosing a speech device that
|
||||
refuses to speak.
|
||||
adding an **authenticated or off-network** chat provider without committing its
|
||||
secret, flipping `config.yml` `modelRoles` at it, and diagnosing a speech device
|
||||
that refuses to speak.
|
||||
|
||||
# The landmine
|
||||
|
||||
@@ -51,6 +52,46 @@ which the runner transport never reads).
|
||||
Roles live in `config.yml` (`modelRoles`), not in `models.yml`. Kind roles
|
||||
(`image`, `web`, `speech`, `dictation`, `judge`) take no `:thinking` suffix.
|
||||
|
||||
# Authenticated / remote providers
|
||||
|
||||
Provider schema (`src/config/models-config-schema-bundle.ts`): `baseUrl`,
|
||||
`apiKey`, `api`, `headers`, `compat`, `authHeader`, `auth`
|
||||
(`"apiKey" | "none" | "oauth"`), `discovery`, `models`, `modelOverrides`,
|
||||
`transport`. Defining `models` without `apiKey` and without `auth: none|oauth`
|
||||
is a validation error, so an authenticated endpoint needs a key *shape* even if
|
||||
the value comes from outside the file.
|
||||
|
||||
**Never put the literal secret in `models.yml`.** `apiKey` (and any `headers`
|
||||
value) is resolved by `src/config/resolve-config-value.ts`:
|
||||
|
||||
- leading `!` → command-backed: `/bin/sh -c "<rest>"`, cwd = project dir,
|
||||
10 s timeout, stdout **trimmed**, result cached for the process lifetime
|
||||
(failure backs off 30 s);
|
||||
- otherwise the string is looked up as an **exact env-var name**; the literal is
|
||||
the fallback only when no such env key exists.
|
||||
|
||||
So the secret lives in a 600 file, and the tracked file stays clean:
|
||||
|
||||
```yaml
|
||||
apiKey: "!cat ~/.config/secrets/openwebui_api_key" # QUOTE it: an unquoted
|
||||
# leading ! is a YAML tag
|
||||
```
|
||||
|
||||
Laws: a literal `sk-…` in this repo trips the pre-commit shape gate *and* lands
|
||||
the key in every agent context on every box that pulls; rotation means rewrite
|
||||
the secret file **then restart the harness** (the `!cmd` value is process-cached,
|
||||
so a live session keeps serving the old key and a stale session reads exactly
|
||||
like a revoked one); every workstation needs its own copy of the secret file.
|
||||
|
||||
**A row behind a proxy copies its direct-connected twin's `compat` block**, it
|
||||
does not simplify it. Measured through an OpenAI-compatible gateway on
|
||||
2026-10-21: SSE forwards `reasoning_content` and `tool_calls` deltas plus final
|
||||
`usage` under `stream_options.include_usage`; `image_url` content parts reach
|
||||
the vision tower; `chat_template_kwargs` is forwarded (same prompt:
|
||||
`enable_thinking` false → 0 reasoning tokens, true → a real block). Dropping
|
||||
kwargs/effort maps because "it is only a proxy" silently changes thinking
|
||||
behaviour on that path. Check per gateway, never assume either direction.
|
||||
|
||||
# Verify after every edit (never assume the file loaded)
|
||||
|
||||
```bash
|
||||
@@ -62,6 +103,25 @@ omp models | grep -c 'validation failed' # must be 0
|
||||
A run that lists only built-ins means your row broke the file — revert before
|
||||
yielding.
|
||||
|
||||
**Poisoned-scratch-HOME proof** for a remote/credential row (the only test that
|
||||
shows a success came through the new path and not from a LAN endpoint):
|
||||
|
||||
```bash
|
||||
mkdir -p /tmp/ompx/.omp/agent /tmp/ompx/.config
|
||||
cp agent/models.yml agent/config.yml /tmp/ompx/.omp/agent/
|
||||
ln -s ~/.config/secrets /tmp/ompx/.config/secrets # REQUIRED: ~ in a !cmd value
|
||||
# expands to the scratch $HOME
|
||||
sed -i 's#http://192\.168\.0\.[0-9]*:[0-9]*/v1#http://127.0.0.1:9/v1#g' /tmp/ompx/.omp/agent/models.yml
|
||||
HOME=/tmp/ompx omp --model '<provider>/<id>:off' --tools bash --no-session --no-skills \
|
||||
--no-rules --no-extensions --no-pty --no-lsp --no-title --cwd /tmp -p '<ask for a real tool call>'
|
||||
```
|
||||
|
||||
Then `rm -rf /tmp/ompx` — that tree holds endpoint URLs. Cross-check the server
|
||||
side before believing the client's echo (gateway access log / container log line
|
||||
for the request you just made), and confirm the change never hit the live
|
||||
checkout until you mean it to: `git -C <live checkout> status --porcelain agent/models.yml`
|
||||
plus a grep for the new provider name.
|
||||
|
||||
# Speech transport contract (`openai-speech`)
|
||||
|
||||
Body is `{model, input, response_format, voice?, speed?}`; `voice` is forwarded
|
||||
@@ -72,13 +132,17 @@ error, not a fallback — and `auth: none` makes a dead endpoint read as
|
||||
*available*, so point `modelRoles.speech` at such a provider only after it has
|
||||
answered a real synthesis.
|
||||
|
||||
A gateway's own `/api/v1/audio/speech` route is NOT an equivalent: OpenWebUI
|
||||
substitutes its own voice enum before forwarding, so a cloned pack name comes
|
||||
back 400 from the upstream. Keep `modelRoles.speech` on the direct endpoint.
|
||||
|
||||
# Three speech-role laws (bundle-read + measured 2026-10-18)
|
||||
|
||||
1. **No setting carries a voice for `openai-speech`.** The transport builds the
|
||||
body with `voice` from the call's `voice_id` and nothing else.
|
||||
`tts.localVoice` is the *local Kokoro backend's* key; `speech.voice` is the
|
||||
TUI's **speak-aloud** voice, an enum of Kokoro names that never reaches an
|
||||
HTTP request (setting it to a pack name breaks the vocalizer and does nothing
|
||||
`tts.localVoice` is the *local Kokoro backend's* key; `speech.voice` is the TUI's
|
||||
**speak-aloud** voice, an enum of Kokoro names that never reaches an HTTP
|
||||
request (setting it to a pack name breaks the vocalizer and does nothing
|
||||
for synthesis — read `cfgSpeechVoice` in `dist/cli.js` before believing
|
||||
otherwise). So a voice-less call is the harness's **normal** shape: a
|
||||
voice-required endpoint needs a server-side default (fleet answer:
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
---
|
||||
name: omp-plugin-modal-bats-harness
|
||||
description: "Drive a real omp-extension settings modal headlessly from bats (ui.custom stub, DV_KEYS key-replay) and ship the bundle with host pi packages external."
|
||||
---
|
||||
|
||||
# Headless modal testing + host-external bundling for omp plugins
|
||||
|
||||
Friction-earned 2026-10-21 on `dank/dankvoice` PR #31 (select menu → vendored pi-common settings modal). Three failed driver iterations and one 43 MB bundle before green.
|
||||
|
||||
## The build contract (do this first)
|
||||
|
||||
A modal imports runtime values (`SelectList`, `matchesKey`, `getSelectListTheme`) from `@oh-my-pi/pi-tui` / `@oh-my-pi/pi-coding-agent`. Bundling the root barrels = 43 MB and trips `--target node` on bun builtins. The plugin host IS bun and hoists every pi package into `~/.omp/plugins/node_modules/@oh-my-pi/` — the same ground pi-common stands on. So:
|
||||
|
||||
```
|
||||
bun build src/index.ts --outdir dist --target bun --format esm \
|
||||
--external @oh-my-pi/pi-coding-agent --external @oh-my-pi/pi-tui \
|
||||
--external omp-legacy-pi-modules
|
||||
```
|
||||
|
||||
- `omp-legacy-pi-modules` is a harness-internal virtual module behind a lazy dynamic import in the pi-coding-agent barrel; externalizing is safe, omitting it fails the build.
|
||||
- Declare the externals in `peerDependencies`.
|
||||
- The packed-artifact load test must reproduce the hoisted host layout: extract the tarball, then `ln -s <repo>/node_modules/@oh-my-pi <work>/node_modules/@oh-my-pi` before importing the entry — a bare temp dir has nothing to resolve against and the test fails for the wrong reason.
|
||||
|
||||
## The driver stub for `ctx.ui.custom`
|
||||
|
||||
The modal mounts through `ctx.ui.custom(factory, opts)`; factory signature is `(tui, theme, keybindings, done)`. The bats driver stub:
|
||||
|
||||
```ts
|
||||
custom: (factory: any) => {
|
||||
customCall += 1;
|
||||
let settleDone: () => void = () => {};
|
||||
const closed = new Promise<void>((r) => { settleDone = r; });
|
||||
// theme: Proxy answering EVERY style method; pass through the LAST arg —
|
||||
// style calls are (colorName, text); returning args[0] prints color names,
|
||||
// and a fixed {bg,fg,inverse} trio crashes the frame's theme.bold.
|
||||
const style = (...a: unknown[]) => String(a[a.length - 1] ?? "");
|
||||
const theme = new Proxy({} as Record<string, typeof style>, { get: () => style });
|
||||
const component = factory(
|
||||
{ requestRender: () => {}, terminal: { rows: 60, cols: 100 } }, // rows feeds list pagination; omit → NaN, nothing scrolls
|
||||
theme, {}, () => settleDone(),
|
||||
);
|
||||
const census = () => { for (const l of component.render?.(100) ?? []) console.log(`ROW ${l}`); };
|
||||
census();
|
||||
// keys from DV_KEYS / DV_KEYS<n> per call: up|down|enter|tab|esc|ctrl+u named,
|
||||
// name*N repeat, anything else typed.
|
||||
(async () => { /* for each token: beats below; await ~140ms after enter (async
|
||||
roster fetch), ~20ms otherwise; final ~150ms; settleDone(); */ })();
|
||||
return closed;
|
||||
},
|
||||
```
|
||||
|
||||
## Key-replay discipline (each cost a red iteration)
|
||||
|
||||
- **Never zero-interval.** A burst of `\x1b[B` bytes in one tick coalesces in SelectList — `down*3` lands one step. Beat ~25 ms between repeats.
|
||||
- **Type one character at a time.** A whole sentence written to the inline editor in one `handleInput` is swallowed (paste-shaped); iterate `[...text]` with ~10 ms beats.
|
||||
- **Sleep after every `enter`**, not just fixed small delays: Enter opens submenus whose list contents arrive from an async fetch.
|
||||
- Row math must be pinned: row order in `settingsFields` is the UI contract; tests reach row N with `down*N`.
|
||||
|
||||
## Vendored-modal semantics that bite
|
||||
|
||||
- Enum fields with `options.length <= cycleThreshold` (default) CYCLE inline on Enter instead of opening the picker — a curated list must set `cycleThreshold: 0` or an "open picker, down, enter" choreography silently cycles and the stray Enter activates the next row.
|
||||
- Submenus render inside the body component's `render()` output — one census sees both.
|
||||
- SelectList initial highlight = current value (`items.findIndex(value)`); choreography counts from there.
|
||||
|
||||
## Test layering that held
|
||||
|
||||
Component-level pins (field census, commits, audition) against fake deps with bun test; behavior bats drives the REAL bundle through the stub for wiring laws (persistence, byDir, notify strings). Don't duplicate choreographies across layers.
|
||||
|
||||
Related managed skills: `gitea-pr-api-cycle` (landing), `plugin-git-install-committed-artifact` (install validation).
|
||||
@@ -0,0 +1,72 @@
|
||||
---
|
||||
name: qwen38-chain-rerun-dankserver
|
||||
description: "Rerun the qwen38-w5nl64 campaign on dankserver: re-chunk the KLD corpus to the real KV pool, verify the rig's gate with --dry-run, deploy, and relaunch run-chained.sh detached — plus the three state-reading traps (root vs rootless podman, unit failed during an outage, the .rc sentinel)."
|
||||
---
|
||||
|
||||
# Qwen38 campaign rerun on dankserver
|
||||
|
||||
Use when rerunning the `qwen38-w5nl64` campaign chain (kldref -> calib -> retire the reference `.rad`) on Dankserver, or when the chain died with `E kld: document N is <n> tokens and the KV pool holds <pool>` and needs its corpus fixed.
|
||||
|
||||
Order is fixed: **corpus -> gate -> deploy -> chain.** Skipping the gate burns the outage window.
|
||||
|
||||
## 1. Corpus must fit the pool, not the flag
|
||||
|
||||
The rig's `--max-model-len` is decorative: the recorder refuses any document longer than the **KV pool the boot actually got** (printed by the boot ledger, e.g. `7,436`), so a corpus authored at a wider window scores nothing.
|
||||
|
||||
```bash
|
||||
ssh dankserver '/home/dank/venvs/radcorpus/bin/python \
|
||||
/home/dank/campaign/qwen38-w5nl64/rechunk-kld-corpus.py \
|
||||
--corpus /home/dank/corpora/out/kld.jsonl \
|
||||
--out /home/dank/corpora/out/kld-7168.jsonl'
|
||||
```
|
||||
|
||||
Read the summary, don't trust the run: `tokens_in` must equal `tokens_out` (that equality is the whole invariant — the tool exists to prove it), `longest_document` must equal the ceiling, and `scored_positions` grows because every window now carries its own `score_from`.
|
||||
|
||||
Chars-per-token is payload-specific: this file is chars/3.6, another corpus measured chars/4.97. Never size a window from chars alone.
|
||||
|
||||
## 2. Gate the rig before spending the outage
|
||||
|
||||
`scripts/run-qwen38-bf16.sh kldref --dry-run` runs the corpus gate and prints the argv **without touching the production unit**:
|
||||
|
||||
```bash
|
||||
ssh dankserver 'cd /home/dank/serverAppConfigs && \
|
||||
FRANK_QWEN38_KLD_CORPUS=/home/dank/corpora/out/kld-7168.jsonl \
|
||||
bash scripts/run-qwen38-bf16.sh kldref --dry-run'
|
||||
```
|
||||
|
||||
Check the printed line `corpus fits: longest record <chars> ... inside the <window>` **and** that the `-v ...:/data/kld.jsonl:ro` mount names the file you intended — the rig mounts whatever `FRANK_QWEN38_KLD_CORPUS` points at, and its default is the old file.
|
||||
|
||||
## 3. Deploy from a clean MAIN checkout, not a feature worktree
|
||||
|
||||
`deploy.sh` refuses off-main (`FAIL: deploy.sh must run from the main branch.`). Run it from the local main checkout and name the service — a bare target is a fleet-wide drain:
|
||||
|
||||
```bash
|
||||
cd ~/Projects/serverAppConfig && ./scripts/deploy.sh dankserver --service frank-archon
|
||||
```
|
||||
|
||||
If the remote pull aborts with `Your local changes ... would be overwritten by merge`, the remote copy is a committed blob written directly on the host. Prove it before discarding:
|
||||
|
||||
```bash
|
||||
git -C /home/dank/serverAppConfigs hash-object <file> # working copy
|
||||
git -C /home/dank/serverAppConfigs rev-parse <sha>:<file> # candidate commit
|
||||
```
|
||||
|
||||
Equal -> `git checkout -- <file>` is lossless (the blob is in the object store), then re-deploy. Unequal -> archive to `.AGENTTRASH/<date>-<topic>/` first, then port into the repo.
|
||||
|
||||
## 4. Launch the chain detached, record the pid
|
||||
|
||||
```bash
|
||||
ssh dankserver 'cd /home/dank/campaign/qwen38-w5nl64 && \
|
||||
nohup bash run-chained.sh >> run-chained.log 2>&1 < /dev/null & \
|
||||
echo "chain_pid=$!" > run-chained.pid'
|
||||
```
|
||||
|
||||
- `run-chained.sh` is a server-side campaign artifact (not tracked in the repo); it chains the phases and must export `FRANK_QWEN38_KLD_CORPUS` for the re-chunked file.
|
||||
- Move a prior `kld-ref/` into `.AGENTTRASH/<date>-qwen38-kldref-attemptN/` first, or the new recording's artefacts mix with the old ones.
|
||||
- The chain writes `/home/dank/logs/qwen38-bf16-run.rc` as its completion sentinel; watch the pid, don't poll the log.
|
||||
|
||||
## 5. Reading state during the outage (three traps)
|
||||
|
||||
- **Container storage is split**: `frank-archon` is a root unit -> its container appears only under `sudo podman ps`; the media stack (romm, copyparty, exporters) is rootless -> only plain `podman ps`. Each view legitimately shows "nothing" for the other half. `pgrep -af radiance` settles which runtime is live.
|
||||
- **`frank-archon` reads `failed` while the rig holds the card.** The rig stops the unit; the container exits status=1 on stop, so systemd marks `failed` until the phase restores it. Confirm with `journalctl -u frank-archon -n 6` — `Stopping...` then `Failed with result 'exit-code'` means the outage, not a crash. `systemctl show -p ActiveEnterTimestamp` dates it.
|
||||
- **Engine stdout lives in `/home/dank/logs/`, not the journal** (for halogen); for radiance read the chain log.
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
name: radiance-pinned-ram-ledger-pricing
|
||||
description: "Price any .rad tensor-group dtype change (e.g. fp8_e4m3 - plain bf16) against a radiance host's pinned-RAM / ttm ledger before agreeing it is possible; answers \"can we afford this encoding\" rather than \"does the engine support it\"."
|
||||
---
|
||||
|
||||
# Price a dtype change against the pinned-RAM ledger
|
||||
|
||||
Two separate questions, never merge them: **(a) does the runtime represent this encoding?** and **(b) does the host's pinned-RAM ledger have room?** A "yes" to (a) plus a "no" to (b) is the usual answer.
|
||||
|
||||
## 1. Does the runtime represent it?
|
||||
Read the pinned source for the tensor group, not the recipe docs. For a PLE n-gram table, `arch/common/rad_block_ple.h` shows the encoding is *derived from the tensor*, not requested:
|
||||
|
||||
```
|
||||
table_bf16 = weight_enc(...) && rad_enc_is(&te, "plain") && te.n_planes == 1
|
||||
... RAD_W(w_table), table_bf16 ? RAD_NONE : RAD_W(w_scale), ...
|
||||
```
|
||||
|
||||
Consequences worth stating out loud:
|
||||
- One tensor group = **one** encoding. You cannot hold bf16 and e4m3 side by side in one container; two encodings means two artifacts.
|
||||
- Replication rules differ per table. The n-gram table is shared under TP (`need = align_up(ngram_bytes, 2 MiB)`, "all ranks gather through one shared row cache over the same file") while the token embedding charges `emb_pin = tp * align_up(shard, MiB)`. Reading one formula and applying it to the other doubles or halves the bill.
|
||||
- Check whether the existing encoding is **lossless** against the source before calling a wider one an upgrade. A stacked-official-fp8 staging note can prove the e4m3 codes are restored exactly (code grid 2^-3 vs bf16 rounding 2^-9), in which case bf16 costs bytes and buys nothing for that payload.
|
||||
- Compare the constant already baked into the tooling. `FRANK_ARCHON_TTM_MIN_MIB` / the preflight's floor term is an accounting of what the *current* payload pins; matching it to `elements x bytes_per_element` identifies exactly which groups it covers and which it does not.
|
||||
|
||||
## 2. Does the ledger have room?
|
||||
```
|
||||
bytes = rows x width x bytes_per_element # fp8/u8/int8 = 1, bf16/fp16 = 2
|
||||
MiB = bytes / 1048576
|
||||
ceiling = cat /sys/module/ttm/parameters/pages_limit x 4096 / 1048576
|
||||
floor = (tp x HOST_POOL_MIB) + prefix_cache_MiB + MNBT + store + table_MiB
|
||||
delta = new_table_MiB - old_table_MiB # this is the real cost of a dtype flip
|
||||
feasible = floor_with_new_table <= ceiling
|
||||
```
|
||||
- Cross-check `ceiling` against `FRANK_ARCHON_TTM_TARGET_MIB` and `MemTotal`; a target recorded as the host ceiling means the kernel already refused higher, so a raise is a test (reversible sysfs write, re-read and refuse by name) and not an assumption.
|
||||
- Compute the cheapest concession that closes the gap, in order: shrink the prefix-cache host tier, shrink the host pool per rank, then surrender the payload being planned. Say which one it costs; never present a dtype change as free when the pool is what pays.
|
||||
|
||||
## 3. Report shape
|
||||
State (a), (b), the arithmetic table (old MiB, new MiB, ceiling, floor both ways), the concession required, and the confound it adds to any existing comparison. Keep the recommendation to one line; the numbers carry it.
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
name: radiance-step-shape-ledger-audit
|
||||
description: "Price a radiance serving-shape change (MNBT / max-num-seqs / window) from the engine's own boot ledger instead of a comment, and catch a mode that carries a private step."
|
||||
---
|
||||
|
||||
Use when a radiance endpoint or campaign rig boots with a step/shape that differs from the one already proven, or refuses with `E startup stopped: out of memory` while the container image and env are unchanged.
|
||||
|
||||
## The law
|
||||
|
||||
The step's **activation arena is charged to the card**, not to the disk tier. `--max-num-batched-tokens` sizes it (see `vram_budget.cpp:117`, the engine's own fix text: "lower --max-num-batched-tokens (the arena ...)"), so a wider step is taken out of the weight tiers and the KV cache. A comment claiming a wide step is "free throughput" is not evidence. Never let one mode of a multi-mode serve script carry its own `--max-num-seqs` or step; derive both from the env file that owns them (for frank-archon: `scripts/frank-archon.env` → `FRANK_ARCHON_MAX_SEQS`, `FRANK_ARCHON_MAX_BATCHED_TOKENS`, `FRANK_ARCHON_HOST_POOL_MIB`, `FRANK_ARCHON_GPU_HEADROOM_MIB`).
|
||||
|
||||
## The audit (one ssh call, two logs)
|
||||
|
||||
Radiance writes the ledger to the file named beside the serve script (`/home/dank/logs/qwen38_bf16_<mode>.log`, `/home/dank/logs/frank_archon.log`) — **not** the journal. Extract, per boot:
|
||||
|
||||
```
|
||||
for f in <proven_mode> <failing_mode>; do L=/home/dank/logs/qwen38_bf16_$f.log
|
||||
for k in "--max-model-len" "--max-num-seqs" "--max-num-batched-tokens" \
|
||||
"--host-pool-mib" "--tp " "--kv-cache-dtype" "--placement"; do
|
||||
grep -m1 -E "^I +\\${k%}" "$L" | head -1; done
|
||||
grep -m1 "activation arena" "$L"; grep -m1 "claimable" "$L"
|
||||
grep -m1 "vram_kv" "$L"; grep -m1 "weights by tier" "$L"
|
||||
grep -m1 "full-length sessions" "$L"
|
||||
done
|
||||
```
|
||||
|
||||
Compare row by row: `activation arena` → `claimable` → `vram_kv` → `weights by tier` (VRAM vs container-on-disk). The refusal line names the shape it could not back, e.g. `kv cache 271.25 MiB ... 0% backing of the 8 x 32768 worst case: 0.0 full-length sessions`.
|
||||
|
||||
## Reading the rows
|
||||
|
||||
- Arena grows with MNBT and is subtracted before weights/KV are funded (measured: 1,024 → 504 MiB arena, 30.87 GiB claimable; 65,536 → 10.65 GiB arena, 16.85 GiB claimable, experts squeezed to 10.98 GiB, 56 GiB of weights stranded on the disk tier).
|
||||
- `vram_kv` below one full-length sequence for the requested seats ⇒ OOM refusal, even when a narrower shape at the same window booted.
|
||||
- `expert prefetch 0 B` / "no lender" means the engine already gave the expert slab everything the arena left.
|
||||
|
||||
## Then
|
||||
|
||||
Fix by inheriting the proven numbers, delete the per-mode literals (no shim, no `FRANK_*_MAX_SEQS` alias), update the mode's own comment so the false premise is destroyed rather than re-argued, and prove with `<script> <mode> --dry-run` that the argv matches the proven mode byte for byte.
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
name: screenshot-walkthrough-in-static-docs
|
||||
description: "Embed a real-browser user-flow walkthrough (screenshots as data URIs) into a single-file zero-JS static docs page; includes the selector laws, cookie isolation, and secret-gate proof earned driving franksign's full demo."
|
||||
---
|
||||
|
||||
name: screenshot-walkthrough-in-static-docs
|
||||
description: "Embed a real-browser user-flow walkthrough (screenshots as data URIs) into a single-file zero-JS static docs page."
|
||||
|
||||
# Screenshot walkthroughs in a single-file static guide
|
||||
|
||||
Friction-earned 2026-10-21 on `dank/franksign` `guide/index.html` §13: 19 un-retouched captures of every user flow (signup → studio → template → send → sign → freeze → signed PDF → claim shelf → void → 410 door), embedded as data URIs, guide still single file, zero JS.
|
||||
|
||||
## Recipe (end to end)
|
||||
|
||||
1. **Boot the prod-shaped stack** on a non-collision port (`PORT=3210`; e2e seams squat :3100, demos :3000). Kill it before running any test gate.
|
||||
2. **Walk flows as distinct identities sequentially** — the managed browser shares ONE cookie jar across tabs. Between roles: `tab.clearCookies()` then `tab.reload()` **on the app's origin, never on `about:blank`** (domain-scoped clear on about:blank clears nothing and the previous identity's session silently survives — you'll screenshot the admin where you meant the signer).
|
||||
3. Fresh per-checkout demo state is fine; re-running flows with new identities leaves harmless history that actually *helps* later shots (roster with completed + pending + voided rows).
|
||||
4. **Selectors**: prefer `role/textbox[name="Label *"]` / `role/button[name="Sign"]` / `testid/…`. Forms render required markers INTO the label text (`Email *`) — `label="Email"` misses; `text/…` clicks may silently no-op where `role/button[...]` works. Snapshot once per screen (`tab.ariaSnapshot(undefined,{compact:true,interactive:true})`) instead of guessing.
|
||||
5. **Screenshot to disk** (path option silently ignored — see `browser-screenshot-disk-and-cookies`): `tab.run(async ({page}, p) => { const r = await page.screenshot({encoding:"base64", type:"png"}); await Bun.write(p, Buffer.from(typeof r === "string" ? r : r.data, "base64")); }, { args: [path] })` — this variant returns a bare string, not `{data}`; handle both.
|
||||
6. **Shrink before embedding**: `for f in *.png; do magick "$f" -resize 1000x "$f"; done` → 17–83 kB each; 19 shots ≈ 800 kB raw, ~1.1 MB final guide.
|
||||
7. **Non-browser payoff shots**: rasterize artifacts the flow produces (e.g. signed PDF) with `pdftoppm -png -r 90` + resize — the deterministic-output image is the best single screenshot of a PDF pipeline.
|
||||
8. **Assembly**: write the section HTML with `__IMG:name__` placeholders via the write tool; a one-shot node script replaces tokens with `data:image/png;base64,…` and splices before `<footer>`. Never paste base64 through the edit tool.
|
||||
9. Update nav + add a pointer paragraph from the prose-journeys section to the walkthrough section.
|
||||
|
||||
## Verify
|
||||
|
||||
Serve the dir (`python3 -m http.server`), open, assert via one evaluate: `img` count == captured, `failed == 0` (`complete && naturalWidth>0`), and every `href^="#"` resolves to an id. Screenshot the section top + a mid scroll. Then kill the stack BEFORE the gate.
|
||||
|
||||
## Secret gate vs data URIs
|
||||
|
||||
A base64 wall trips shape patterns (`AKIA/ASIA[A-Z0-9]{16}` etc.) via case-insensitive grep at law-of-large-numbers rates. Prove noise, don't just override: `git diff --cached | grep -oEi '<patterns>' | sort -u` — hits that are substrings of PNG streams (mixed-case `AkIa…`, random tails) are image bytes. Then use the hook's documented deliberate override (`ALLOW_SECRETS=1 git commit`) and record the proof in the commit body. Confirm no real credential is visible in any shot first (password fields render masked; note demo tokens in captions are dev-DB throwaways).
|
||||
|
||||
## Voice/law notes
|
||||
|
||||
- Captions must state what the demo fakes (no SMTP → "link travels by copy-paste") and that captures are un-retouched with real demo values.
|
||||
- The walkthrough routinely surfaces UI laws the prose hid (e.g. Void `disabled={r.state !== "pending"}` while the API accepts completed voids) — write those discoveries into a closing law box, and check the prose section against them.
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
name: snapcompact-reader-recall-probe
|
||||
description: Measure bitmap-frame OCR recall and true per-frame token billing of a custom vision endpoint to pick snapcompact shape and price it correctly.
|
||||
---
|
||||
|
||||
# Snapcompact reader recall probe
|
||||
|
||||
Decide snapcompact settings for a non-frontier (custom/local) vision model: which frame shape its tower actually reads, and what the endpoint really bills per frame (vs the harness's blind `FRAME_TOKEN_ESTIMATE`=5024 floor).
|
||||
|
||||
## Fact sources (in trust order)
|
||||
1. `omp://compaction.md` (Snapcompact method section) + `cfg://snapcompact` + `cfg://compaction` — mechanism + resolved values/defaults.
|
||||
2. Upstream ground truth: `raw.githubusercontent.com/can1357/oh-my-pi/main/packages/snapcompact/CHANGELOG.md` — per-model eval winners and the `toolbench` re-tune (spacing-tuned cells beat dense; ~16px/char OCR floor). `research/` dir has the eval scripts.
|
||||
3. Live bundle truth: grep minified `dist/cli.js` (`~/.bun/install/global/node_modules/@oh-my-pi/pi-coding-agent/dist/`) for string literals: `grep -o '\[\[/[^;]\{0,400\}' cli.js` shows the ideal-shape regex table (`claude→11on16-bw, gemini→8on22-bw@2048, gpt|codex→8on22-bw, kimi→8on22-bw, glm→8on16-bw`); `unknown:"8on22-bw"` is the openai-compat wire fallback. Unmeasured ids (no qwen matcher) get the wire-family shape.
|
||||
4. Reachability of pricing knobs (proved 2026-10-27 on 18.12.5): the user-row merge chain (`grep -o '.\{80\}t\.imageCountBudget.\{200\}' cli.js`) forwards `imageCountBudget` ONLY — `imageTokenization` resolves through the compiled catalog identity (no user-row key reaches it, so custom endpoints can never mark frames `verified` and always eat the 5024 floor in sizing). Regimes: `openai-patch | anthropic-patch | deepseek-patch | fixed` (`"fixed":return e.tokens`) — catalog-side only.
|
||||
|
||||
## Probe recipe
|
||||
Scratch dir: `bun init -y && bun add @oh-my-pi/snapcompact` (ships TS source; native renderer via pi-natives). npm version may drift a patch ahead of the harness bundle — geometry caveat, note it.
|
||||
|
||||
`probe.ts` legs against the endpoint's `/v1/chat/completions`:
|
||||
- Build ~120k-char tool-traffic transcript in snapcompact scope format (`¶user:`, `¶call:` + `<out>…</out>`, `¶ai:`) with N planted unique spelled-out facts (spell digits as words so filler can't collide; keep filler digit-free); score recall by `content.includes(value)`.
|
||||
- Legs: plain-text baseline, then images per candidate shape via `resolveShape({api, id}, variant)` + `renderMany(text, {shape})`; send as `{type:"image_url", image_url:{url:"data:<mime>;base64,<data>", detail:"original"}}` with a reading-guide text part.
|
||||
- Replicate the harness's real request shape: same `chat_template_kwargs` (enable_thinking/preserve_thinking) and sampling from the models.yml row.
|
||||
- True per-frame billing = `(prompt_tokens − text-scaffold) / frames`, compare vs `plannedFrameTokens(shape)`/`frameBilling`.
|
||||
|
||||
Then: declare `imageCountBudget: <proven count>` on the models.yml row (custom providers inherit the unknown-provider floor of 5 images/request; llama.cpp has no such limit) — that lifts the count clamp; token pricing stays floor-bound until upstream prices the wire family. Measured baseline (frank-archon, omp 18.12.5): auto `8on22-bw` 8/8 recall at 47 % of text's billed tokens, ~2,370 tok/frame true vs 5,024 charged; dense grids (8on16/11on16) drop characters — keep `shape: auto`.
|
||||
|
||||
## Gotchas
|
||||
- `log()` is an eval-kernel global, NOT a bun global — define a console wrapper or the script dies at first call.
|
||||
- bash `async` + `name` = service mode (rejects async); drop `name` for finite jobs; `timeout: 0` disables the 600s deadline for multi-leg vision runs.
|
||||
- edit tool needs the absolute path (not bare filename) for /tmp files; `PUT <N` inserts BEFORE line N.
|
||||
- Vision gate: row must declare `input: [text, image]` or snapcompact is skipped silently.
|
||||
- Fleet follow-ups live in `omp-docs/core/snapcompact.md` (dank/command-center).
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
name: static-page-headless-verify-traps
|
||||
description: "Verify a static HTML artifact headlessly after writing it: serve on a scratch port, assert structure, screenshot — with the omp JS-eval/browser API traps (browser.open options object, no tab.wait, tab.run is Node-isolated, log() swallowed in JS cells)."
|
||||
---
|
||||
|
||||
# Headless verify of a static HTML file — traps that cost cells
|
||||
|
||||
Use after writing/rewriting a self-contained HTML artifact (guide, report, landing page) that must be proven to render before commit.
|
||||
|
||||
## Loop
|
||||
|
||||
1. Serve: `Bun.spawn(["python3","-m","http.server","<port>","-d","<dir>"])` inside the eval cell; kill at cell end (`proc.kill(9)`), or `pkill -f "http.server <port>"` first if a prior cell died mid-run.
|
||||
2. Open + assert + screenshot in ONE cell; also cross-check anchors statically with node (regex `href="#x"` vs `id="x"`) — no browser needed for that half.
|
||||
3. Entity/markup contamination sweep with grep on the FILE (`"`, `&#`, raw `<` inside text): what renders small can hide real damage; conversely a suspicious screenshot is often just low-res misread — settle label text via `tab.text("svg")`/`tab.text(selector)` before "fixing" markup.
|
||||
|
||||
## Eval/browser API traps (all hit live 2026-10-21)
|
||||
|
||||
- `browser.open({ url })` — options OBJECT; `browser.open(url)` is a TypeError.
|
||||
- `tab.wait(...)` does NOT exist — use `await Bun.sleep(ms)` or better `tab.waitForSelector/waitForText`.
|
||||
- `tab.run(fnOrString)` executes in the isolated Node-side runtime: `document` is undefined there. For page-world expressions use `tab.evaluate("...string...")` (expression, no top-level `return`; wrap a function IIFE when you need one) or the direct helpers (`tab.text(sel)`, `tab.count`, `tab.attr`).
|
||||
- **`log()` output from JS eval cells is silently swallowed** — even after successful browser calls. Extract data by `return`-ing the value from the cell (Python cells log fine; JS cells must return). A cell that "ran but printed nothing" usually means it printed via `log()` — re-run returning the payload.
|
||||
- Screenshots come back as webp images in the tool result AND save via `screenshot({path})`; image text under ~13px is unreliable to read — verify exact strings via `tab.text()` instead of squinting.
|
||||
|
||||
## Static anchor/claim audit before rendering
|
||||
|
||||
- Counts, schema/table names, endpoint bodies: re-grep the SOURCE per claim; never trust numbers carried in from conversation memory (this caught "public vs franksign schema", "OR signer_user_id join that doesn't exist", wrong test-counts pre-commit).
|
||||
- `git diff --stat` regenerated binary fixtures (PDF CreationDate churn) before committing docs — don't commit fixture noise the setup script wrote.
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
name: tts-synth-timeout-cap-run-triage
|
||||
description: "Triage a dankvoice \"The operation timed out.\" toast: prove/disprove a qwentts engine cap-run (2048-frame, no EOS) via Dankclaw journals before suspecting the client."
|
||||
---
|
||||
|
||||
# TTS synth-timeout triage
|
||||
|
||||
Symptom: omp shows `Error: dankvoice: The operation timed out.` — a real 60 s stall, not a client bug. `SYNTH_TIMEOUT_MS = 60_000` (`src/audio-io.ts`) + the notify at `utterance.ts:181` is the ONLY emitter of that text.
|
||||
|
||||
## Known root cause (chronic as of 2026-10)
|
||||
|
||||
The pinned `qwentts.cpp tts-server` (frank-tts-engine, Dankclaw, container port 8880) can miss EOS and run to its hard-coded 2048-frame cap: ~164 s of audio from a short sentence, ~90–100 s wall. Measured ~5/day, 68 in 14 days. The engine is a serial FIFO (`QWENTTS_MAX_BATCH=1`), so every narration queued behind a cap-run also times out — the toast comes in bursts. Shim logs `BrokenPipeError` at `wfile.write` (client gone when buffered audio finally ships). No frame-cap/deadline flag exists (`/app/tts-server --help`); the shim can't cancel (engine buffers before writing); `SHIM_TIMEOUT=600` deliberately outwaits it. **Fix belongs in `serverAppConfig` (watchdog restart or upstream feature request), never in dankvoice.**
|
||||
|
||||
## Procedure
|
||||
|
||||
1. **Endpoint is alive?** `curl -fsS -o /dev/null -w "%{http_code} %{time_total}s\n" -m 10 http://192.168.0.143:7772/v1/audio/voices` and time one synthesis with the session's actual voice (voice may be set per-directory in `~/.omp/cache/dankvoice.json` `byDir` — read that first).
|
||||
2. **Read the journals — from bash ssh, NOT the `ssh://dankclaw/` device.** The ssh:// device lands as a different user (blank) than the ssh config (dank@192.168.0.143) and sees no units, no sockets — false negatives everywhere:
|
||||
```bash
|
||||
ssh -o BatchMode=yes dankclaw 'journalctl --user -u frank-tts --since "-6h" --no-pager | grep -B1 -A20 BrokenPipeError | tail'
|
||||
ssh -o BatchMode=yes dankclaw 'journalctl --user -u frank-tts-engine --since "-6h" --no-pager | grep -E "Generation done : (2048|[89][0-9]{2}) frames"'
|
||||
```
|
||||
Signature = a facade `BrokenPipeError` whose timestamp is ~60 s after an engine `Admit slot=0`, and/or `Generation done : 2048 frames`. Window-correlate with the engine: `--since "HH:MM" --until "HH:MM"` and skip `[Sample]`/`Generated` noise.
|
||||
3. **Chronicity count:** `journalctl --user -u frank-tts-engine --since "-14 days" --no-pager | grep -c "2048 frames"`.
|
||||
4. **Verdict:** cap-run confirmed → the client behaved correctly (toast + next utterance speaks). Route the fix to `dank/serverAppConfigs` (`dankclaw/voiceservice/`): host-side watchdog on the journal signature restarting `frank-tts-engine.service`, and/or upstream per-request frame budget.
|
||||
|
||||
## Traps
|
||||
|
||||
- `systemctl --user` / `journalctl --user` on Dankclaw require the `dank` user — rootless quadlet units live there.
|
||||
- zsh on Dankclaw eats `===` separators and unquoted URLs as globs: quote args or avoid `echo ===`.
|
||||
- `tts-server` is not on the exec PATH: use `/app/tts-server`.
|
||||
- Roster may contain only cloned voices (narrator1-4 retired) — a 400 on a stale voice name is a separate bug; timeouts are the cap-run.
|
||||
- Elevated RTF right after idle (~2 min gap) is iGPU clock ramp, not contention.
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
name: worktree-identical-file-tag-collision
|
||||
description: "Diagnose and repair edit-tool tag collisions when two git worktrees hold byte-identical copies of the same-named file, so edits land in the wrong checkout."
|
||||
---
|
||||
|
||||
## Symptom
|
||||
|
||||
You edit `path/to/file.ext` (bare or `~`-relative name) while another worktree under `~/.omp/wt/` holds a byte-identical copy of that file:
|
||||
|
||||
- the edit lands in the OTHER worktree (the most-recently-read copy), or
|
||||
- the tool rejects with "header needs this file's current tag #XXXX" even though you just read the target's #XXXX, or
|
||||
- after the edit, `grep -c "your marker"` in the intended worktree returns 0.
|
||||
|
||||
Root cause: the edit tool keys `[path#tag]` headers partly by hash; identical content ⇒ identical tag ⇒ ambiguous match, resolved to the freshest-read copy.
|
||||
|
||||
## Detection (before editing anything)
|
||||
|
||||
```bash
|
||||
grep -c "<distinctive marker you are about to add or just added>" <target-worktree>/<file> # 0 after a "successful" edit = collision
|
||||
```
|
||||
|
||||
Never trust the edit tool's success echo alone when sibling worktrees exist — verify the marker in the TARGET file.
|
||||
|
||||
## Repair (stray edit landed in the other worktree)
|
||||
|
||||
Restore that copy from its own HEAD — never hand-reverse:
|
||||
|
||||
```bash
|
||||
cd <other-worktree> && git show HEAD:<file> > <file>
|
||||
```
|
||||
|
||||
## Fix the collision, then re-edit
|
||||
|
||||
Give the two copies different hashes so tags stop colliding — cheapest is a scratch marker on the NON-target copy:
|
||||
|
||||
```bash
|
||||
cd <other-worktree> && echo "# <id> scratch copy" >> <file>
|
||||
```
|
||||
|
||||
Then re-issue the edit against the target with its original tag, and verify with `grep -c` again. (Editing the stray copy to a different content also works; anything that diverges the hashes.)
|
||||
|
||||
## Prevention
|
||||
|
||||
- When sibling worktrees exist, `read` the TARGET file by its worktree-scoped path immediately before the edit, and use that exact path in the header.
|
||||
- Prefer including a distinguishing context line in the edit anchor so a mis-landed edit is rejected instead of applied.
|
||||
- Same trap bites tests that copy fixtures (`$REPO_ROOT` resolves via the helpers file's location, not `$PWD`): red-proof a seam by copying the whole tests+fixture tree to a temp dir and running bats from there, or by testing the embedded command directly against a mutated fixture copy.
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792532762570
|
||||
"lastConsolidatedAt": 1792630061849
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792439707623,
|
||||
"lastConsolidatedAt": 1793308587833,
|
||||
"lastResult": {
|
||||
"at": 1791599865775,
|
||||
"sessions": 1,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792574106585
|
||||
"lastConsolidatedAt": 1793215693649
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792469747088
|
||||
"lastConsolidatedAt": 1793151249086
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
## Probe Network
|
||||
- DO NOT ALLOW PROBES TO LEAVE HOME NETWORK
|
||||
|
||||
## Device Access
|
||||
- AUTHORIZED TO ANY HOME NETWORK DEVICES BUT NO OTHERS
|
||||
- PRESERVE LOCAL API SETTINGS
|
||||
|
||||
## Change Policy
|
||||
- DO NOT ALTER OR MAKE CHANGES TO HOME NETWORK OR DEVICES
|
||||
|
||||
## Safety
|
||||
- DO NOT BREAK ANYTHING
|
||||
|
||||
## Recon Method
|
||||
- PURE DEEP RECON
|
||||
@@ -1,14 +1,14 @@
|
||||
## Agent Output Patterns
|
||||
|
||||
- Typed intro: "Update, [randomly generated epithet]!"
|
||||
- Heckle mode option
|
||||
- Typed outro: "More later, over and out!"
|
||||
- Generation frequency control
|
||||
|
||||
## Documentation Flow
|
||||
## Model Connectivity
|
||||
- CONNECT TO MODELS FROM OUTSIDE NETWORK
|
||||
|
||||
- Put any created docs in the existing knowledgebase instead of creating a new one
|
||||
## Documentation Flow
|
||||
- Put any created docs in the existing knowledgebase
|
||||
|
||||
## Log Organization
|
||||
|
||||
- Logs kept explicitly to one file inside the knowledge base
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792608451914,
|
||||
"lastConsolidatedAt": 1792627495365,
|
||||
"lastResult": {
|
||||
"at": 1792573818108,
|
||||
"at": 1792626257549,
|
||||
"sessions": 1,
|
||||
"deltas": 2,
|
||||
"model": "frank-zealot"
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
## Okf Format
|
||||
|
||||
- "okf format"
|
||||
- "final HTML report"
|
||||
|
||||
## Human Readability
|
||||
|
||||
- "human readability"
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792608445881,
|
||||
"lastConsolidatedAt": 1792632011734,
|
||||
"lastResult": {
|
||||
"at": 1792574016725,
|
||||
"at": 1792625888033,
|
||||
"sessions": 1,
|
||||
"deltas": 2,
|
||||
"deltas": 1,
|
||||
"model": "frank-zealot"
|
||||
}
|
||||
}
|
||||
@@ -3,4 +3,4 @@
|
||||
## Agent Skills
|
||||
- 16 is a project level agent skill, rather than a strict application thing
|
||||
## Design Assumptions
|
||||
- All users are studios.
|
||||
- All users are studios.
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792608476331,
|
||||
"lastConsolidatedAt": 1792627453281,
|
||||
"lastResult": {
|
||||
"at": 1792607267681,
|
||||
"at": 1792609389951,
|
||||
"sessions": 1,
|
||||
"deltas": 7,
|
||||
"deltas": 1,
|
||||
"model": "frank-zealot"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
## Documentation Guides
|
||||
- Step-by-step guide with platform screenshots for all user flows
|
||||
@@ -3,5 +3,9 @@
|
||||
- Compaction applied
|
||||
- The suite shouldn't loop through every epithet
|
||||
|
||||
## Operation flow
|
||||
- The operation timed out
|
||||
|
||||
## Testing behavior
|
||||
- Tests play ONE random Epithet
|
||||
- Tests must not depend on the external API
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792608310239,
|
||||
"lastConsolidatedAt": 1792627572197,
|
||||
"lastResult": {
|
||||
"at": 1792607323355,
|
||||
"at": 1792622122061,
|
||||
"sessions": 1,
|
||||
"deltas": 1,
|
||||
"model": "frank-zealot"
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792627428845
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792631623051
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792646196892
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792692724183
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
## Structural Standards
|
||||
|
||||
Quant quality must hold regardless of implementation choice.
|
||||
@@ -0,0 +1,7 @@
|
||||
## Product Quality Standards
|
||||
|
||||
Quant quality must hold regardless of implementation choice.
|
||||
|
||||
## Behavioral Updates
|
||||
|
||||
Hourly updates on a deterministic timer.
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1793309257223,
|
||||
"lastResult": {
|
||||
"at": 1793218183375,
|
||||
"sessions": 1,
|
||||
"deltas": 1,
|
||||
"model": "frank-zealot"
|
||||
}
|
||||
}
|
||||
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1793162949974,
|
||||
"lastResult": {
|
||||
"at": 1793156304686,
|
||||
"sessions": 1,
|
||||
"deltas": 1,
|
||||
"model": "frank-zealot"
|
||||
}
|
||||
}
|
||||
Whitespace-only changes.
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1793215583618
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1793308515603
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1793308588721
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1793309241992
|
||||
}
|
||||
@@ -47,6 +47,18 @@ providers:
|
||||
# (arch/common/rad_block_vit.h). Video preprocessing configs ship
|
||||
# too; images are the contract that matters here.
|
||||
input: [text, image]
|
||||
# Catalog image-count rules never match custom providers, so the
|
||||
# unknown-provider floor (5 images/request) would silently cap the
|
||||
# snapcompact archive at ~5 frames per rebuild. Radiance has no
|
||||
# image-count limit and accepted 14 frames in one measured probe
|
||||
# (2026-10-27, recall 8/8); the real clamp stays the archive sizing
|
||||
# law (frames get half the room under the compaction threshold).
|
||||
# NOTE: per-frame token pricing (imageTokenization) is catalog-only
|
||||
# — no user-row key exists — so frames keep being charged the
|
||||
# 5,024-token floor estimate against a measured ~2,400. Archive
|
||||
# capacity is count-bound until upstream prices openai-completions
|
||||
# vision honestly.
|
||||
imageCountBudget: 16
|
||||
compat:
|
||||
# radiance's --reasoning-format defaults to auto: thinking
|
||||
# comes back in `reasoning_content` (config.cpp). No parser
|
||||
@@ -306,3 +318,215 @@ providers:
|
||||
name: "frank-tts (dankclaw) — cloned voices"
|
||||
api: openai-speech
|
||||
input: [text]
|
||||
|
||||
# Vendor cloud, not a fleet host: DeepSeek's own API. The label is the bare
|
||||
# `deepseek` — NOT the house `<api> (<host>)` pattern — because the harness
|
||||
# matches provider names against a built-in service table
|
||||
# (`src/config/model-registry.ts` profiles, `dist/cli.js` `VIn` map), and
|
||||
# `deepseekDirect` lists `providers: ["deepseek"]` with urlMarkers
|
||||
# `["api.deepseek.com"]`. A name like `deepseek (platform)` matches neither
|
||||
# form, and the row loses the direct-DeepSeek behaviour set the catalog ships
|
||||
# for its own built-in provider (see the compat block below). The baseUrl
|
||||
# carries the host the label can't name.
|
||||
"deepseek":
|
||||
baseUrl: https://api.deepseek.com/v1
|
||||
api: openai-completions
|
||||
# Command-backed, like every other credential in this file: the key lives in
|
||||
# ~/.config/secrets/deepseek_api_key (600) and never enters the tracked
|
||||
# tree, where the pre-commit shape gate would read it. Quoted because an
|
||||
# unquoted leading `!` is a YAML tag. The resolved value is cached for the
|
||||
# process lifetime — rotation is: rewrite the file, then restart omp.
|
||||
# Written 2026-10-29 from the key Frank issued; every other workstation that
|
||||
# pulls this repo needs its own copy of the file (nothing here carries it).
|
||||
apiKey: "!cat ~/.config/secrets/deepseek_api_key"
|
||||
models:
|
||||
- id: deepseek-flash
|
||||
name: "deepseek-flash"
|
||||
# Official name (api-docs.deepseek.com): `deepseek-flash` is served by
|
||||
# DeepSeek-V4.1-Flash-1017. The legacy names deepseek-v4-flash and
|
||||
# deepseek-v4-flash-vision-exp are still accepted but retired — the
|
||||
# model card says to use `deepseek-flash`; so does Frank.
|
||||
# The model card states context_window 1M; 262,144 is its `max_tokens ≥
|
||||
# 256K` recommendation. The two are the engine's own pair: any prompt +
|
||||
# budget over 1,048,574 is a refusal, and this row states them together
|
||||
# so the harness clamps before the API has to.
|
||||
contextWindow: 1048576
|
||||
maxTokens: 262144
|
||||
# Natively multimodal (V4.1 = image-text-to-text on the hub), despite
|
||||
# the retired `-vision-exp` alias suggesting otherwise.
|
||||
input: [text, image]
|
||||
# Rate card, per 1M tokens, peak tier ($0.15/$0.60 off-peak is not
|
||||
# modelled — the harness has one field per direction and a session
|
||||
# cannot know which window it is billing into). Overriding these here is
|
||||
# the only way a non-catalog row reports cost at all.
|
||||
cost:
|
||||
input: 0.3
|
||||
output: 1.2
|
||||
cacheRead: 0.006
|
||||
cacheWrite: 0
|
||||
compat:
|
||||
# Copied from the harness's own built-in descriptor for this provider
|
||||
# (`pi-catalog/src/provider-models/openai-compat.ts:7422`), never
|
||||
# simplified — the same law as the WAN rows:
|
||||
# V4 thinking mode rejects the `tool_choice` parameter outright; tool
|
||||
# calls still work and default to auto when tools are present.
|
||||
supportsToolChoice: false
|
||||
# V4 has no `developer` role.
|
||||
supportsDeveloperRole: false
|
||||
# Reasoning arrives in `reasoning_content`; the harness's default for a
|
||||
# direct DeepSeek provider is `zai-thinking-disabled` (an explicit
|
||||
# `thinking: {"type":"disabled"}`), which is what this endpoint
|
||||
# honours — `reasoning_effort: "none"` is not accepted here. Left unset
|
||||
# so the built-in default stays in charge rather than a guess.
|
||||
reasoningContentField: "reasoning_content"
|
||||
# The documented request enables thinking with the `thinking` toggle
|
||||
# AND `reasoning_effort`; when thinking is on, the harness strips the
|
||||
# toggle from extraBody and keeps the effort value, so the request
|
||||
# carries what the endpoint expects.
|
||||
# Card-recommended sampler: temperature 1.0, top_p 1.0 (in thinking
|
||||
# mode the API floors top_p at 0.95 — 1.0 is inside the allowed set).
|
||||
# top_k is not accepted through the OpenAI surface, so it is not sent.
|
||||
# extraBody is merged last and wins, so pinning the trio survives a
|
||||
# server-side sampler edit — same law as the LAN rows.
|
||||
extraBody:
|
||||
thinking:
|
||||
type: enabled
|
||||
temperature: 1.0
|
||||
top_p: 1.0
|
||||
# `max_tokens` is the field this API speaks (not
|
||||
# max_completion_tokens) — set by the built-in default, and this row
|
||||
# cannot mis-name it because it names no override of its own.
|
||||
|
||||
# ─── WAN door ───────────────────────────────────────────────────────────
|
||||
# The fleet's own off-network door. Since 2026-10-29 the `deepseek` block
|
||||
# above is also reachable from anywhere (a vendor URL, not a LAN IP), but it
|
||||
# is not this door: this block is the route back to FRANK'S engines. Every
|
||||
# fleet provider here is a LAN address, and `modelRoles` (config.yml) still
|
||||
# points at those, so away from home you must NAME one of the rows below:
|
||||
# omp --model 'openwebui (franksdank.cloud)/frank-dragoon:medium'
|
||||
# The door is the OpenWebUI vhost `ai.franksdank.cloud` — Caddy → OWU on
|
||||
# Dankclaw:3005, Anubis-exempt because OWU enforces its own auth, public A
|
||||
# record present (LAN clients resolve 192.168.0.143 via Pi-hole, measured
|
||||
# 2026-10-21). It exposes an OpenAI-compatible surface at /api/v1 and
|
||||
# proxies to the SAME engines as the LAN rows — no second inference stack,
|
||||
# so every engine-side law below still holds through it.
|
||||
# CREDENTIAL LAW: the key is not in this file. `apiKey` accepts a
|
||||
# command-backed value (`!cmd`, resolved by src/config/resolve-config-value.ts:
|
||||
# stdout trimmed, cached for the process lifetime; anything else is tried as
|
||||
# an env-var name, then taken literally), so the secret lives in
|
||||
# ~/.config/secrets/openwebui_api_key, mode 600, like every other credential
|
||||
# on these boxes. A literal `sk-…` here would trip the pre-commit shape gate
|
||||
# and then live in every agent context on every box that pulls this repo.
|
||||
# Rotate: rewrite the secret file, then RESTART omp — the `!cmd` result is
|
||||
# cached per process, so a live session keeps serving the old key.
|
||||
# WHAT THE DOOR SHOWS (probed 2026-10-21 with this key): /api/v1/models
|
||||
# answers frank-dragoon and frank-zealot only. frank-archon is NOT published
|
||||
# in OpenWebUI's picker and the API answers 400 "Model not found" for it, so
|
||||
# the fleet's biggest endpoint has no WAN path until it is published
|
||||
# server-side on Dankclaw — a fleet change, not a harness change.
|
||||
# CONTRACT PROBED THROUGH THE DOOR (2026-10-21): streaming SSE carries
|
||||
# `reasoning_content` deltas and `tool_calls` deltas, finish_reason
|
||||
# `tool_calls`, and a final `usage` under stream_options.include_usage;
|
||||
# `image_url` content parts reach both vision towers (a 64 px red PNG comes
|
||||
# back "Red" from each); and `chat_template_kwargs` IS forwarded — proven on
|
||||
# frank-zealot, where enable_thinking false gives reasoning 0 chars and true
|
||||
# gives a real thinking block on the same prompt. Every compat block below is
|
||||
# therefore a copy of its LAN twin, never a simplification: dropping the
|
||||
# kwargs here would quietly change thinking behaviour on the WAN path.
|
||||
# Chat completions made with an API key create NO OpenWebUI chat row (the
|
||||
# chat list is unchanged after probing), so an off-network session leaves no
|
||||
# trail in the family's chat history.
|
||||
# NOT BEHIND THIS DOOR: speech. OWU does route /api/v1/audio/speech and it
|
||||
# does reach frank-tts:7772, but OWU substitutes its own voice enum first —
|
||||
# a cloned pack name comes back 400 from the upstream. `modelRoles.speech`
|
||||
# stays on `frank-tts (dankclaw)`; do not repoint it here.
|
||||
"openwebui (franksdank.cloud)":
|
||||
baseUrl: https://ai.franksdank.cloud/api/v1
|
||||
api: openai-completions
|
||||
# Quoted because an unquoted leading `!` is a YAML tag, not a command.
|
||||
apiKey: "!cat ~/.config/secrets/openwebui_api_key"
|
||||
models:
|
||||
- id: frank-dragoon
|
||||
# Same halogen flash-server as `llamacpp (dankclaw)` above, one hop
|
||||
# further out; the picker row and the engine share this id, so there
|
||||
# is no alias to keep in sync.
|
||||
name: "frank-dragoon (WAN)"
|
||||
# Off-network the compaction hub must be reachable too: the LAN
|
||||
# frank-zealot row is a dead address from anywhere but the house.
|
||||
# Same engine, same slot arithmetic, so the keepRecent/threshold
|
||||
# math in the LAN rows transfers untouched.
|
||||
compactionModel: openwebui (franksdank.cloud)/frank-zealot
|
||||
# Inherited from the engine, not invented: 200,000 under the native
|
||||
# 262,144 ceiling (the 400-zone law — never sit at the ceiling). The
|
||||
# proxy neither widens the window nor truncates: an over-ceiling
|
||||
# request is still a 400.
|
||||
contextWindow: 200000
|
||||
# Same clamp arithmetic as the LAN row (see frank_dragoon_unit.bats);
|
||||
# HALOGEN_MAX_TOKENS_CAP on the engine is unchanged.
|
||||
maxTokens: 45056
|
||||
reasoning: true
|
||||
input: [text, image]
|
||||
compat:
|
||||
# Verified through the door: reasoning arrives as
|
||||
# `reasoning_content` in the deltas, not folded into content.
|
||||
reasoningContentField: "reasoning_content"
|
||||
# `:off` must be a real switch, and it is: this engine honours
|
||||
# reasoning_effort "none"; the harness default would only send the
|
||||
# lowest effort, which still thinks.
|
||||
reasoningDisableMode: none-effort
|
||||
# Identity map, same reason as the LAN row — flash-server lists
|
||||
# minimal|low|medium|high|xhigh|max|none and 400s anything else.
|
||||
reasoningEffortMap:
|
||||
minimal: minimal
|
||||
low: low
|
||||
medium: medium
|
||||
high: high
|
||||
xhigh: xhigh
|
||||
max: max
|
||||
# Pinned so a server-side HALOGEN_TOP_K edit cannot change this
|
||||
# session's recipe; the engine accepts all three keys.
|
||||
extraBody:
|
||||
temperature: 1.0
|
||||
top_p: 0.95
|
||||
top_k: 20
|
||||
- id: frank-zealot
|
||||
# The 3090 endpoint, same llama.cpp engine behind the proxy. Stays
|
||||
# the widest slot in the file, so it is the WAN compaction target.
|
||||
name: "frank-zealot (WAN)"
|
||||
compactionModel: openwebui (franksdank.cloud)/frank-zealot
|
||||
# PER-SLOT window: llama.cpp divides --ctx-size across slots and
|
||||
# answers a longer prompt with a 400, never a truncation. Follows
|
||||
# every reslice on the engine (3 × 200,704 today).
|
||||
contextWindow: 200704
|
||||
# Prompt + thinking budget inside the slot; compaction fires at 85 %
|
||||
# of the window on this row set (see the LAN twin for the full
|
||||
# worst-case arithmetic).
|
||||
maxTokens: 16384
|
||||
reasoning: true
|
||||
input: [text, image]
|
||||
compat:
|
||||
reasoningContentField: "reasoning_content"
|
||||
# LOAD-BEARING, exactly as on the LAN row: the Bonsai-2 template
|
||||
# accepts ONLY xhigh|medium|low and raises on anything else, so an
|
||||
# unmapped harness level is a hard failure on the role's first
|
||||
# request. Probed 2026-10-21 that the mapped value, not the
|
||||
# harness-level, is what reaches the template through this proxy.
|
||||
reasoningEffortMap:
|
||||
minimal: low
|
||||
low: low
|
||||
medium: medium
|
||||
high: xhigh
|
||||
max: xhigh
|
||||
# The payload's own sampling recipe plus the thinking switches.
|
||||
# chat_template_kwargs survives the proxy (verified), so
|
||||
# preserve_thinking keeps prior assistants' thinking blocks in
|
||||
# history for multi-turn tool traffic — same contract as at home.
|
||||
extraBody:
|
||||
temperature: 1.0
|
||||
top_p: 0.95
|
||||
top_k: 20
|
||||
min_p: 0.0
|
||||
presence_penalty: 0.0
|
||||
chat_template_kwargs:
|
||||
enable_thinking: true
|
||||
preserve_thinking: true
|
||||
@@ -24,4 +24,6 @@ input {
|
||||
|
||||
focus-follows-mouse // Automatically focus windows under the mouse pointer
|
||||
workspace-auto-back-and-forth // Enable workspace back & forth switching
|
||||
|
||||
trackpoint
|
||||
}
|
||||
@@ -8,12 +8,12 @@
|
||||
proportion 0.25 // 1/4
|
||||
proportion 0.3333 // 1/3
|
||||
proportion 0.5 // 1/2
|
||||
}
|
||||
}
|
||||
|
||||
preset-window-heights {
|
||||
proportion 0.25 // 1/4
|
||||
proportion 0.5 // 1/2
|
||||
}
|
||||
}
|
||||
|
||||
struts {
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
config_version = 14
|
||||
config_version = 15
|
||||
|
||||
[audio]
|
||||
enable_overdrive = true
|
||||
@@ -99,6 +99,7 @@ widget_order = [
|
||||
show_keyboard_layout = true
|
||||
show_login_button = true
|
||||
show_media = true
|
||||
show_password_reveal = true
|
||||
show_session_buttons = true
|
||||
show_unlock_hint = true
|
||||
show_weather = true
|
||||
@@ -126,6 +127,7 @@ widget_order = [
|
||||
show_keyboard_layout = true
|
||||
show_login_button = true
|
||||
show_media = true
|
||||
show_password_reveal = true
|
||||
show_session_buttons = true
|
||||
show_unlock_hint = true
|
||||
show_weather = true
|
||||
@@ -153,6 +155,7 @@ widget_order = [
|
||||
show_keyboard_layout = true
|
||||
show_login_button = true
|
||||
show_media = true
|
||||
show_password_reveal = true
|
||||
show_session_buttons = true
|
||||
show_unlock_hint = true
|
||||
show_weather = true
|
||||
@@ -180,6 +183,7 @@ widget_order = [
|
||||
show_keyboard_layout = true
|
||||
show_login_button = true
|
||||
show_media = true
|
||||
show_password_reveal = true
|
||||
show_session_buttons = true
|
||||
show_unlock_hint = true
|
||||
show_weather = true
|
||||
@@ -275,22 +279,22 @@ transition = [ "fade" ]
|
||||
interval_seconds = 1200
|
||||
|
||||
[wallpaper.default]
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_g7r3vd.jpg"
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven-mlomkm.png"
|
||||
|
||||
[wallpaper.last]
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_g7r3vd.jpg"
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven-mlomkm.png"
|
||||
|
||||
[wallpaper.monitors.DP-1]
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_g7r3vd.jpg"
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven-mlomkm.png"
|
||||
|
||||
[wallpaper.monitors.DP-2]
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_oxex69.jpg"
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven-gjqmel.png"
|
||||
|
||||
[wallpaper.monitors.HDMI-A-1]
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_v9e2w3.jpg"
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven-gjqmel.png"
|
||||
|
||||
[wallpaper.monitors.eDP-1]
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_g7r3vd.jpg"
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven-mlomkm.png"
|
||||
|
||||
[weather]
|
||||
unit = "imperial"
|
||||
|
||||
@@ -30,7 +30,7 @@ output "Sceptre Tech Inc Sceptre M34 Unknown" {
|
||||
output "HDMI-A-1" {
|
||||
scale 1
|
||||
transform "90"
|
||||
position x=1200 y=0
|
||||
position x=2781 y=0
|
||||
}
|
||||
|
||||
output "Lenovo Group Limited 0x40A9 Unknown" {
|
||||
@@ -43,8 +43,8 @@ output "DP-2" {
|
||||
|
||||
scale 1.0
|
||||
|
||||
transform "normal"
|
||||
position x=1581 y=-600
|
||||
transform "90"
|
||||
position x=1581 y=0
|
||||
}
|
||||
output "eDP-1" {
|
||||
mode "1920x1080@60.001"
|
||||
@@ -52,7 +52,7 @@ output "eDP-1" {
|
||||
scale 1.0
|
||||
|
||||
transform "normal"
|
||||
position x=-339 y=-240
|
||||
position x=3981 y=840
|
||||
}
|
||||
output "DP-1" {
|
||||
mode "3440x1440@180.000"
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
---
|
||||
type: reference
|
||||
title: DeepSeek Platform Provider (deepseek/deepseek-flash)
|
||||
description: The vendor-cloud row added to agent/models.yml on 2026-10-29 — why the provider label is the bare word `deepseek`, where its key lives, and the window/maxTokens/cost values it declares.
|
||||
resource: https://api-docs.deepseek.com/
|
||||
tags:
|
||||
- omp
|
||||
- models-yml
|
||||
- deepseek
|
||||
- providers
|
||||
verified: 2026-10-29
|
||||
timestamp: 2026-10-29
|
||||
---
|
||||
|
||||
# DeepSeek Platform Provider (`deepseek` / `deepseek-flash`)
|
||||
|
||||
`agent/models.yml` gained its first vendor-cloud chat row on 2026-10-29: DeepSeek's
|
||||
own hosted API, added alongside the four fleet endpoints and the OpenWebUI WAN
|
||||
door. Use this page when a `deepseek/…` selector fails, when the key needs
|
||||
rotating, or before adding another upstream-vendor row.
|
||||
|
||||
```bash
|
||||
omp --model 'deepseek/deepseek-flash:off'
|
||||
```
|
||||
|
||||
## Why the label is the bare word `deepseek`
|
||||
|
||||
The house pattern for provider labels is `<api> (<host>)` — `llamacpp (dankclaw)`,
|
||||
`openwebui (franksdank.cloud)`. This row breaks it deliberately: the harness
|
||||
resolves a provider's behaviour set by matching the label (and the `baseUrl`)
|
||||
against the catalog's host table, `pi-catalog/src/hosts.ts`, whose entry reads
|
||||
|
||||
```ts
|
||||
/** DeepSeek's first-party API only — gates direct-API quirks (max_tokens field, thinking extraBody). */
|
||||
deepseekDirect: { providers: ["deepseek"], urlMarkers: ["api.deepseek.com"] },
|
||||
```
|
||||
|
||||
A label of `deepseek (platform)` matches neither the provider list nor the URL
|
||||
marker rule for the *name*, and the row loses the direct-DeepSeek quirks the
|
||||
catalog gates behind that key — the `max_tokens` field name, the thinking
|
||||
`extraBody`, and the thinking-disable mode, which on this provider must be
|
||||
`zai-thinking-disabled` (`thinking: {"type":"disabled"}`), not the
|
||||
`none-effort` the LAN rows need. The `baseUrl` carries the host the label
|
||||
cannot name.
|
||||
|
||||
The same table is why the provider's roster is not one row but four. A `models.yml`
|
||||
entry is an **overlay** on the built-in catalog, keyed by `provider/id`
|
||||
(`#mergeCustomModels` → `mergeByModelKey`), so `omp models deepseek` now prints:
|
||||
|
||||
| Row | Source |
|
||||
|---|---|
|
||||
| `deepseek-flash` | the authored row — 1M context, 262K max-out |
|
||||
| `deepseek-v4-flash` | built-in catalog row, untouched |
|
||||
| `deepseek-v4-flash-vision-exp` | built-in catalog row, untouched |
|
||||
| `deepseek-v4-pro` | built-in catalog row, untouched |
|
||||
|
||||
The other three keep catalog windows and pricing; adding a model to the file does
|
||||
not replace the provider.
|
||||
|
||||
## What the row declares, and from where
|
||||
|
||||
| Field | Value | Authority |
|
||||
|---|---|---|
|
||||
| `baseUrl` | `https://api.deepseek.com/v1` | official docs, OpenAI base_url |
|
||||
| `api` | `openai-completions` | same |
|
||||
| `id` | `deepseek-flash` | official model name — `deepseek-v4-flash` / `deepseek-v4-flash-vision-exp` are retired aliases that route to `DeepSeek-V4.1-Flash-1017` and bill at the Flash price |
|
||||
| `contextWindow` | `1048576` | model card (`context_window 1M`) |
|
||||
| `maxTokens` | `262144` | model card (`max_tokens ≥ 256K`) |
|
||||
| `input` | `[text, image]` | V4.1 is natively image-text-to-text |
|
||||
| `cost` | `0.3 / 1.2 / 0.006 / 0` per 1M (in / out / cache-read / cache-write) | October 2026 rate card, peak tier |
|
||||
| `compat.supportsToolChoice` | `false` | V4 thinking mode rejects the `tool_choice` parameter; tool calls still work and default to `auto` |
|
||||
| `compat.supportsDeveloperRole` | `false` | V4 has no `developer` role |
|
||||
| `compat.reasoningContentField` | `reasoning_content` | same field every fleet engine uses |
|
||||
| `compat.extraBody.thinking` | `{type: enabled}` | the documented request shape |
|
||||
|
||||
Laws worth carrying forward:
|
||||
|
||||
- **`maxTokens` and `contextWindow` are a pair.** The serving contract is
|
||||
`prompt + max_tokens ≤ contextWindow`; the harness clamps
|
||||
`max_tokens = min(maxTokens, contextWindow − est − 4096)`, so a declared pair
|
||||
that exceeds the engine's own contract turns into a 400 rather than a
|
||||
truncation. `1048576 / 262144` is the pair the card itself states, and it is
|
||||
the pair the row declares.
|
||||
- **Cost rates go on the row or nowhere.** `cost` is optional in the schema, and a
|
||||
custom row that omits it reports `$0` in the status line forever. The schema
|
||||
models one rate per direction, so the off-peak tier (`$0.15 / $0.60`) cannot be
|
||||
expressed — peak is the value.
|
||||
- **Sampling.** The card recommends `temperature 1.0 / top_p 1.0`; thinking mode
|
||||
floors `top_p` at 0.95 server-side, and `top_k` is not accepted through the
|
||||
OpenAI surface. The row pins `temperature: 1.0` + `top_p: 1.0` anyway —
|
||||
`extraBody` is merged last and wins, so pinning it survives a server-side
|
||||
sampler edit, exactly as the LAN rows do.
|
||||
- **A row behind a proxy or a vendor façade copies its direct twin's compat
|
||||
block**; this row has no LAN twin, so it copies the harness's own built-in
|
||||
descriptor for the same provider (`pi-catalog/src/provider-models/openai-compat.ts`,
|
||||
`openAiCompletionsDescriptor("deepseek", …)`) instead of inventing one.
|
||||
|
||||
## The key
|
||||
|
||||
`apiKey: "!cat ~/.config/secrets/deepseek_api_key"` — 600-mode file, same
|
||||
convention as `gitea`, `kagi_api_key`, `hf_token`, and `openwebui_api_key`. The
|
||||
rules are the ones in [WAN Model Access](/core/wan-model-access.md#the-key): a
|
||||
literal `sk-…` in the tracked file trips the pre-commit shape gate, and a quoted
|
||||
`!cmd` value is the only shape that both resolves and stays out of the gate's
|
||||
assignment scan. Rotation is rewrite-then-restart; the resolved value is cached
|
||||
for the process lifetime. Every workstation that pulls this repo needs its own
|
||||
copy — nothing in the repo carries the value.
|
||||
|
||||
## Verification performed 2026-10-29
|
||||
|
||||
- `omp models --no-extensions` from a scratch `HOME` containing the edited
|
||||
`models.yml` + `config.yml`: zero `validation failed` lines, all four fleet
|
||||
providers still listed, `deepseek (4)` present with `deepseek-flash 1M/262K`.
|
||||
- Live call through the scratch `HOME` — the only proof that auth came from the
|
||||
new `apiKey` line and not from `~/.omp/agent/auth.json`:
|
||||
`omp --model 'deepseek/deepseek-flash:off' … -p 'Reply with exactly the word OK'`
|
||||
→ `OK` in 2.6 s. The same scratch tree with the `baseUrl` port poisoned to `:9`
|
||||
hangs instead of answering, which is what rules out a cached or built-in answer.
|
||||
|
||||
## See also
|
||||
|
||||
- [WAN Model Access](/core/wan-model-access.md) — the credential law and the
|
||||
off-network route to Frank's own engines; `deepseek` is reachable from anywhere
|
||||
but is not that door.
|
||||
- [Core config](/core/index.md) — the rest of `agent/config.yml` / `models.yml`.
|
||||
- [OMP Subagent Limits](/core/subagent-limits.md) — `providers.maxInFlightRequests`
|
||||
is set per provider id; the `deepseek` row currently has no cap.
|
||||
@@ -15,5 +15,8 @@ These files define the structural conventions that govern the agent system itsel
|
||||
* [dankvoice](dankvoice.md) — Voice output is the `dank/dankvoice` package (`pi install git:`); this page keeps fleet wiring and the cutover procedure.
|
||||
* [OMP Status Line Customization](statusline.md) — How presets, composer shapes, and segment arrays actually interact; the fleet's tuned bar. (moved from /pi/, 2026-10-19)
|
||||
* [OMP Subagent Limits & Fan-out Policy](subagent-limits.md) — Live caps: concurrency 2, no nested spawns (the `maxRecursionDepth: 0` trap), per-endpoint in-flight ceilings, isolation policy. (moved from /pi/, 2026-10-19)
|
||||
* [WAN Model Access](wan-model-access.md) — Driving the fleet's models from outside the home network through the OpenWebUI door: the command, the key file and its rotation law, what the door publishes, and the off-network failure modes.
|
||||
* [DeepSeek Platform Provider](deepseek-provider.md) — The first vendor-cloud row in `agent/models.yml`: why its label is the bare word `deepseek`, the key file, and the window/maxTokens/cost values it declares.
|
||||
* [OMP Snapcompact — Bitmap-Frame Compaction](snapcompact.md) — What shape frank-archon actually reads, the measured recall/billing table, the `imageCountBudget` knob and the pricing that isn't, and the 85 % trigger law.
|
||||
|
||||
**Change log:** the bundle keeps exactly one, one level up — [Bundle Change Log](../log.md). A directory here never has its own.
|
||||
@@ -0,0 +1,113 @@
|
||||
---
|
||||
type: concept
|
||||
title: OMP Snapcompact — Bitmap-Frame Compaction
|
||||
description: How snapcompact archives session history as bitmap frames, what shape frank-archon resolves to, the measured recall/billing table behind the fleet's settings, and the models.yml knobs that actually exist.
|
||||
resource: https://github.com/can1357/oh-my-pi/tree/main/packages/snapcompact
|
||||
tags:
|
||||
- omp
|
||||
- compaction
|
||||
- snapcompact
|
||||
- frank-archon
|
||||
verified: 2026-10-27
|
||||
timestamp: 2026-10-27
|
||||
---
|
||||
|
||||
# OMP Snapcompact — Bitmap-Frame Compaction
|
||||
|
||||
Snapcompact is omp's automatic-maintenance method that replaces the LLM
|
||||
summarizer call with a local, deterministic archival pass: discarded history is
|
||||
serialized, whitespace-collapsed, and printed onto dense PNG frames of
|
||||
pixel-font glyphs that a vision-capable model reads back directly. No model,
|
||||
key, or network is involved, so it also serves overflow recovery. It runs only
|
||||
when the current model's row declares `input` including `image`; otherwise
|
||||
maintenance advances to the next method in `compaction.methodOrder`.
|
||||
|
||||
Fleet position (`agent/config.yml`): `methodOrder: [shake, snapcompact, soft,
|
||||
handoff, remote]` — deterministic, lossless-first. Shake elides heavy blocks to
|
||||
`artifact://` references before anything is archived; snapcompact then archives
|
||||
what shake couldn't reclaim; the lossy LLM lanes (`soft`/`handoff`/`remote` via
|
||||
frank-zealot) should never be reached while a vision endpoint is active.
|
||||
|
||||
## Shape resolution for the fleet
|
||||
|
||||
The frame shape (font grid + pitch + frame width) resolves from the **model
|
||||
id** against a measured table, falling back to the **wire API family** when the
|
||||
id is unmeasured:
|
||||
|
||||
| Matcher | Shape | Frame |
|
||||
|---|---|---|
|
||||
| claude (fable/mythos) | `11on16-bw` | 1932px |
|
||||
| claude | `11on16-bw` | 1568px |
|
||||
| gemini | `8on22-bw` | 2048px |
|
||||
| gpt / codex | `8on22-bw` | 1568px |
|
||||
| kimi | `8on22-bw` | 1568px |
|
||||
| glm | `8on16-bw` | 1568px |
|
||||
| openai-completions wire (unmeasured id) | `8on22-bw` | 1568px |
|
||||
|
||||
There is **no qwen matcher**: `frank-archon` (radiance, Qwen3.8 vision tower,
|
||||
`openai-completions` wire) resolves via the wire fallback to `8on22-bw`
|
||||
@1568px — which is 56 clean 28px token-rows for a Qwen-style ViT. `snapcompact.shape`
|
||||
stays `auto`; forcing a variant would bypass the font-aware CJK escape hatch
|
||||
(`silver16-bw`).
|
||||
|
||||
## Measured recall and billing (frank-archon, 2026-10-27)
|
||||
|
||||
A 212,916-char tool-traffic transcript with 8 planted unique facts was sent to
|
||||
`frank-archon` four ways (probe: `@oh-my-pi/snapcompact` renderer, production
|
||||
request shape, `omp` 18.12.5-era bundle):
|
||||
|
||||
| Leg | Frames | Billed prompt tokens | Recall |
|
||||
|---|---|---|---|
|
||||
| Plain text baseline | — | 63,066 | 8/8 |
|
||||
| auto = `8on22-bw` | 14 | 33,533 | **8/8** |
|
||||
| forced `8on16-bw` | 11 | 24,364 | 6/8 (dropped characters) |
|
||||
| forced `11on16-bw` | 14 | 33,533 | 6/8 (dropped characters) |
|
||||
|
||||
Laws this established:
|
||||
|
||||
- **The default shape is the accurate one for the fleet.** The Qwen3.8 FP8
|
||||
tower OCRs `8on22-bw` pixel fonts with full recall; both denser grids drop
|
||||
single characters inside identifiers — matching upstream's toolbench finding
|
||||
(spacing-tuned cells won everywhere; dense prose-era shapes erase line
|
||||
structure).
|
||||
- **Bitmap archive ≈ 47 % of the billed tokens of raw text at equal recall.**
|
||||
- **True radiance billing is ~2,400 tokens/frame; the harness charges a blind
|
||||
5,024 floor.** Unpriced custom endpoints get `FRAME_TOKEN_ESTIMATE` (5,024)
|
||||
per frame in archive sizing, so archives plan roughly half the frames the
|
||||
window actually fits. This is conservative (never overflows) and costs
|
||||
recall-per-compaction.
|
||||
|
||||
## The knobs that exist (and the one that doesn't)
|
||||
|
||||
- `imageCountBudget: 16` (top-level models.yml row field, set on
|
||||
`frank-archon`): the catalog never matches custom providers, so the
|
||||
unknown-provider floor of **5 images per request** silently caps archives.
|
||||
Radiance accepted 14 frames in one probe request; 16 leaves margin. Real
|
||||
frame count stays bounded by the sizing law below.
|
||||
- `imageTokenization` is **catalog-only** (compiled KDL compat rules; no
|
||||
user-row key reaches the pricing path). A custom row cannot mark frames
|
||||
verified or price them — until upstream prices `openai-completions` vision,
|
||||
custom-endpoint archive capacity is count-bound.
|
||||
- Archive sizing law: frames get **half the room left under the compaction
|
||||
threshold** after system prompt, tools, and kept recent turns; the compacted
|
||||
result targets ≤ 60 % of threshold, and post-compaction headroom must reach
|
||||
the 80 % recovery band.
|
||||
- `snapcompact.systemPrompt: none` and `snapcompact.toolResults: false` stay
|
||||
off fleet-wide: imaging inline text is a token-cost lever that routes
|
||||
always-loaded instructions through the OCR path — accuracy-negative.
|
||||
|
||||
## The 85 % trigger law
|
||||
|
||||
`compaction.thresholdPercent: 85` on frank-archon is load-bearing for the
|
||||
max_tokens clamp documented in `agent/models.yml`:
|
||||
`170,000 est × 1.03 + 16,384 ≤ 200,000`. At 95 % the clamp squeezes output to
|
||||
~6 k near the trigger and the worst-case request overshoots the window, so
|
||||
radiance **refuses** and compaction reroutes through the overflow-recovery
|
||||
emergency path instead of the planned threshold pass.
|
||||
|
||||
## Follow-ups
|
||||
|
||||
- `frank-dragoon` / `frank-zealot` rows carry the same unpriced vision towers;
|
||||
the count budget and probe apply to them but are unmeasured there.
|
||||
- Re-run the probe after the next upstream snapcompact/omp release (renderer
|
||||
and shape table move under us).
|
||||
@@ -0,0 +1,139 @@
|
||||
---
|
||||
type: reference
|
||||
title: WAN Model Access (ai.franksdank.cloud)
|
||||
description: How to drive the fleet's models from outside the home network — the OpenWebUI door in agent/models.yml, the key file and its rotation, what the door does and does not publish, and the off-network failure modes.
|
||||
tags:
|
||||
- omp
|
||||
- models-yml
|
||||
- openwebui
|
||||
- remote-access
|
||||
- fleet
|
||||
verified: 2026-10-21
|
||||
timestamp: 2026-10-21
|
||||
---
|
||||
|
||||
# WAN Model Access (ai.franksdank.cloud)
|
||||
|
||||
Every provider in `agent/models.yml` except one is a LAN address, so a laptop
|
||||
that leaves the house loses the fleet. The exception is the
|
||||
`openwebui (franksdank.cloud)` block: the fleet's already-public OpenWebUI vhost
|
||||
speaks OpenAI-compatible HTTP at `/api/v1` and proxies to the same engines the
|
||||
LAN rows point at. One hop further out, no second inference stack.
|
||||
|
||||
Read this page when you are off-network and a model call fails, when the key
|
||||
needs rotating, or before adding another row behind the door.
|
||||
|
||||
## Run a session away from home
|
||||
|
||||
The door is added as a provider, never as a role default. `modelRoles` in
|
||||
`agent/config.yml` still names LAN providers, so away from home you must name
|
||||
the model explicitly:
|
||||
|
||||
```bash
|
||||
omp --model 'openwebui (franksdank.cloud)/frank-dragoon:medium'
|
||||
omp --model 'openwebui (franksdank.cloud)/frank-zealot:off'
|
||||
```
|
||||
|
||||
The same two ids work in the `/model` picker inside a running session — the
|
||||
provider label is what disambiguates them from the LAN rows, since the wire ids
|
||||
are identical by design (OpenWebUI's picker row and the engine share the id, so
|
||||
there is no alias to keep in sync).
|
||||
|
||||
## The key
|
||||
|
||||
`apiKey: "!cat ~/.config/secrets/openwebui_api_key"` — a **command-backed**
|
||||
value, resolved by `src/config/resolve-config-value.ts` in the harness: stdout
|
||||
is trimmed, an env-var name is tried next, and only then is the string taken
|
||||
literally. The secret itself is a 600-mode file in `~/.config/secrets/`, the
|
||||
same convention as `gitea` and `kagi_api_key`.
|
||||
|
||||
Laws:
|
||||
|
||||
- **Never inline the `sk-…`.** It is a tracked file; the pre-commit shape gate
|
||||
blocks `sk-…` anywhere in the staged diff, and a key that slipped through
|
||||
would live in every agent's context on every box that pulls this repo.
|
||||
- **Rotation is: rewrite the secret file, then restart omp.** The `!cmd` result
|
||||
is cached for the process lifetime, so a long-lived session keeps serving the
|
||||
old key — a stale-session failure reads exactly like a revoked key.
|
||||
- The key is an **OpenWebUI** key (User menu → Settings → Account → API keys),
|
||||
not a fleet endpoint key. It carries that user's model ACL, so what the door
|
||||
shows is whatever the picker shows that user.
|
||||
- Every other workstation needs its own copy of the file; nothing in the repo
|
||||
carries the value.
|
||||
|
||||
## What the door publishes
|
||||
|
||||
Probed 2026-10-21 with the fleet key.
|
||||
|
||||
|Behind the door|Notes|
|
||||
|---|---|
|
||||
|`frank-dragoon`|halogen flash-server, Dankclaw iGPU — same row as the LAN `llamacpp (dankclaw)`|
|
||||
|`frank-zealot`|llama.cpp on Dankclaw's 3090 — same row as `llamacpp (dankclaw-3090)`; also the WAN compaction target|
|
||||
|`frank-archon`|**not published.** `/api/v1/models` omits it and the API answers `400 "Model not found"`. Dankserver's endpoint has no public face at all. Giving it a WAN path is a fleet change on Dankclaw (publish it in the OpenWebUI picker), not a harness change|
|
||||
|`frank-tts`|route exists, contract does not. OpenWebUI forwards `/api/v1/audio/speech` to `frank-tts:7772` but substitutes its own voice enum first, and a cloned-pack name comes back `400` from the upstream. `modelRoles.speech` stays on the LAN `frank-tts (dankclaw)` row|
|
||||
|
||||
## Contract verified through the proxy
|
||||
|
||||
Each of these was measured end-to-end on 2026-10-21 — they are why the compat
|
||||
blocks in the WAN rows are copies of their LAN twins rather than simplifications:
|
||||
|
||||
- Streaming SSE carries `reasoning_content` **and** `tool_calls` deltas,
|
||||
`finish_reason: tool_calls`, and a final `usage` under
|
||||
`stream_options.include_usage`.
|
||||
- `image_url` content parts reach both vision towers.
|
||||
- `chat_template_kwargs` is forwarded — proven on `frank-zealot`, where
|
||||
`enable_thinking: false` yields zero reasoning and `true` yields a real
|
||||
thinking block on the same prompt. Drop the kwargs from a WAN row and you
|
||||
silently change thinking behaviour on the WAN path.
|
||||
- API-key completions create **no** OpenWebUI chat row: the chat list is
|
||||
unchanged after a probe, so an off-network session leaves nothing in the
|
||||
family's chat history.
|
||||
|
||||
Context windows, `maxTokens`, sampling and effort maps are inherited from the
|
||||
engines and documented on the LAN rows and in the fleet vault
|
||||
(`serverAppConfigs-docs/LLM/`). The proxy neither widens a window nor truncates:
|
||||
an over-ceiling request is still a `400`.
|
||||
|
||||
## Off-network failure modes
|
||||
|
||||
- **`Connection refused` / long timeout on a LAN id.** Not a service outage —
|
||||
you are not home. Switch to the WAN id.
|
||||
- **`Model not found` for `frank-archon`.** Expected; see the table above.
|
||||
- **Compaction wants a LAN address.** Each WAN row sets `compactionModel` to
|
||||
the WAN `frank-zealot`, but `compaction.remoteEndpoint` in `agent/config.yml`
|
||||
is a LAN URL. It is last in `methodOrder`, so the other methods run first and
|
||||
the remote attempt just fails — leave it unless off-network compaction turns
|
||||
out to need it.
|
||||
- **Roles still resolve to LAN.** A full off-network session that must not
|
||||
depend on hand-picked models needs `retry.fallbackChains` entries pointing
|
||||
the LAN defaults at the WAN rows; that is a deliberate `config.yml` change,
|
||||
not part of adding rows here.
|
||||
|
||||
## Verify the door
|
||||
|
||||
```bash
|
||||
# Key + model list (LAN clients resolve 192.168.0.143 via Pi-hole; the WAN
|
||||
# A record is what makes this reachable from elsewhere).
|
||||
curl -s https://ai.franksdank.cloud/api/v1/models \
|
||||
-H "Authorization: Bearer $(cat ~/.config/secrets/openwebui_api_key)" | jq -r '.data[].id'
|
||||
|
||||
# Registry loaded, WAN rows present, no validation failure (a failed load
|
||||
# silently disables EVERY custom provider — see
|
||||
# agent/models.yml's schema-law comment):
|
||||
omp models --no-extensions | grep -E 'franksdank|validation failed'
|
||||
```
|
||||
|
||||
A scratch-HOME run is the only real proof that a harness change reaches the
|
||||
door: copy `models.yml` + `config.yml` to `$TMP/.omp/agent/`, symlink
|
||||
`$TMP/.config/secrets` at the real one (a `~` in a `!cmd` value expands to
|
||||
`$HOME`), rewrite the LAN baseUrls to a dead port so a success cannot be
|
||||
faked, then `HOME=$TMP omp --model 'openwebui (franksdank.cloud)/…' -p …`.
|
||||
Delete the scratch tree afterwards — it holds endpoint URLs.
|
||||
|
||||
## Pointers
|
||||
|
||||
- Door topology: [Caddy](/caddy/index.md), [System hosts](/system/index.md).
|
||||
- Model registry law and per-provider limits: [Core config](index.md),
|
||||
[OMP Subagent Limits](subagent-limits.md).
|
||||
- Server-side record: `serverAppConfigs-docs/Dankclaw/OpenWebUI.md`
|
||||
(that repo's vault, not this bundle).
|
||||
+56
-1
@@ -22,8 +22,59 @@ not restated here — entry, then pointer. That is what the 2026-10-21 audit pas
|
||||
newest twelve entries: the facts stayed, the duplicates went, `git log --follow` keeps the
|
||||
long form.
|
||||
|
||||
## 2026-10-29
|
||||
|
||||
### A vendor-cloud provider must be named the way the harness names it
|
||||
|
||||
`deepseek` / `deepseek-flash` joins `agent/models.yml` — Frank's DeepSeek platform
|
||||
key in `~/.config/secrets/deepseek_api_key` (600, `!cat`-backed), a 1M/262K row,
|
||||
the documented compat block. The label breaks the house `<api> (<host>)` pattern on
|
||||
purpose: the catalog's host table (`pi-catalog/src/hosts.ts`) keys the
|
||||
direct-DeepSeek behaviour set off the literal name `deepseek` (or an
|
||||
`api.deepseek.com` URL), so a prettier label silently loses the quirks that make
|
||||
the endpoint answer — the `max_tokens` field, the thinking `extraBody`, and the
|
||||
thinking-disable mode this API takes. Law: **a provider label is a lookup key, not
|
||||
only a display name**; check the table before inventing one, and prove the row with
|
||||
a scratch-HOME call (a poisoned-port control is what separates a real auth success
|
||||
from a cached one). Page: [core/deepseek-provider.md](core/deepseek-provider.md).
|
||||
|
||||
|
||||
## 2026-10-27
|
||||
|
||||
### Snapcompact audited against frank-archon; the blind frame price is the real limit
|
||||
|
||||
A live recall probe (212,916-char archive, 8 planted facts, four legs) against
|
||||
frank-archon: the auto shape `8on22-bw` reads back at full recall for 47 % of
|
||||
the billed tokens of raw text, while both denser grids drop characters inside
|
||||
identifiers — `snapcompact.shape: auto` stays. `thresholdPercent` returns 95 →
|
||||
85 (the max_tokens gap math in `models.yml` was written for 85; 95 routes
|
||||
compaction through the overflow-refuse path). `imageCountBudget: 16` lands on
|
||||
the frank-archon row: custom providers inherit the unknown-provider floor of 5
|
||||
images/request and silently halve the archive. Law: **frame pricing is
|
||||
catalog-only** — radiance charges ~2,400 tokens/frame and the harness assumes
|
||||
5,024, and no user-row key can fix that until upstream prices
|
||||
`openai-completions` vision. Page: [core/snapcompact.md](core/snapcompact.md).
|
||||
|
||||
## 2026-10-21
|
||||
|
||||
### The WAN door into the model registry
|
||||
|
||||
`openwebui (franksdank.cloud)` joins `agent/models.yml` — the already-public
|
||||
OpenWebUI vhost as an OpenAI-compatible provider, so a workstation outside the
|
||||
house still reaches `frank-dragoon` and `frank-zealot` while every LAN row stays
|
||||
untouched. The credential is command-backed
|
||||
(`apiKey: "!cat ~/.config/secrets/openwebui_api_key"`, 600-mode file): a literal
|
||||
`sk-…` in a tracked file is a key in every agent context on every box that pulls.
|
||||
|
||||
Laws: **OpenWebUI forwards `chat_template_kwargs`**, so a proxied row copies its
|
||||
LAN twin's compat block instead of simplifying it — proved on `frank-zealot`,
|
||||
`enable_thinking` false giving zero reasoning and true a real block on one
|
||||
prompt; **the picker IS the API's model list**, `frank-archon` answers `400
|
||||
"Model not found"` because it was never published, so a WAN path for it is a
|
||||
fleet change on Dankclaw and not a harness one; and **a `!cmd` value is cached
|
||||
for the process lifetime**, so rotating a key without restarting omp leaves the
|
||||
old key working. Page: [core/wan-model-access.md](core/wan-model-access.md).
|
||||
|
||||
### the audit grows an advisory table, and corrects three of its own numbers
|
||||
|
||||
`omp-docs/reports/fleet-security-audit-2026-10-21.html` folds in the two per-host vulnerability
|
||||
@@ -39,7 +90,11 @@ the OpenSSH advisory count (0 → **5** unpatched CVEs; the first query omitted
|
||||
|
||||
Laws: **an empty OSV response is not a clean bill** — verify the query shape against the distro
|
||||
tracker before writing "0 advisories"; and **a version quoted from a subagent gets re-probed
|
||||
before it enters a report**, this being the second mis-attribution the audit caught.
|
||||
before it enters a report**, this being the second mis-attribution the audit caught. Merged as PR
|
||||
[dank/command-center#66](https://gitea.franksdank.cloud/dank/command-center/pulls/66); the remediation
|
||||
clusters are filed as issues
|
||||
[dank/command-center#67](https://gitea.franksdank.cloud/dank/command-center/issues/67)–#77, listed in
|
||||
[reports/index.md](reports/index.md).
|
||||
|
||||
### reviewing a day of work, and finding checks that lie
|
||||
|
||||
|
||||
@@ -10,3 +10,16 @@ pages are the working data they were built from. The bundle-wide work log stays
|
||||
| [vuln-dankclaw.md](vuln-dankclaw.md) | reference | CVE/GHSA pass for the 33 Dankclaw quadlet images and the Ubuntu 26.04 host — the evidence base the audit's Dankclaw rows rest on. |
|
||||
| [vuln-dankserver.md](vuln-dankserver.md) | reference | CVE/GHSA pass for Dankserver (containers + host), blankpad's Arch package set, and danklivingroom's OpenSSH. |
|
||||
| [sleep-wake-investigation-report.html](sleep-wake-investigation-report.html) | report | blankpad suspend/resume investigation, 2026-09-28. |
|
||||
|
||||
Open remediation work is tracked as Gitea issues, one per host-and-package cluster:
|
||||
[#67](https://gitea.franksdank.cloud/dank/command-center/issues/67) Ubuntu OpenSSH on both servers ·
|
||||
[#68](https://gitea.franksdank.cloud/dank/command-center/issues/68) gnome-remote-desktop + rpcbind ·
|
||||
[#69](https://gitea.franksdank.cloud/dank/command-center/issues/69) SurrealDB ·
|
||||
[#70](https://gitea.franksdank.cloud/dank/command-center/issues/70) Alpine image bases ·
|
||||
[#71](https://gitea.franksdank.cloud/dank/command-center/issues/71) Pi-hole ·
|
||||
[#72](https://gitea.franksdank.cloud/dank/command-center/issues/72) Valkey ·
|
||||
[#73](https://gitea.franksdank.cloud/dank/command-center/issues/73) copyparty ·
|
||||
[#74](https://gitea.franksdank.cloud/dank/command-center/issues/74) redis ·
|
||||
[#75](https://gitea.franksdank.cloud/dank/command-center/issues/75) radiance-vllm ·
|
||||
[#76](https://gitea.franksdank.cloud/dank/command-center/issues/76) danklivingroom SSH ·
|
||||
[#77](https://gitea.franksdank.cloud/dank/command-center/issues/77) blankpad unfixed AVGs.
|
||||
Reference in new issue
Block a user