open-notebook as the fleet's podcast generator on Dankclaw: notes and sources
in, a multi-speaker episode out — script written by frank-zealot, voices
rendered by the fleet's own frank-tts, no cloud provider anywhere in the
path. Repo-side only: nothing was deployed, no host was touched, no Caddy vhost,
no container started.
Piece
Decision, and why
Units
dankclaw/open-notebook/quadlet/{open-notebook,open-notebook-surrealdb}.container + open-notebook.network — a private bridge at 10.89.4.0/24 (open-webui holds 10.89.2.0/24, searxng 10.89.3.0/24; measured against live podman network ls). Two containers because that is what upstream runs; the deprecated -single tag would give the host a second hidden database. Both digest-pinned with Pull=never + a documented one-time pull, Restart=always, short RestartSec, [Install] WantedBy=default.target.
Ports
UI publishes 0.0.0.0:8502:8502. The API publishes 0.0.0.0:5056:5055 because host 5055 is taken — seerr has held 127.0.0.1:5055 since 2026-10-14 and live ss -ltn agrees. The frontend derives its API URL from the window origin when API_URL is unset, so a silent remap would hang the UI on a port nobody listens on: API_URL=http://192.168.0.143:5056 is forced in scripts/open-notebook.env. SurrealDB's 8000 is published nowhere (upstream exposes it on loopback, with root credentials); the seam forbids it.
Auth posture
Auth is off by default and the episode-audio route is unauthenticated, so OPEN_NOTEBOOK_PASSWORD must be set, CORS_ORIGINS is origin-form, and OPEN_NOTEBOOK_ENCRYPTION_KEY is required before the UI can save any provider credential. The tracked env file carries those keys empty; the deployer seeds /home/dank/Containers/open-notebook/env (0600) from the host env and refuses to start the pair while any secret is blank, and never rewrites a seeded file (a rotated key would silently make stored credentials undecryptable). No secret in the repo.
CPU guard
TTS_BATCH_SIZE=1, OPEN_NOTEBOOK_WORKER_MAX_TASKS=1 (upstream ships 5 × 5): frank-tts spawns one ~8 GiB / 8-thread engine process per request on a box that also serves two LLM endpoints. ESPERANTO_TTS_TIMEOUT=660 sits one round-trip outside frank-tts's own SHIM_TIMEOUT=600 so a shim give-up arrives labelled as frank-tts's error; ESPERANTO_LLM_TIMEOUT stays at the app's 180 s default, under the web UI's 600 s patience.
Homepage
Card added, urlStatus=hidden until the service is deployed (the probe runs on the gateway and would render a dead card on day one); internal port only, no vhost field.
Episode destination
Interim: the app keeps its own named volume and /home/dank/media/podcasts is bind-mounted over /app/data/podcasts (DATA_FOLDER is hard-coded ./data), so finished MP3s are ordinary host files instead of volume contents needing podman unshare. One mount line; deleting it is the whole rollback. The destination decision itself is Gitea issue #7, human-only — not worked here.
Tests
19 new tests in tests/open_notebook.bats, structure only. Writing them caught a real routing bug: in scripts/test-changed.sh the dankclaw/*.container case's *crosses /, so a case placed below it never matched dankclaw/open-notebook/quadlet/*.container — the open-notebook quadlet was being tested by the open-webui/pihole/searxng seams.
Not runnable end-to-end yet
Nothing listens on 7772: frank-tts is not deployed (ss -ltn, Dankclaw,
2026-10-18). Deployed today, open-notebook writes the episode script and then
fails on the first voice line. The render path unblocks when the voice-stack
branch (feat/frank-tts-engine, being built now) lands and frank-tts gets its
deployer/homepage registration.
The engine decision on that branch — qwentts.cpp arm A, judged on live output
— was taken by Frank today and belongs to that branch, not this one. Nothing
here depends on it beyond the base URL of the TTS credential.
Merge order
This branch first is fine. Whoever merges second will hit overlap in serverAppConfigs-docs/log.md, serverAppConfigs-docs/index.md, scripts/deploy-remote.sh, scripts/deploy-plan.py and scripts/test-changed.sh
(both branches append to the same registries). The parent agent resolves that at
merge time — nothing here was rebased onto anything else, per the trunk law.
Verification
./tests/run.sh on the final commit: last test ok 403, then ==> ALL TESTS PASSED (baseline 384 → 403; wall 70 s).
Both digest pins resolved from the registry (lfnovo/open_notebook:v1-latest,
1.27 GB, and surrealdb:v2 = 2.7.0). The database digest was run: /surreal is-ready reports ready with the credentials in the environment and
exits non-zero without SURREAL_PASS — which is why Exec= carries no
credential and both health probes are argument-only.
podman generate systemd dry-run on both units plus a network dry-run
confirmed the generated ExecArgs: the publishes, both EnvironmentFiles in
order (podman applies last-match-wins, so the repo tunables go first and the
host secrets second), both mounts, and podman's own health wrapper.
The deployer's gate names paths that answer: /health (upstream route, on the
auth middleware's exclude list, so it stays 200 once the password is set) at
the published 5056 mapping, and the database judged by its container health.
Sourced from upstream source rather than executed (the app image was never
pulled): the supervisord layout, DATA_FOLDER="./data", curl + ffmpeg present
in the image, no USER/HEALTHCHECK, the /v1 requirement on the TTS/LLM
base URL, free-text per-speaker voice ids (narrator1/2/3 typed verbatim),
and why Test Connection and Discover Models fail cosmetically against
frank-tts instead of being a fault.
No deploy, no host change, no Caddy change, no merge.
Build worktree: /home/blank/.omp/wt/wt-opennotebook (built by the OpenNotebookDeploy subagent of this session).
## What lands
**open-notebook** as the fleet's podcast generator on Dankclaw: notes and sources
in, a multi-speaker episode out — script written by **frank-zealot**, voices
rendered by the fleet's own **frank-tts**, no cloud provider anywhere in the
path. Repo-side only: nothing was deployed, no host was touched, no Caddy vhost,
no container started.
| Piece | Decision, and why |
|---|---|
| Units | `dankclaw/open-notebook/quadlet/{open-notebook,open-notebook-surrealdb}.container` + `open-notebook.network` — a **private bridge at `10.89.4.0/24`** (open-webui holds `10.89.2.0/24`, searxng `10.89.3.0/24`; measured against live `podman network ls`). Two containers because that is what upstream runs; the deprecated `-single` tag would give the host a second hidden database. Both **digest-pinned** with `Pull=never` + a documented one-time pull, `Restart=always`, short `RestartSec`, `[Install] WantedBy=default.target`. |
| Ports | UI publishes `0.0.0.0:8502:8502`. The API publishes **`0.0.0.0:5056:5055` because host 5055 is taken** — seerr has held `127.0.0.1:5055` since 2026-10-14 and live `ss -ltn` agrees. The frontend derives its API URL from the window origin when `API_URL` is unset, so a silent remap would hang the UI on a port nobody listens on: **`API_URL=http://192.168.0.143:5056` is forced** in `scripts/open-notebook.env`. SurrealDB's 8000 is published **nowhere** (upstream exposes it on loopback, with root credentials); the seam forbids it. |
| Auth posture | Auth is off by default and the episode-audio route is unauthenticated, so `OPEN_NOTEBOOK_PASSWORD` must be set, `CORS_ORIGINS` is origin-form, and `OPEN_NOTEBOOK_ENCRYPTION_KEY` is required before the UI can save *any* provider credential. The tracked env file carries those keys **empty**; the deployer seeds `/home/dank/Containers/open-notebook/env` (0600) from the host env and **refuses to start the pair while any secret is blank**, and never rewrites a seeded file (a rotated key would silently make stored credentials undecryptable). No secret in the repo. |
| CPU guard | `TTS_BATCH_SIZE=1`, `OPEN_NOTEBOOK_WORKER_MAX_TASKS=1` (upstream ships 5 × 5): frank-tts spawns one ~8 GiB / 8-thread engine process per request on a box that also serves two LLM endpoints. `ESPERANTO_TTS_TIMEOUT=660` sits one round-trip **outside** frank-tts's own `SHIM_TIMEOUT=600` so a shim give-up arrives labelled as frank-tts's error; `ESPERANTO_LLM_TIMEOUT` stays at the app's 180 s default, under the web UI's 600 s patience. |
| Homepage | Card added, **`urlStatus=hidden`** until the service is deployed (the probe runs on the gateway and would render a dead card on day one); internal port only, no vhost field. |
| Episode destination | **Interim:** the app keeps its own named volume and `/home/dank/media/podcasts` is bind-mounted over `/app/data/podcasts` (`DATA_FOLDER` is hard-coded `./data`), so finished MP3s are ordinary host files instead of volume contents needing `podman unshare`. One mount line; deleting it is the whole rollback. The destination decision itself is **Gitea issue #7, `human-only`** — not worked here. |
| Tests | **19 new tests** in `tests/open_notebook.bats`, structure only. Writing them caught a real routing bug: in `scripts/test-changed.sh` the `dankclaw/*.container` case's `*` **crosses `/`**, so a case placed below it never matched `dankclaw/open-notebook/quadlet/*.container` — the open-notebook quadlet was being tested by the open-webui/pihole/searxng seams. |
## Not runnable end-to-end yet
**Nothing listens on 7772: `frank-tts` is not deployed** (`ss -ltn`, Dankclaw,
2026-10-18). Deployed today, open-notebook writes the episode script and then
fails on the first voice line. The render path unblocks when the voice-stack
branch (`feat/frank-tts-engine`, being built now) lands and frank-tts gets its
deployer/homepage registration.
The engine decision on that branch — **qwentts.cpp arm A, judged on live output**
— was taken by Frank today and belongs to that branch, not this one. Nothing
here depends on it beyond the base URL of the TTS credential.
## Merge order
This branch first is fine. Whoever merges **second** will hit overlap in
`serverAppConfigs-docs/log.md`, `serverAppConfigs-docs/index.md`,
`scripts/deploy-remote.sh`, `scripts/deploy-plan.py` and `scripts/test-changed.sh`
(both branches append to the same registries). The parent agent resolves that at
merge time — nothing here was rebased onto anything else, per the trunk law.
## Verification
- `./tests/run.sh` on the final commit: last test `ok 403`, then
`==> ALL TESTS PASSED` (baseline 384 → **403**; wall 70 s).
- Both digest pins resolved from the registry (`lfnovo/open_notebook:v1-latest`,
1.27 GB, and `surrealdb:v2` = **2.7.0**). The database digest was **run**:
`/surreal is-ready` reports ready with the credentials in the environment and
exits non-zero without `SURREAL_PASS` — which is why `Exec=` carries no
credential and both health probes are argument-only.
- `podman generate systemd` dry-run on both units plus a network dry-run
confirmed the generated `ExecArgs`: the publishes, both `EnvironmentFile`s in
order (podman applies last-match-wins, so the repo tunables go first and the
host secrets second), both mounts, and podman's own health wrapper.
- The deployer's gate names paths that answer: `/health` (upstream route, on the
auth middleware's exclude list, so it stays 200 once the password is set) at
the published 5056 mapping, and the database judged by its container health.
- Sourced from upstream source rather than executed (the app image was never
pulled): the supervisord layout, `DATA_FOLDER="./data"`, curl + ffmpeg present
in the image, no `USER`/`HEALTHCHECK`, the `/v1` requirement on the TTS/LLM
base URL, free-text per-speaker voice ids (`narrator1/2/3` typed verbatim),
and why **Test Connection** and **Discover Models** fail cosmetically against
frank-tts instead of being a fault.
- No deploy, no host change, no Caddy change, no merge.
Build worktree: `/home/blank/.omp/wt/wt-opennotebook` (built by the `OpenNotebookDeploy` subagent of this session).
---
cwd: /home/blank/.omp/wt/wt-20261017-181502-6d1fc1f · session: 01a14c93-8870-75d9-9900-9a731559feef
Two containers, because that is what upstream runs (docker-compose.yml:6,13):
the app image under supervisord, plus SurrealDB. The `v1-latest-single` tag is
not in play — it bundles its own database, is deprecated upstream, and would
give the host a second hidden DB.
Both images are digest-pinned with Pull=never and a documented one-time pull,
the frank-dragoon posture: a Quadlet on :latest re-resolves on the first restart
after an unrelated daemon-reload, and the failure is silent.
Port map paid for by what is already on the host: seerr holds 127.0.0.1:5055
(since 2026-10-14, live `ss -ltn` agrees), so the API leaves on 5056 and the env
file has to set API_URL to match, because the frontend derives the API URL from
the window origin when it is unset. 8502 publishes on 0.0.0.0 — the service
carries its own password and the episode-audio route is unauthenticated. 8000 is
published nowhere, where upstream exposes it on loopback with root credentials.
The app joins dankclaw_default first and the private systemd-open-notebook
second: it reaches the published fleet endpoints through the former
(host.containers.internal, the arrangement Prometheus is proven against) and
finds its database by container name through the latter. Generated --network
flags keep file order, so the tunable file is listed before the host secret file
in both units — podman applies last-match-wins.
No UserMapping: the app runs as root in its namespace (no USER in the Dockerfile)
and writes only paths owned by that root. Exec= carries no database credential —
`exec start --user --pass` puts it in world-readable argv; the image's own
entrypoint passes it on stdin, so only the container env holds it.
The two knobs that keep the box alive: TTS_BATCH_SIZE=1 and
OPEN_NOTEBOOK_WORKER_MAX_TASKS=1. Upstream ships 5 x 5; frank-tts spawns one
~8 GiB, 8-thread engine process per synthesis request on a machine that also
serves two LLMs, so a five-way fan-out is a self-inflicted shutdown. The worker
knob is real, not decorative: the image's supervisord expands
${OPEN_NOTEBOOK_WORKER_MAX_TASKS:-5} into the worker command
(supervisord.conf:25).
Both ESPERANTO_* timeouts go above upstream's 300 s defaults, sized for CPU
synthesis (SHIM_TIMEOUT=600, LLAMA_TTS_THREADS=8) — one episode is dozens of
per-line /audio/speech calls and a timeout kills the episode, not the line.
API_URL is forced for the 5055 -> 5056 remap; SURREAL_URL addresses the database
by container name; CORS_ORIGINS is origin-form (scheme://host:port), the form
the app compares against. OPEN_NOTEBOOK_PASSWORD stays absent here on purpose:
the app treats a set-but-empty value as auth-on.
Secrets stay placeholders in the tracked file and in dankclaw/.env.example, which
lists SURREAL_PASSWORD rather than SURREAL_PASS — the deployer derives the
surreal CLI's spelling from it when it seeds, so the app and the database cannot
be handed different values.
Registration is load-bearing, not cosmetic: deploy-plan.py refuses any service
it does not know, so SERVICE_ORDER + DANKCLAW_SERVICES are what let `--only` and
`--auto` address the pair at all; the dispatch case in deploy-remote.sh runs it;
the requirement in check_prerequisites gates a manual deploy; and the dispatch
regex in deploy.sh's host-mode branch decides whether a changed path dispatches
a service or falls through to "no service mapped" — an omission there is silent,
which is exactly how the AGENTS.md deployer-registration law was earned.
The deploy seeds /home/dank/Containers/open-notebook/env (0600) from the host
env and refuses to start the pair while OPEN_NOTEBOOK_ENCRYPTION_KEY or
OPEN_NOTEBOOK_PASSWORD is blank: without the key no provider credential saves
and without the password the API — including the episode-audio route — is open
to the LAN with no auth. A seeded file is never rewritten, so re-deploying
cannot clobber a rotated key, which would silently make every stored credential
undecryptable. SURREAL_PASS is derived from SURREAL_PASSWORD in the same write.
The gate names paths that answer, not invented ones: the database is judged by
its own container health status (/surreal is-ready, verified against the pinned
digest) and the app by wait_for_health on /health at the published 5056 mapping —
the route is upstream's and is on the auth middleware's exclude list, so it
stays 200 after the password is set. 180 s covers Next.js build + startup and a
degraded-but-listening first boot, matching the sibling LLM deploys.
Both digest pins are checked against local storage before restart, because
Pull=never means a missing image would otherwise surface as a unit stuck in
"Start request queued" (the 203/EXEC trap's sibling). The interim episode
directory is created before the app starts: a missing bind source makes podman
create it as root-owned and the app's mkdir would then fail.
Internal-only: port 8502 direct, no vhost field, no Caddy change — exposure
stays behind an explicit decision, and the app carries its own password.
urlStatus=hidden is deliberate: the homepage probe runs on the gateway and the
pair is not deployed, so a live card would render dead on day one. Flipping it
to live is the only homepage change the deploy needs; the seam asserts the
hidden state while the service is repo-only and requires the required card
fields either way.
mono-on.svg follows the mono-* icon set the sibling cards use, so the card does
not fall back to the generic dot.
The case has to sit where it does: the dankclaw/*.container glob's `*` crosses
`/`, so placed below it, dankclaw/open-notebook/quadlet/open-notebook.container
matched that generic case and ran the open-webui/pihole/searxng seams instead of
its own. Same reason the env path is grouped here — scripts/*.env would otherwise
send it to the generic env seams.
Structure only: what the units declare and how the scripts route them, never how
the app behaves inside a container. 19 tests.
The ones carrying real weight: the app unit has no publishable 8000 anywhere and
the database unit publishes nothing at all; the API remap off host 5055 is only
correct if API_URL names the same host port, so the seam parses the PublishPort
and compares; each unit reads the repo tunable file before the host secret file,
because podman's last-match-wins makes the order load-bearing; both health
probes are argument-only (a shell string there is a unit that starts dead); the
database password appears only as an env reference, never in Exec=; and the
episode host bind appears exactly once, since a second mount would silently
shadow the first.
The deployer assertions read the functions themselves: the gate URL path is
/health on the remapped port and must not name 5055, and the seed function
contains the blank-secret refusal plus the no-clobber test. No test asserts a
made-up endpoint or a cloud URL, and nothing touches the network.
Page registered in the vault index (the docs-completeness seam fails an
unregistered page) and logged in the bundle change log.
What the page owes the reader: the provider contract (frank-zealot for scripts
with a /v1 base URL, frank-tts for voices with 1-4 speakers mapped to the
narrator1/2/3 voice packs typed as free text), why Test Connection and Discover
Models fail cosmetically against frank-tts rather than being a fault, the
encryption key as the precondition for saving any credential, and the honest
blocker — frank-tts is not deployed and nothing listens on 7772, so a generation
run writes its script and fails on the first voice.
Episode destination is stated as interim and cites Gitea issue #7 (human-only,
agents must not work it): the app keeps its named volume, one bind mount puts
finished MP3s under /home/dank/media/podcasts, and deleting that one line is the
whole rollback. Rollback section also covers the full undo, including that the
podman network only disappears once the units are gone.
The env file first landed with ESPERANTO_TTS_TIMEOUT equal to SHIM_TIMEOUT=600.
Equality is a race: both budgets expire at the same instant, so the case the
number existed to prevent — the worker abandoning a synthesis frank-tts is still
finishing — is exactly what happens on the tail of a long line. Nested budgets
go inside-out, which is the rule scripts/frank-tts.env states for its own
LLAMA_TIMEOUT; the client waits one round-trip longer than the server, so a
frank-tts give-up arrives labelled as frank-tts's error. 660.
The seam's assertion moves from equality to `>` for the same reason, and now
also holds ESPERANTO_LLM_TIMEOUT under upstream's 600 s web-UI patience — above
it the failure surfaces as a UI timeout with no name on it. The scriptwriting
number itself stays at the app's 180 s default: frank-zealot answers an outline
or a transcript well inside it, which corrects the earlier commit message's
claim that both timeouts were raised past upstream's 300 s.
dank
merged commit 1eb8b2199a into main2026-10-18 16:50:48 -07:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
What lands
open-notebook as the fleet's podcast generator on Dankclaw: notes and sources
in, a multi-speaker episode out — script written by frank-zealot, voices
rendered by the fleet's own frank-tts, no cloud provider anywhere in the
path. Repo-side only: nothing was deployed, no host was touched, no Caddy vhost,
no container started.
dankclaw/open-notebook/quadlet/{open-notebook,open-notebook-surrealdb}.container+open-notebook.network— a private bridge at10.89.4.0/24(open-webui holds10.89.2.0/24, searxng10.89.3.0/24; measured against livepodman network ls). Two containers because that is what upstream runs; the deprecated-singletag would give the host a second hidden database. Both digest-pinned withPull=never+ a documented one-time pull,Restart=always, shortRestartSec,[Install] WantedBy=default.target.0.0.0.0:8502:8502. The API publishes0.0.0.0:5056:5055because host 5055 is taken — seerr has held127.0.0.1:5055since 2026-10-14 and livess -ltnagrees. The frontend derives its API URL from the window origin whenAPI_URLis unset, so a silent remap would hang the UI on a port nobody listens on:API_URL=http://192.168.0.143:5056is forced inscripts/open-notebook.env. SurrealDB's 8000 is published nowhere (upstream exposes it on loopback, with root credentials); the seam forbids it.OPEN_NOTEBOOK_PASSWORDmust be set,CORS_ORIGINSis origin-form, andOPEN_NOTEBOOK_ENCRYPTION_KEYis required before the UI can save any provider credential. The tracked env file carries those keys empty; the deployer seeds/home/dank/Containers/open-notebook/env(0600) from the host env and refuses to start the pair while any secret is blank, and never rewrites a seeded file (a rotated key would silently make stored credentials undecryptable). No secret in the repo.TTS_BATCH_SIZE=1,OPEN_NOTEBOOK_WORKER_MAX_TASKS=1(upstream ships 5 × 5): frank-tts spawns one ~8 GiB / 8-thread engine process per request on a box that also serves two LLM endpoints.ESPERANTO_TTS_TIMEOUT=660sits one round-trip outside frank-tts's ownSHIM_TIMEOUT=600so a shim give-up arrives labelled as frank-tts's error;ESPERANTO_LLM_TIMEOUTstays at the app's 180 s default, under the web UI's 600 s patience.urlStatus=hiddenuntil the service is deployed (the probe runs on the gateway and would render a dead card on day one); internal port only, no vhost field./home/dank/media/podcastsis bind-mounted over/app/data/podcasts(DATA_FOLDERis hard-coded./data), so finished MP3s are ordinary host files instead of volume contents needingpodman unshare. One mount line; deleting it is the whole rollback. The destination decision itself is Gitea issue #7,human-only— not worked here.tests/open_notebook.bats, structure only. Writing them caught a real routing bug: inscripts/test-changed.shthedankclaw/*.containercase's*crosses/, so a case placed below it never matcheddankclaw/open-notebook/quadlet/*.container— the open-notebook quadlet was being tested by the open-webui/pihole/searxng seams.Not runnable end-to-end yet
Nothing listens on 7772:
frank-ttsis not deployed (ss -ltn, Dankclaw,2026-10-18). Deployed today, open-notebook writes the episode script and then
fails on the first voice line. The render path unblocks when the voice-stack
branch (
feat/frank-tts-engine, being built now) lands and frank-tts gets itsdeployer/homepage registration.
The engine decision on that branch — qwentts.cpp arm A, judged on live output
— was taken by Frank today and belongs to that branch, not this one. Nothing
here depends on it beyond the base URL of the TTS credential.
Merge order
This branch first is fine. Whoever merges second will hit overlap in
serverAppConfigs-docs/log.md,serverAppConfigs-docs/index.md,scripts/deploy-remote.sh,scripts/deploy-plan.pyandscripts/test-changed.sh(both branches append to the same registries). The parent agent resolves that at
merge time — nothing here was rebased onto anything else, per the trunk law.
Verification
./tests/run.shon the final commit: last testok 403, then==> ALL TESTS PASSED(baseline 384 → 403; wall 70 s).lfnovo/open_notebook:v1-latest,1.27 GB, and
surrealdb:v2= 2.7.0). The database digest was run:/surreal is-readyreports ready with the credentials in the environment andexits non-zero without
SURREAL_PASS— which is whyExec=carries nocredential and both health probes are argument-only.
podman generate systemddry-run on both units plus a network dry-runconfirmed the generated
ExecArgs: the publishes, bothEnvironmentFiles inorder (podman applies last-match-wins, so the repo tunables go first and the
host secrets second), both mounts, and podman's own health wrapper.
/health(upstream route, on theauth middleware's exclude list, so it stays 200 once the password is set) at
the published 5056 mapping, and the database judged by its container health.
pulled): the supervisord layout,
DATA_FOLDER="./data", curl + ffmpeg presentin the image, no
USER/HEALTHCHECK, the/v1requirement on the TTS/LLMbase URL, free-text per-speaker voice ids (
narrator1/2/3typed verbatim),and why Test Connection and Discover Models fail cosmetically against
frank-tts instead of being a fault.
Build worktree:
/home/blank/.omp/wt/wt-opennotebook(built by theOpenNotebookDeploysubagent of this session).cwd: /home/blank/.omp/wt/wt-20261017-181502-6d1fc1f · session: 01a14c93-8870-75d9-9900-9a731559feef
The two knobs that keep the box alive: TTS_BATCH_SIZE=1 and OPEN_NOTEBOOK_WORKER_MAX_TASKS=1. Upstream ships 5 x 5; frank-tts spawns one ~8 GiB, 8-thread engine process per synthesis request on a machine that also serves two LLMs, so a five-way fan-out is a self-inflicted shutdown. The worker knob is real, not decorative: the image's supervisord expands ${OPEN_NOTEBOOK_WORKER_MAX_TASKS:-5} into the worker command (supervisord.conf:25). Both ESPERANTO_* timeouts go above upstream's 300 s defaults, sized for CPU synthesis (SHIM_TIMEOUT=600, LLAMA_TTS_THREADS=8) — one episode is dozens of per-line /audio/speech calls and a timeout kills the episode, not the line. API_URL is forced for the 5055 -> 5056 remap; SURREAL_URL addresses the database by container name; CORS_ORIGINS is origin-form (scheme://host:port), the form the app compares against. OPEN_NOTEBOOK_PASSWORD stays absent here on purpose: the app treats a set-but-empty value as auth-on. Secrets stay placeholders in the tracked file and in dankclaw/.env.example, which lists SURREAL_PASSWORD rather than SURREAL_PASS — the deployer derives the surreal CLI's spelling from it when it seeds, so the app and the database cannot be handed different values.