Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96553979fb | ||
|
|
8f5a229cc6 | ||
|
|
ea514c8fd2 | ||
|
|
bc60fa33be | ||
|
|
e4ecc93f29 | ||
|
|
d055eb1e7f | ||
|
|
70346f3d57 | ||
|
|
1251cfbd40 | ||
|
|
e67bbd0de0 | ||
|
|
cd20d6ddfd | ||
|
|
d99c1c3c4f |
No files matched your search
@@ -576,7 +576,18 @@ export function resolveRewriter(env: NodeJS.ProcessEnv, modelsYml: string, confi
|
||||
return resolveTinyRewriter(modelsYml, configYml);
|
||||
}
|
||||
|
||||
/** The enhancer's rewrite step; never throws — null is its "couldn't". */
|
||||
/**
|
||||
* The enhancer's rewrite step; never throws — null is its "couldn't".
|
||||
*
|
||||
* The `chat_template_kwargs.enable_thinking: false` field is load-bearing.
|
||||
* The @tiny fleet model (frank-zealot, a Qwen35 hybrid) defaults to a
|
||||
* reasoning pass that fills `reasoning_content` and leaves `content` empty.
|
||||
* The harness injects this flag for the built-in vocalizer; a raw POST does
|
||||
* not get it, so without it the rewrite spends its whole 6 s budget thinking
|
||||
* (blowing the timeout → mechanical fallback) and, when it answers in time,
|
||||
* returns an empty `content` that would mute the utterance. Measured: 6.9 s /
|
||||
* empty content without the flag, 0.55 s / clean prose with it.
|
||||
*/
|
||||
export async function rewriteForSpeech(target: RewriteTarget, text: string): Promise<string | null> {
|
||||
try {
|
||||
const res = await fetch(target.chatUrl, {
|
||||
@@ -589,6 +600,7 @@ export async function rewriteForSpeech(target: RewriteTarget, text: string): Pro
|
||||
{ role: "user", content: text.slice(0, REWRITE_MAX_INPUT) },
|
||||
],
|
||||
max_tokens: REWRITE_MAX_TOKENS,
|
||||
chat_template_kwargs: { enable_thinking: false },
|
||||
}),
|
||||
signal: AbortSignal.timeout(REWRITE_TIMEOUT_MS),
|
||||
});
|
||||
@@ -600,20 +612,33 @@ export async function rewriteForSpeech(target: RewriteTarget, text: string): Pro
|
||||
const message: unknown = first.message;
|
||||
if (!message || typeof message !== "object" || !("content" in message)) return null;
|
||||
const content: unknown = message.content;
|
||||
if (typeof content === "string") return content.trim();
|
||||
if (!Array.isArray(content)) return null;
|
||||
const parts: string[] = [];
|
||||
for (const part of content) {
|
||||
if (part && typeof part === "object" && "type" in part && part.type === "text" && "text" in part && typeof part.text === "string") {
|
||||
parts.push(part.text);
|
||||
}
|
||||
const spoken = typeof content === "string" ? content.trim() : Array.isArray(content) ? textParts(content) : null;
|
||||
if (spoken === null) return null;
|
||||
if (spoken === "") {
|
||||
// A reasoning-only reply means the endpoint ignored the thinking-off
|
||||
// flag: the prose landed in reasoning_content, not content. That is a
|
||||
// failed rewrite (fall back to mechanical text), NOT the enhancer's
|
||||
// legitimate "" ("nothing worth saying") — which has no reasoning.
|
||||
const reasoning = "reasoning_content" in message ? message.reasoning_content : undefined;
|
||||
if (typeof reasoning === "string" && reasoning.trim().length > 0) return null;
|
||||
}
|
||||
return parts.join(" ").trim();
|
||||
return spoken;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Concatenated text of a content-part array; undefined shape handled by caller. */
|
||||
function textParts(content: readonly unknown[]): string {
|
||||
const parts: string[] = [];
|
||||
for (const part of content) {
|
||||
if (part && typeof part === "object" && "type" in part && part.type === "text" && "text" in part && typeof part.text === "string") {
|
||||
parts.push(part.text);
|
||||
}
|
||||
}
|
||||
return parts.join(" ").trim();
|
||||
}
|
||||
|
||||
/** The text the assistant last actually said; tool-only messages do not count. */
|
||||
export function lastAssistantText(messages: readonly unknown[]): string {
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
@@ -1043,7 +1068,14 @@ export default function dankvoice(pi: ExtensionAPI) {
|
||||
persistSettings(ctx);
|
||||
return;
|
||||
}
|
||||
await speak(ctx, `dankvoice: ${voiceLabel()} through ${cfg.player?.command ?? "no player"}, checking.`);
|
||||
// Fire-and-forget on purpose: awaiting the playback chain here blocks
|
||||
// the menu handler for the whole phrase and the harness reports the
|
||||
// command as timed out. The point of the test is to hear it, not to
|
||||
// block the menu on it.
|
||||
ctx.ui.notify("dankvoice: speaking the test phrase", "info");
|
||||
void speak(ctx, `dankvoice: ${voiceLabel()} through ${cfg.player?.command ?? "no player"}, checking.`).catch((error) =>
|
||||
ctx.ui.notify(`dankvoice: ${error instanceof Error ? error.message : String(error)}`, "error"),
|
||||
);
|
||||
}
|
||||
|
||||
pi.registerCommand("say", {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
name: docs-dedupe-verify-campaign
|
||||
description: "Run an exhaustive docs dedupe + fact-verification campaign in dank/command-center: scout cluster ledgers, evidence-overridden merges, live fleet probes, log compaction law, and landing via Gitea PR."
|
||||
---
|
||||
|
||||
Campaign procedure for deduping + fact-checking `omp-docs/` in dank/command-center (proven on Refs #29, 2026-10-19: 8 commits, -2,229 lines, 0 lint/coverage/bats failures).
|
||||
|
||||
## 1. Recon
|
||||
- Fresh branch off `origin/main` in a new worktree (`git worktree add ~/.omp/wt/<name> -b docs/<issue>-<slug> origin/main`); the session worktree's `git checkout -b` is policy-blocked.
|
||||
- Run `scripts/kb-lint.sh omp-docs` baseline; write a throwaway index-coverage checker (each dir's `index.md` must link every subdir/file) — mechanical, no scout needed.
|
||||
- Open the Gitea issue first (multi-moving-parts law).
|
||||
|
||||
## 2. Scout fan-out (overlap detection)
|
||||
- One `task` batch, `agent: scout`, one task per doc cluster + one cross-cutting sweep (cap 2 concurrent; rest queue).
|
||||
- Each scout writes a ledger to `local://dedupe/<cluster>.md`: inventory table (file, lines, purpose, verdict KEEP/MERGE/DELETE/UPDATE), overlaps, facts-to-keep ledger, inbound links, staleness, claims-needing-verification.
|
||||
- Scouts are LEADS, not truth. Observed failures: fabricated ledger entries (claims about text that does not exist in any file), a DELETE verdict on a generated artifact that is under test (`tools/niri/keybindings.md` is generated by `scripts/gen-keybinds.sh` and tested by `tests/gen-keybinds.bats` — keep), and a KEEP based on misreading a changelog leftover. Re-derive every consequential verdict from the files/repo yourself.
|
||||
|
||||
## 3. Verify fleet facts live (never trust doc dates)
|
||||
- Read-only BatchMode probes: `ssh -o BatchMode=yes dankclaw '...'` (podman ps/inspect, systemctl status, curl localhost, os-release, cpuinfo). `sudo -n` only has systemctl verbs — use `podman exec <ctr> grep/cat` for in-container files.
|
||||
- Update docs with dated drift banners ("probed 2026-10-19") for policy divergence; NEVER silently re-apply a policy — flag as operator decision (e.g. SearXNG engine policy).
|
||||
|
||||
## 4. Verify external claims via direct web research (no subagents)
|
||||
- Version/protocol/benchmark claims: primary source only (upstream README, raw.githubusercontent, /api/v1/version, arxiv). If a number cannot be re-attributed to a published benchmark, drop it with an explicit note in the doc.
|
||||
- Fabrication hunt: invented distros/projects/model names (found: "FreeToken" distro, ornith-llama-cpp-setup tree, Intel CPU on an AMD box) — delete the artifact, not just the line.
|
||||
|
||||
## 5. Log compaction law
|
||||
- Changelog entry = date + what changed + durable lesson + concept-page pointer, few lines. Full narratives live in git (`git log --follow`). Record the law at the top of `log.md`. Never re-expand an entry in place. Keep pi-era entries tagged "(pi era)".
|
||||
|
||||
## 6. Gates + landing
|
||||
- Gate every commit: `bash scripts/kb-lint.sh omp-docs` + index-coverage + `bats tests/` (67 tests). Pre-commit hook prints `error: command not found: caddy` on Caddyfile-touching commits — warning only, commit still lands.
|
||||
- Gitea API auth: `~/.config/secrets/gitea` is **URL-form** (`http://dank:<token>@host`), not a raw token. Extract: `T=$(sed -n 's|.*://[^:]*:\([^@]*\)@.*|\1|p' ~/.config/secrets/gitea | head -1)`; raw `cat` as bearer token → 401.
|
||||
- PR body ends with `---` + `cwd: <wd> · session: <id>` footer; then `xdg-open <PR-URL>` immediately; comment the campaign ledger summary on the issue. NEVER merge without Frank's explicit OK.
|
||||
|
||||
## Edit-tool field notes (recurring traps)
|
||||
- `PUT N+=:` is invalid syntax — it inserts the literal text. One insert-op per edit call; recompute line numbers per call.
|
||||
- File hash tag changes after every edit — re-`read` before each subsequent edit call to the same file; reusing a stale tag is rejected.
|
||||
@@ -0,0 +1,62 @@
|
||||
---
|
||||
name: fleet-service-runtime-census
|
||||
description: "Answer \"what is service running right now?\" for a dank-fleet service: live endpoint probe plus host units, image ids, live-vs-repo env, and drop-ins."
|
||||
---
|
||||
|
||||
# Fleet service runtime census
|
||||
|
||||
Answer the question "what is `<service>` running currently?" with evidence, not from docs. Docs say what *should* run; the host says what *does*. Run all five probes, then report one table.
|
||||
|
||||
## 1. Probe the published port (works from any LAN box, no ssh)
|
||||
|
||||
```bash
|
||||
curl -s -m 10 http://<host-ip>:<port>/health; echo
|
||||
curl -s -m 10 http://<host-ip>:<port>/v1/audio/voices # per-endpoint extras: /v1/models, /metrics
|
||||
```
|
||||
|
||||
The extra route matters more than `/health` for identity claims:
|
||||
- llama.cpp and radiance **echo the requested model name** — a 200 proves nothing; read `/v1/models`.
|
||||
- halogen 404s `/completion`, `/ping`, `/props`, `/tokenize`.
|
||||
- The frank-tts façade answers `/health` **without dialing the engine**, so "façade healthy, engine down" is a real state; the voice list in that same response is the honest census.
|
||||
|
||||
## 2. Units + containers on the host (ssh as the deploy user, no sudo)
|
||||
|
||||
```bash
|
||||
ssh <host> 'systemctl --user status <svc>.service <engine>.service --no-pager \
|
||||
| grep -E "^(●| Active| Loaded)"; \
|
||||
podman images --format "{{.Repository}}:{{.Tag}} {{.ID}} {{.Created}}"; \
|
||||
podman ps -a --format "{{.Names}}\t{{.Status}}\t{{.Image}}"'
|
||||
```
|
||||
|
||||
- `active (running) since …` + `Up N hours (healthy)` is the answer; `activating (auto-restart)` with a ~50 ms exit is a **preflight refusal**, not an engine fault.
|
||||
- **NEVER prefix these podman calls with `sudo`.** Containers live in the deploy user's rootless storage; `sudo podman inspect frank-tts-engine` returns `no such object` while the plain form succeeds. Treating that as "container missing" is a false finding.
|
||||
|
||||
## 3. Drop-ins — the override that no health gate sees
|
||||
|
||||
```bash
|
||||
ssh <host> 'systemctl --user show <svc>.service -p DropInPaths; \
|
||||
systemctl --user show <engine>.service -p DropInPaths'
|
||||
```
|
||||
|
||||
`DropInPaths=` must be empty. A leftover bench drop-in (`Environment=FRANK_<SVC>_ENV_FILE=/tmp/<arm>.env`) makes the next deploy boot a different model at a different shape while `/health` still answers 200 and the e2e gate passes.
|
||||
|
||||
## 4. Live env vs repo env — the only truth test
|
||||
|
||||
```bash
|
||||
# repo side (local checkout)
|
||||
grep -vE '^\s*#|^\s*$' scripts/<svc>.env
|
||||
# host side
|
||||
ssh <host> 'sudo cat /home/dank/serverAppConfigs/scripts/<svc>.env' | grep -vE '^\s*#|^\s*$'
|
||||
```
|
||||
|
||||
Diff them. Per repo law the env file beside the serve script **is** the authority; a doc page that disagrees with it is the bug, and host-side drift means an uncommitted edit. Also check `git log --oneline -3 -- scripts/<svc>.env` to confirm the host isn't ahead of git.
|
||||
|
||||
## 5. Report
|
||||
|
||||
Table of: unit state + since, image repo:tag@short-ID + age, backend/alias actually served (from step 1, not from the name), drop-ins, and whether live env == repo env. State explicitly when a fact came from docs rather than from a probe.
|
||||
|
||||
## Traps that produce false findings
|
||||
|
||||
- Digest-pinned images: the local image ID's *creation date* answers "is this stale" better than the tag; `latest` + `Pull=never` is a locally built image, not a registry pull.
|
||||
- `ssh` user context changes what podman sees (root vs rootless) — see step 2.
|
||||
- Two ports on one host are two providers in `models.yml`; a port mismatch is a rename or drift, not a config typo.
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: gitea-api-pr-operations
|
||||
description: "Open, list, merge, and diagnose issues/PRs on the Dankclaw Gitea API (192.168.0.143:3000, repo dank/command-center) with the token stored in ~/.config/secrets/gitea — incl. the required merge payload and where review comments actually live on 1.27.3."
|
||||
description: "\"Open, list, merge, and diagnose issues/PRs on the Dankclaw Gitea API (192.168.0.143:3000, repo dank/command-center) with the token stored in ~/.config/secrets/gitea — incl. the required merge payload, where review comments actually live on 1.27.3, and how to recover the session id for the mandatory body footer.\""
|
||||
---
|
||||
|
||||
# Gitea API PR operations (Dankclaw `dank/command-center`)
|
||||
@@ -25,11 +25,13 @@ description: "Open, list, merge, and diagnose issues/PRs on the Dankclaw Gitea A
|
||||
fold footers into the --arg string instead.)
|
||||
- **List open PRs**: `GET /pulls?state=open` → `jq -r '.[] | "#\(.number) \(.head.ref) \(.title)"'`.
|
||||
- **Read PR**: `GET /pulls/<n>` (no token; public repo) → `jq -c '{state,merged,mergeable,head:.head.ref}'`.
|
||||
`mergeable:false` is often Gitea's *not-yet-checked* state, not a conflict — prove a real
|
||||
conflict locally with `git merge-tree --write-tree HEAD origin/main` before believing it.
|
||||
- **MERGE**: `POST /pulls/<n>/merge` with `-H 'Content-Type: application/json'`
|
||||
and body **`{"Do":"merge"}`** — this build REQUIRES the `Do` field:
|
||||
`{}` or `{"DoNotMerge":false,...}` → 422; `{"Do":"merge"}` → 200 +
|
||||
`{"state":"closed","merged":true,"merge_commit_sha":"..."}`. Verify with a
|
||||
plain GET /pulls/<n> afterwards (merge endpoint's own stdout may not parse).
|
||||
`{"state":"closed","merged":true,"merge_commit_sha":"..."}`. Verify with a plain
|
||||
GET /pulls/<n> afterwards (merge endpoint's own stdout may not parse).
|
||||
Merge is the `--no-ff` merge commit on main; then `git -C ~/.omp fetch && git
|
||||
merge --ff-only origin/main` propagates the live deployment.
|
||||
- **Issues**: same shape at `/issues` (`POST {title, body}`), `/issues/<n>/comments`
|
||||
@@ -51,6 +53,13 @@ description: "Open, list, merge, and diagnose issues/PRs on the Dankclaw Gitea A
|
||||
Every issue/PR body ends with `---` line then `cwd: <wd> · session: <id>`. Keep
|
||||
it inside the --arg string (not a `---` CLI arg).
|
||||
|
||||
`cwd` is THIS session's own working directory (note a build worktree separately if they
|
||||
differ). When `/status` is unreachable (headless/subagent context), recover the session id
|
||||
from the session store instead of inventing one:
|
||||
`ls -t ~/.omp/agent/sessions/<cwd-slug>/ | head -2` → `<timestamp>_<uuid>` (the same uuid as
|
||||
`<uuid>.jsonl`); the slug is the cwd with `/` replaced by `-`. Subagent files live under the
|
||||
**parent** session's directory, so the newest entry there is the parent id.
|
||||
|
||||
## Gotchas
|
||||
- Content-Type: POSTs with `-d @file.json` MUST carry
|
||||
`-H 'Content-Type: application/json'` on the command line. `-H` entries put in
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
name: strix-halo-npu-readiness-census
|
||||
description: "Engine-agnostic census of whether an AMD Strix Halo / Ryzen AI host can run anything on its XDNA2 NPU: driver bind, firmware, device perms, IOMMU, distro XRT, missing AIE compiler, fabric clock, RAM headroom."
|
||||
---
|
||||
|
||||
Use when asked "can we run <engine/model> on the Strix Halo / Ryzen AI NPU?" for a runtime that is **not** halogen (halogen has its own gate + vendor catalog: use `halogen-npu-feasibility-census` for that). This answers only: is the *host* able, and what is missing.
|
||||
|
||||
## Why it is usually the wrong door first
|
||||
|
||||
XDNA2 executes **precompiled AIE partition programs** loaded through XRT → `amdxdna`. There is no generic tensor dispatch: an engine built on ggml/llama.cpp (or any GGUF loader) cannot "enable" the NPU — the port must be a re-export + vendor compile, not a flag. So pair this census with the accelerator the same engine *can* select by build flag (Vulkan/HIP on the gfx1151 iGPU) before proposing NPU work.
|
||||
|
||||
## Census (all read-only, over ssh from the operator box)
|
||||
|
||||
```bash
|
||||
ssh <host> '
|
||||
cat /proc/cmdline; ls /sys/kernel/iommu_groups | wc -l # iommu=pt required; groups==0 => IOMMU off => NPU dead
|
||||
lsmod | grep -i amdxdna # in-tree driver
|
||||
sudo dmesg | grep -iE "xdna|npu" | tail # "Initialized amdxdna_accel_driver <ver> for 0000:c6:00.1"
|
||||
lspci -nn | grep -iE "npu|processing" # Strix Halo = 1022:17f0
|
||||
ls /sys/bus/pci/drivers/amdxdna # must list the BDF
|
||||
ls -l /dev/accel # accel0, root:render 660
|
||||
id -nG <deploy-user> | tr " " "\n" | grep -E "render|video" # rootless podman needs render, else --device fails at create
|
||||
find /usr/lib/firmware -iname "*npu*" | head # firmware is /usr/lib/firmware/amdnpu/<devid>_<rev>/npu.sbin*.zst — match the PCI rev
|
||||
dpkg -l | grep -iE "xrt|xdna"; ls /opt/xilinx/xrt/lib 2>&1 # distro libxrt-npu2 = XDNA2 runtime; vendor /opt/xilinx/xrt usually absent
|
||||
apt-cache policy libxrt2 libxrt-npu2 | grep Candidate
|
||||
apt-cache search onnxruntime | head # distro ONNX RT exists; the Ryzen AI EP does not
|
||||
python3 -c "import onnxruntime" 2>&1 | tail -1 # absence of the EP + of ryzenai-compile is the real blocker
|
||||
for c in /sys/class/drm/card*/device; do echo "-- $c"; cat $c/power_dpm_force_performance_level 2>&1; cat $c/pp_dpm_fclk 2>&1 | tr "\n" " "; echo; done
|
||||
nproc; free -g | head -2 # RAM headroom + thread count the engine will pick
|
||||
'
|
||||
```
|
||||
|
||||
## Reading it
|
||||
|
||||
- Driver bound + `accel0` + firmware dir + `iommu=pt` ⇒ **kernel side is ready**; do not re-litigate "is the NPU disabled". A missing device node is *not* the modern failure mode.
|
||||
- `auto` on `pp_dpm_fclk` / `power_dpm_force_performance_level` ⇒ fabric clock **unheld**. GPU+NPU concurrently with a moving fclk hangs these hosts and corrupts NPU results; any NPU plan must pin the top fclk step (or run in a container holding it) first.
|
||||
- No `ryzenai-compile`/ONNX Ryzen AI EP ⇒ nothing on the host can produce a partition program. That is the enablement delta, and it is a toolchain install + a model-specific export/quantize/compile project, not a config change.
|
||||
- `free` headroom matters: NPU partitions want a few GB of host RAM each and can log benign `Failed to alloc dev bo memory, ret -28` under pressure.
|
||||
|
||||
## Laws / traps
|
||||
|
||||
- Read state only. Never probe or load the NPU beside a live inference service — stop + mask the service first (fleet benching policy); this host class has a precedent of a concurrent GPU/NPU experiment needing a power cycle.
|
||||
- `sudo podman ...` finds nothing (per-user rootless storage); run podman as the deploy user.
|
||||
- `/dev/accel/accel0` existing does not mean a context/PASID can be created; with IOMMU off the failure is at context creation, not at the node.
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: vault-one-fact-one-owner-dedupe
|
||||
description: "Collapse per-payload/per-attempt vault pages into one owner page per endpoint without losing a fact: port-before-cut orphan census, index regeneration, the one-page seam, and the merge-resolution order."
|
||||
description: "\"Collapse per-payload/per-attempt vault pages into one owner page per endpoint without losing a fact: port-before-cut orphan census, doubled-page repair, trash-aware tools, delegated-write verification, index regeneration, and the merge-resolution order.\""
|
||||
---
|
||||
|
||||
# One fact, one owner: dedupe the vault without losing a fact
|
||||
@@ -18,6 +18,9 @@ mechanics are general.
|
||||
table or boot ledger**.
|
||||
- `log.md` owns incidents and laws only. A swap is an Eras row, never a log entry.
|
||||
- `AGENTS.md` carries operator-critical numbers + cross-border laws; everything else links.
|
||||
- A **retired** endpoint still gets one locator page: era windows quoted verbatim from each
|
||||
era page's own banner, no numbers restated, and any date conflict recorded as an open
|
||||
question instead of silently resolved. Never pick a winner between two primary sources.
|
||||
|
||||
## 2. Single-writer rule (the mistake that costs the most)
|
||||
|
||||
@@ -29,56 +32,71 @@ to be reconciled by hand.
|
||||
|
||||
## 3. Port-before-cut census (proves nothing was lost)
|
||||
|
||||
For every deleted line, extract distinctive keys (integers ≥ 4 digits with commas stripped,
|
||||
decimals with ≥ 2 fraction digits) and check them against the whole vault minus the file
|
||||
being rewritten. Any line with **no** surviving key is an orphan → port, or accept
|
||||
deliberately and say so.
|
||||
For every deleted line, extract distinctive keys and check them against the vault minus the
|
||||
file being rewritten. Any line with **no** surviving key is an orphan → port, or accept
|
||||
deliberately and say so. Use `scripts/port-census.py <new-dir> <git-HEAD:path|file>` (numbers
|
||||
with ≥ 4 int digits, decimals, flags, paths, backticked spans, `--`, ALL-CAPS, or ≥ 2 hyphens).
|
||||
|
||||
```python
|
||||
num = re.compile(r'\d[\d,]*(?:\.\d+)?')
|
||||
def keys(line):
|
||||
out = set()
|
||||
for n in num.findall(line):
|
||||
i = n.split('.')[0].replace(',', '')
|
||||
if len(i) >= 4: out.add(i)
|
||||
if '.' in n and len(n.split('.')[1]) >= 2: out.add(n.replace(',', '')[:6])
|
||||
return out
|
||||
# orphan = deleted line whose keys are absent from corpus(vault minus target file, minus .AGENTTRASH)
|
||||
```
|
||||
Two calibration traps:
|
||||
- **Prose words are not facts.** A hyphen or trailing period inside a word (`long-standing`,
|
||||
`deployed:`) was counted as a tracked token while real identifiers hid behind it; that noise
|
||||
buried 300 real lines under 979 fake ones. Strip punctuation, require fact shape.
|
||||
- **The corpus decides the question.** Corpus = the new file only → "what did this rewrite
|
||||
drop?" Corpus = whole vault → "is the dropped fact orphaned repo-wide?" Ask one at a time.
|
||||
|
||||
Expect ~30 orphans per 1,000 deleted lines, of which ~40 % are process trivia recoverable
|
||||
from git (PR/commit hashes, per-boot log lines). Port the measurement/law ones to the owner
|
||||
page's Laws/Eras, and list the accepted losses in the commit message and PR body. Also grep
|
||||
`AGENTS.md` separately — a fact it carries is not an orphan.
|
||||
Also grep `AGENTS.md` separately — a fact it carries is not an orphan.
|
||||
|
||||
## 4. Deletion mechanics
|
||||
## 4. When a page is the same page twice
|
||||
|
||||
- Trash is **gitignored** (`.gitignore` has `.AGENTTRASH/`). Move with plain `mv` into the
|
||||
repo-root dated dir, e.g. `.AGENTTRASH/<YYYYMMDD>-topic/LLM/`. `git mv` a tracked page into
|
||||
it stages a rename *into an ignored path* (index entry the next agent cannot see); undo with
|
||||
`git restore --staged <src> <dst>` and let the deletion be the change.
|
||||
- Never `rm`, never `git clean -f`.
|
||||
Signature: a page whose `wc -l` is ~2× its `HEAD` size, with a **second frontmatter block in
|
||||
the body** whose closing `---` is glued onto the end of a body/timestamp line. Cause chain: a
|
||||
frontmatter rewrite that re-reads the file it just wrote (so its own output becomes the body),
|
||||
then a fence-repair pass keyed on `line == "---"` that cannot see a glued fence and inserts a
|
||||
fence mid-page, so the next pass treats the tail as a new document.
|
||||
|
||||
## 5. Rewire + indexes mechanically
|
||||
Un-double rule that worked: find the first frontmatter's close (accepting a glued fence), then
|
||||
the first `type:` line after it that has more keys (`title|description|tags|timestamp`) behind
|
||||
it — that is the second copy, and a stray `type:` in a code block has no key run. Keep the copy
|
||||
whose body is a **superset** (compare after stripping the dropped copy's own frontmatter),
|
||||
un-glue the fence, and prove `0` census losses before writing. Refuse to guess: if the halves
|
||||
differ materially, stop.
|
||||
|
||||
## 5. `.AGENTTRASH` is not vault content
|
||||
|
||||
Every tool that walks the vault must exclude it — **both the scan set and the resolution
|
||||
index**. The link checker resolving links *into* the trash turns "live page points at a
|
||||
deleted page" into a false PASS, which is worse than a false alarm. The `docs_completeness`
|
||||
seam's `find` must carry `-not -path '*/.AGENTTRASH/*'` in both walks, or a trashed page
|
||||
demands an index row and a trashed page can satisfy the docs requirement for a live service.
|
||||
|
||||
Trash is gitignored; move with plain `mv` into the dated dir. `git mv` into an ignored path
|
||||
stages a rename the next agent cannot see (`git restore --staged <src> <dst>`). Never `rm`,
|
||||
never `git clean -f`.
|
||||
|
||||
## 6. Rewire + regenerate indexes mechanically
|
||||
|
||||
```bash
|
||||
grep -rnoE "\[\[(page-a|page-b|...)\]\]" --include="*.md" serverAppConfigs-docs | grep -v AGENTTRASH
|
||||
grep -rnoE "\((page-a|page-b|...)\.md\)" --include="*.md" serverAppConfigs-docs | grep -v AGENTTRASH
|
||||
```
|
||||
Both wikilinks and markdown path refs must be repointed (Research `See Also` lists and
|
||||
`ubatch`-style bench pages are where path refs hide). Then **regenerate** index pages with a
|
||||
script that walks the vault (`os.walk`, skip `.obsidian`/`.AGENTTRASH`, one row = `[[basename]]`
|
||||
+ first-sentence description stripped of markdown links) rather than hand-editing rows — the
|
||||
`docs index links every docs page` seam fails on any row left behind.
|
||||
|
||||
## 6. Enforce the law with a seam
|
||||
Both wikilinks and markdown path refs must be repointed (Research `See Also` lists and bench
|
||||
pages are where path refs hide). Then **regenerate** index rows with a script, never by hand.
|
||||
The generator bug to check for: presence tested as a **bare stem substring** — a page whose
|
||||
name prefixes another's (`frank-medium` inside `frank-medium-rocmfpx`) then silently never
|
||||
gets a row. Test the wikilink form `[[<stem>]]`.
|
||||
|
||||
## 7. Enforce the law with a seam
|
||||
|
||||
`tests/docs_completeness.bats`: derive the live set instead of hardcoding it — an endpoint is
|
||||
live when `scripts/` holds **both** `<name>.env` and `<name>.service`; fail if any
|
||||
`<name>-*.md` page exists in the vault, and guard against a vacuous derivation
|
||||
(`derived -ge 3`). A re-created era page then cannot pass tests.
|
||||
`<name>-*.md` page exists, and guard against a vacuous derivation (`derived -ge 3`). A
|
||||
re-created era page then cannot pass tests. For the length hint, prefer **derived** exemptions
|
||||
(live endpoint, or a `retired-endpoint` tag: a campaign ledger that exists nowhere else is
|
||||
owned by the page that measured it) plus one documented per-page opt-out; never let a size
|
||||
ceiling force a ledger to be split across two owners.
|
||||
|
||||
## 7. Edit-tool hazard (costs the most time)
|
||||
## 8. Edit-tool hazard (costs the most time)
|
||||
|
||||
Body rows beginning with `-` or `+` are op markers. The documented `+-` escape **writes a
|
||||
literal `+`** — do not use it. Safe rewrites inside an existing bullet: keep the `- ` opener
|
||||
@@ -86,30 +104,49 @@ line untouched and PUT only the indented continuation lines, or convert the note
|
||||
bullet whose body lines start with two/four spaces. If a wrapped `[[Two Word Page]]` shows up
|
||||
as an unresolved link, the target was hard-wrapped across lines — unwrap it.
|
||||
|
||||
## 8. Merge order (a long-lived branch under a moving main)
|
||||
## 9. Delegated writes: verify bytes, never narrative
|
||||
|
||||
Measured in one campaign, all four failure modes live:
|
||||
- A subagent killed by the runtime limit yielded a 495-line owner page, a verified era table
|
||||
and a byte census — **the file did not exist** and its table was self-referential.
|
||||
- Another wrote its output to disk and died without reporting, leaving a rewritten `log.md`;
|
||||
the census showed it had dropped laws held nowhere else.
|
||||
- Verdict tables claimed "0 sole records / no contradictions" while missing the contradiction
|
||||
sitting in their own evidence row, and invented bullet IDs — one renamed a bullet to the
|
||||
**opposite** claim.
|
||||
- A scout called accurate docs "WRONG" because the docs said `wip: "down"`.
|
||||
|
||||
Rules: after ANY delegated write run `wc`/`ls` + the orphan census yourself; re-grep every
|
||||
claimed holder before cutting a line; treat `cancelled`/timed-out yields as unverified no
|
||||
matter how concrete they read; and re-read the source bullet before trusting a table's ID.
|
||||
|
||||
## 10. Merge order (a long-lived branch under a moving main)
|
||||
|
||||
1. Commit your work in atomic groups **before** merging (a dirty tree cannot merge cleanly).
|
||||
2. Inspect the incoming commits first: `git log --oneline --stat HEAD..origin/main`. If they
|
||||
modify a page you deleted, **port their additions into the owner page**, then merge and
|
||||
resolve the modify/delete as `git rm`.
|
||||
2. Inspect the incoming commits: `git log --oneline --stat HEAD..origin/main`. If they modify
|
||||
a page you deleted, **port their additions into the owner page**, then merge and resolve
|
||||
the modify/delete as `git rm`.
|
||||
3. Resolve a content block with `write conflict://<id>` (`@ours` / `@theirs` / explicit text),
|
||||
then **`git add` the file** — resolving content does not stage it, and `git commit` refuses
|
||||
with "you have unmerged files".
|
||||
4. `git * --no-verify` is forbidden here; commit hooks are fine.
|
||||
5. Sweep for dead paths left in `AGENTS.md`: `grep -oE "[A-Za-z0-9._-]+\.md" AGENTS.md | sort -u`
|
||||
then `find` each. Expect false positives from names hard-wrapped across two lines — confirm
|
||||
each with `ls` before "fixing".
|
||||
then **`git add` the file** — resolving content does not stage it.
|
||||
4. **Re-probe live state after the merge, before you write any number.** A campaign that
|
||||
reported fleet shapes at the start was made stale by a mid-campaign reslice on main: probe
|
||||
`/slots` and `tr '\0' '\n' < /proc/$(pidof <engine>)/cmdline` again at the end, and let the
|
||||
live answer beat both your earlier probe and the docs.
|
||||
5. `git * --no-verify` is forbidden here. Sweep dead paths in `AGENTS.md`
|
||||
(`grep -oE "[A-Za-z0-9._-]+\.md" AGENTS.md | sort -u`, then `find` each; hard-wrapped names
|
||||
are false positives — confirm with `ls`).
|
||||
|
||||
## 9. Verify, open, land
|
||||
## 11. Verify, open, land
|
||||
|
||||
- `./tests/run.sh` (full suite) + `bats tests/docs_completeness.bats` +
|
||||
`python3 scripts/check-vault-links.py serverAppConfigs-docs` → must print `0 unresolved links`.
|
||||
- Env trims: prove value-neutrality mechanically — `git show HEAD:scripts/<svc>.env | grep -E
|
||||
'^[A-Z_]+=' | sort` vs the working copy, diff must be empty, then run that service's seam.
|
||||
**Warn that `scripts/<svc>.env` is a deploy-planner path**: a comment-only trim makes the
|
||||
next service-scoped deploy restart a healthy endpoint for identical values.
|
||||
- `./tests/run.sh` (full suite) + `python3 scripts/check-vault-links.py serverAppConfigs-docs`
|
||||
→ must print `0 unresolved links`, and the OKF linter `0 violations`.
|
||||
- Env trims: prove value-neutrality mechanically (`git show HEAD:scripts/<svc>.env | grep -E
|
||||
'^[A-Z_]+=' | sort` vs working copy, diff empty). **`scripts/<svc>.env` is a deploy-planner
|
||||
path**: a comment-only trim makes the next service-scoped deploy restart a healthy endpoint.
|
||||
- Open the PR through the API with a 600-perm curl config (managed skill
|
||||
`gitea-api-pr-operations`), end the body with the `cwd:` / `session:` footer, then
|
||||
`xdg-open <PR-URL>` the instant it exists.
|
||||
`xdg-open <PR-URL>` the instant it exists. Never merge without Frank's current explicit OK.
|
||||
- Docs-only change ⇒ **no deploy**. Fast-forward the deploy checkout only
|
||||
(`git -C ~/Projects/serverAppConfig merge --ff-only origin/main`).
|
||||
- Say what was NOT done, in the PR body: soft lint tallies left (missing `## Citations`),
|
||||
conflicts left standing, repo config that was never installed on a host.
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792386375195
|
||||
"lastConsolidatedAt": 1792401025778
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792400553633,
|
||||
"lastConsolidatedAt": 1792400853634,
|
||||
"lastResult": {
|
||||
"at": 1792393886960,
|
||||
"sessions": 1,
|
||||
|
||||
Whitespace-only changes.
Whitespace-only changes.
@@ -1,4 +1,10 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792400552453
|
||||
"lastConsolidatedAt": 1792426412619,
|
||||
"lastResult": {
|
||||
"at": 1792401456699,
|
||||
"sessions": 1,
|
||||
"deltas": 1,
|
||||
"model": "frank-zealot"
|
||||
}
|
||||
}
|
||||
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792426569902,
|
||||
"lastResult": {
|
||||
"at": 1792402864677,
|
||||
"sessions": 1,
|
||||
"deltas": 8,
|
||||
"model": "frank-zealot"
|
||||
}
|
||||
}
|
||||
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"v": 1,
|
||||
"lastConsolidatedAt": 1792426408958,
|
||||
"lastResult": {
|
||||
"at": 1792401394555,
|
||||
"sessions": 1,
|
||||
"deltas": 1,
|
||||
"model": "frank-zealot"
|
||||
}
|
||||
}
|
||||
Whitespace-only changes.
+5
-5
@@ -158,13 +158,13 @@ providers:
|
||||
# contextWindow is the PER-SLOT window: llama.cpp divides --ctx-size
|
||||
# across slots and answers a longer prompt with a 400, never a
|
||||
# truncation, so this value must follow every reslice (live now:
|
||||
# 2 x 262,144 over exact f16 KV, 68afc6d).
|
||||
contextWindow: 262144
|
||||
# 3 x 200,704 over f16 K + q8_0 V with the MTP drafter armed, ba69ea5).
|
||||
contextWindow: 200704
|
||||
# Prompt + budget must fit inside the slot window; thinking counts
|
||||
# against the budget. Compaction fires at 85 % of the window =
|
||||
# 222,822 est; worst-case prompt + budget ≈ 222,822 × 1.03 +
|
||||
# 16,384 ≈ 245,891 ≤ 262,144. This endpoint is the fleet's
|
||||
# compaction hub: the widest window in the fleet.
|
||||
# 170,598 est; worst-case prompt + budget ≈ 170,598 × 1.03 +
|
||||
# 16,384 ≈ 192,100 ≤ 200,704. This endpoint stays the fleet's
|
||||
# compaction hub: still the widest window in this file.
|
||||
maxTokens: 16384
|
||||
reasoning: true
|
||||
input: [text, image]
|
||||
|
||||
@@ -275,13 +275,13 @@ 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_ymp267.jpg"
|
||||
|
||||
[wallpaper.last]
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_g7r3vd.jpg"
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_ymp267.jpg"
|
||||
|
||||
[wallpaper.monitors.DP-1]
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_g7r3vd.jpg"
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_y8ewzk.jpg"
|
||||
|
||||
[wallpaper.monitors.DP-2]
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_oxex69.jpg"
|
||||
@@ -290,7 +290,7 @@ transition = [ "fade" ]
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_v9e2w3.jpg"
|
||||
|
||||
[wallpaper.monitors.eDP-1]
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_g7r3vd.jpg"
|
||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_ymp267.jpg"
|
||||
|
||||
[weather]
|
||||
unit = "imperial"
|
||||
|
||||
@@ -142,6 +142,15 @@ supplies the `baseUrl`; the request is `POST {base}/chat/completions`. On
|
||||
this fleet that is **frank-zealot** (`llamacpp (dankclaw-3090)`,
|
||||
`:7771`) — the 6 s abort keeps a slow tiny model off the critical path.
|
||||
|
||||
The request body pins `chat_template_kwargs: { enable_thinking: false }`,
|
||||
and that field is load-bearing: the harness injects it for managed roles,
|
||||
but a raw `fetch` gets the model's default — thinking on — so the Qwen35
|
||||
hybrid spends the budget on `reasoning_content` and answers `content: ""`
|
||||
(measured: 6.9 s / 6893 reasoning chars / empty content without the flag;
|
||||
0.55 s / 369 clean chars with it). For a hybrid endpoint, empty content
|
||||
beside non-empty `reasoning_content` means the flag was ignored — a failed
|
||||
rewrite (mechanical fallback), never the legitimate "nothing to say".
|
||||
|
||||
Thinking blocks ride the same path: at `agent_end` the last assistant
|
||||
message's true `thinking` parts are narrated (rewritten first, as their own
|
||||
unit, before the answer), while blocks flagged `narration: true` — which
|
||||
@@ -196,6 +205,14 @@ the choice persists in the settings file.
|
||||
prose segments (mechanical cleanup of that text yields at most 2), and a
|
||||
thinking+answer `agent_end` played thinking first. 22 green in
|
||||
`tests/dankvoice.bats`.
|
||||
* **Thinking-off fix (2026-10-19, Refs #32)** — replaying the exact rewrite
|
||||
body against live frank-zealot: without `chat_template_kwargs.
|
||||
enable_thinking: false` the reply arrives as 6893 `reasoning_content`
|
||||
characters with `content: ""` in 6.9 s (twice the 6 s abort); with it,
|
||||
369 clean characters in 0.55 s. Driver fixtures now pin the flag
|
||||
(`thinkoff=false` on the recorded chat call), turn reasoning-only replies
|
||||
into mechanical speech instead of silence, and time the menu's test
|
||||
phrase (~0 ms handler). 26 green in `tests/dankvoice.bats`.
|
||||
|
||||
## Limits
|
||||
|
||||
@@ -232,6 +249,7 @@ the choice persists in the settings file.
|
||||
* Gitea issue #21 on `dank/command-center` (the work item this page ships with).
|
||||
* Gitea issue #23 on `dank/command-center` (the options-menu work item).
|
||||
* Gitea issue #27 on `dank/command-center` (the enhanced-narration work item).
|
||||
* Gitea issue #32 on `dank/command-center` (the latency/silence fix work item).
|
||||
|
||||
## See Also
|
||||
|
||||
|
||||
@@ -16,6 +16,19 @@ lesson, a few lines. Full incident narratives are recoverable from git
|
||||
(`git log --follow omp-docs/log.md`); knowledge that outlives an entry lives
|
||||
on its concept page, linked here. Never re-expand an entry in place.
|
||||
|
||||
## 2026-10-19 — dankvoice: silence and timeouts, root-caused (Refs #32)
|
||||
|
||||
The humanizer's raw `fetch` to frank-zealot omitted `chat_template_kwargs.
|
||||
enable_thinking: false`, so the Qwen35 hybrid *thought* instead of answering:
|
||||
6.9 s of `reasoning_content`, `content: ""` — timing out the rewrite twice
|
||||
(12 s dead air) and, when it did answer inside the window, speaking silence.
|
||||
Root-caused by replaying the exact wire body against the live endpoint.
|
||||
Fix: pin the kwarg in the request body; treat empty-content-with-reasoning as
|
||||
failure (mechanical voice), not "nothing worth saying"; the menu's test phrase
|
||||
fires without blocking its handler. Lesson: the harness injects thinking-off
|
||||
for managed roles, but raw HTTP callers must set it themselves — 26/26 green
|
||||
with three red-captured regressions.
|
||||
|
||||
## 2026-10-19 — dankvoice: humanized narration and thinking aloud (Refs #27)
|
||||
|
||||
Spoken units are rewritten by the `@tiny` role (frank-zealot) under the
|
||||
|
||||
@@ -8,7 +8,7 @@ tags:
|
||||
- documentation
|
||||
- best-practices
|
||||
- unverified
|
||||
timestamp: 2026-01-15
|
||||
timestamp: 2026-10-19
|
||||
---
|
||||
|
||||
# OKF Bundle Maintenance
|
||||
@@ -139,6 +139,27 @@ git status --short # What is uncommitted
|
||||
| `index.md` count differs from file count | Always | Fix immediately |
|
||||
| `log.md` has no entries for 30+ days | Context-dependent | Normal if no changes made |
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Automated Gates (this bundle)
|
||||
|
||||
Two mechanical checkers live in `scripts/`; both exit non-zero on violations.
|
||||
Run both before committing any `omp-docs/` change; `tests/kb-lint.bats` pins
|
||||
their behavior.
|
||||
|
||||
```bash
|
||||
bash scripts/kb-lint.sh omp-docs # OKF conformance
|
||||
python3 scripts/kb-coverage.py omp-docs # index coverage doctrine
|
||||
```
|
||||
|
||||
- **kb-lint** — OKF v0.1 rules: non-empty `type` frontmatter on every
|
||||
non-index file, no frontmatter on `index.md`, resolvable wikilinks and
|
||||
relative links, reserved names (`index.md`/`log.md`), `YYYY-MM-DD` log
|
||||
headings.
|
||||
- **kb-coverage** — the AGENTS.md anti-rot mechanism: every directory has an
|
||||
`index.md`, and that index links every sibling file and subdirectory.
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
File renamed without changes.
+64
-11
@@ -14,8 +14,9 @@
|
||||
# (player fixture), DV_PWPLAY (stub aliased as pw-play), DV_WPCTL (the sink
|
||||
# name the fixture wpctl reports), DV_MODELS (models.yml fixture),
|
||||
# DANKVOICE_CONFIG (settings file), DV_PLAYER_BIN (player command override),
|
||||
# DV_SELECT (pipe-separated answers fed to ctx.ui.select in order), DV_MODE
|
||||
# (the harness mode reported to the extension).
|
||||
# DV_MODE (the harness mode reported to the extension), DV_REWRITE (fixture
|
||||
# behavior for the humanizer route: prose | echo | reasoning | stall, with
|
||||
# DV_STALL_MS).
|
||||
|
||||
setup() {
|
||||
REPO="$(cd "$BATS_TEST_DIRNAME/.." && pwd)"
|
||||
@@ -31,8 +32,10 @@ setup() {
|
||||
cat > "$DV_STUB" <<'SH'
|
||||
#!/usr/bin/env bash
|
||||
printf '%s\n' "$*" >> "$DV_STUB_LOG"
|
||||
if [ -n "${DV_TIMING_LOG:-}" ]; then printf 'START %s\n' "$(date +%s%3N)" >> "$DV_TIMING_LOG"; fi
|
||||
for arg in "$@"; do case "$arg" in *.wav) cp "$arg" "$DV_COPY" ;; esac; done
|
||||
sleep 0.1
|
||||
sleep "${DV_HOLD:-0.1}"
|
||||
if [ -n "${DV_TIMING_LOG:-}" ]; then printf 'END %s\n' "$(date +%s%3N)" >> "$DV_TIMING_LOG"; fi
|
||||
SH
|
||||
chmod +x "$DV_STUB"
|
||||
|
||||
@@ -84,14 +87,21 @@ import { dirname } from "node:path";
|
||||
|
||||
const scenario = process.argv[2] ?? "speak";
|
||||
const AUDIO = Buffer.from([0x52, 0x49, 0x46, 0x46, 8, 0, 0, 0, 0x57, 0x41, 0x56, 0x45, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]);
|
||||
const received: Array<{ url: string | undefined; body: string }> = [];
|
||||
const started = Date.now();
|
||||
interface Call {
|
||||
url: string | undefined;
|
||||
body: string;
|
||||
at: number;
|
||||
}
|
||||
const received: Call[] = [];
|
||||
|
||||
const server = createServer((req, res) => {
|
||||
const chunks: Buffer[] = [];
|
||||
req.on("data", (chunk: Buffer) => chunks.push(chunk));
|
||||
req.on("end", () => {
|
||||
const at = Date.now() - started;
|
||||
const body = Buffer.concat(chunks).toString("utf8");
|
||||
received.push({ url: req.url, body });
|
||||
received.push({ url: req.url, body, at });
|
||||
if (req.url?.endsWith("/audio/voices")) {
|
||||
res.setHeader("content-type", "application/json");
|
||||
res.end(JSON.stringify(["narrator1", "narrator2"]));
|
||||
@@ -103,11 +113,18 @@ const server = createServer((req, res) => {
|
||||
return;
|
||||
}
|
||||
if (process.env.DV_REWRITE && req.url?.endsWith("/chat/completions")) {
|
||||
res.setHeader("content-type", "application/json");
|
||||
const spoken = process.env.DV_REWRITE === "echo"
|
||||
? String(JSON.parse(body).messages?.[1]?.content ?? "")
|
||||
: "Spoken, humanized words.";
|
||||
res.end(JSON.stringify({ choices: [{ message: { content: spoken } }] }));
|
||||
const respond = () => {
|
||||
res.setHeader("content-type", "application/json");
|
||||
const spoken = process.env.DV_REWRITE === "echo"
|
||||
? String(JSON.parse(body).messages?.[1]?.content ?? "")
|
||||
: "Spoken, humanized words.";
|
||||
const verdict = process.env.DV_REWRITE === "reasoning"
|
||||
? { choices: [{ message: { content: "", reasoning_content: "Long internal deliberation that never reaches the reply field." } }] }
|
||||
: { choices: [{ message: { content: spoken } }] };
|
||||
res.end(JSON.stringify(verdict));
|
||||
};
|
||||
if (process.env.DV_REWRITE === "stall") setTimeout(respond, Number(process.env.DV_STALL_MS ?? 200));
|
||||
else respond();
|
||||
return;
|
||||
}
|
||||
res.statusCode = 404;
|
||||
@@ -219,6 +236,12 @@ if (!say) {
|
||||
await say.handler("on", ctx);
|
||||
await agentEnd?.({ messages: [{ role: "assistant", content: [{ type: "thinking", thinking: "Considering the wiring." }, { type: "text", text: "The answer is nine." }] }] }, ctx);
|
||||
break;
|
||||
case "testphrase": {
|
||||
const startedAt = Date.now();
|
||||
await say.handler("menu", ctx);
|
||||
console.log(`HANDLER_MS ${Date.now() - startedAt}`);
|
||||
break;
|
||||
}
|
||||
case "narration":
|
||||
await say.handler("on", ctx);
|
||||
await agentEnd?.({ messages: [{ role: "assistant", content: [{ type: "thinking", thinking: "Here is the result.", narration: true }] }] }, ctx);
|
||||
@@ -239,7 +262,11 @@ for (const call of received) {
|
||||
}
|
||||
return "-";
|
||||
};
|
||||
console.log(`CALL ${call.url} model=${field("model")} format=${field("response_format")} voice=${field("voice")} input=${field("input")}`);
|
||||
const kwargs = parsed && typeof parsed === "object" && "chat_template_kwargs" in parsed
|
||||
? (parsed as { chat_template_kwargs?: { enable_thinking?: unknown } }).chat_template_kwargs
|
||||
: undefined;
|
||||
const think = kwargs && typeof kwargs === "object" && "enable_thinking" in kwargs ? String(kwargs.enable_thinking) : "absent";
|
||||
console.log(`CALL ${call.url} model=${field("model")} format=${field("response_format")} voice=${field("voice")} input=${field("input")}${call.url?.endsWith("/chat/completions") ? ` thinkoff=${think}` : ""}`);
|
||||
}
|
||||
console.log(`CALLS ${received.filter((call) => call.url?.endsWith("/audio/speech")).length}`);
|
||||
const log = process.env.DV_STUB_LOG && existsSync(process.env.DV_STUB_LOG) ? readFileSync(process.env.DV_STUB_LOG, "utf8").trim() : "";
|
||||
@@ -417,3 +444,29 @@ say_run() {
|
||||
DV_PLAYER_BIN="$DV_PWPLAY" say_run speak
|
||||
[[ "$output" == *"PLAYER --target $DV_WPCTL"* ]]
|
||||
}
|
||||
|
||||
@test "the rewrite asks the humanizer with thinking explicitly off" {
|
||||
DV_REWRITE=prose say_run rewrite
|
||||
[[ "$output" == *"input=- thinkoff=false"* ]]
|
||||
}
|
||||
|
||||
@test "an answer hiding in reasoning_content speaks the mechanical text, not silence" {
|
||||
DV_REWRITE=reasoning say_run rewrite
|
||||
[[ "$output" == *"input=The deploy failed because a secret was missing."* ]]
|
||||
[[ "$output" == *"CALLS 1"* ]]
|
||||
[[ "$output" == *"PLAYS 1"* ]]
|
||||
}
|
||||
|
||||
@test "a rewrite slower than a breath but under the timeout still speaks" {
|
||||
DV_REWRITE=stall DV_STALL_MS=300 say_run rewrite
|
||||
[[ "$output" == *"input=Spoken, humanized words."* ]]
|
||||
[[ "$output" == *"CALLS 1"* ]]
|
||||
}
|
||||
|
||||
@test "the menu test phrase speaks in the background instead of blocking the menu" {
|
||||
DV_REWRITE=stall DV_STALL_MS=1500 say_run testphrase "Speak a test phrase"
|
||||
[[ "$output" == *"NOTIFY info dankvoice: speaking the test phrase"* ]]
|
||||
local ms
|
||||
ms=$(grep -o 'HANDLER_MS [0-9]*' <<<"$output" | cut -d' ' -f2)
|
||||
[ -n "$ms" ] && [ "$ms" -lt 1200 ]
|
||||
}
|
||||
Reference in new issue
Block a user