The first build pushed nothing: `docker login git.ffaerber.duckdns.org` came
back "unauthorized". Gitea scopes the automatic Actions token from the job's
permissions block, and without one it has no package write -- so the build
succeeded and the push failed on auth, which reads like a credential problem
rather than a missing declaration.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CYFnHida4AdiNDek2pzgXx
This repo now lives on git.ffaerber.duckdns.org and its image is consumed by the
homelab swarm, so the build moves with it: .gitea/workflows/docker.yml builds on
the homelab runner and pushes to this Gitea's registry as an immutable short-SHA
tag plus :latest.
Build-only, on purpose. The GitHub workflow it replaces also SSHed into the
Hostinger VPS and ran `docker stack deploy` on every push to main, which
deploys the machine being retired and makes a deploy a side effect of a push.
The homelab pattern is a PR bumping the tag in ffaerber/homelab, so shipping is
a reviewed commit.
Plain `docker build` rather than buildx: the runner shares the host daemon
through a mounted socket, and buildx's gha cache backend does not exist outside
GitHub.
.github/workflows is removed from this copy so Gitea does not also try to run
it -- it would log in to ghcr with a Gitea token and SSH to a host whose secrets
are not here. The GitHub repository keeps its own copy untouched.
uploadChunk only wrapped withBatchRecovery (402), so a Bee 429 —
returned when concurrent uploads hit the same postage batch (multiple
api replicas + worker) — failed the whole completion instead of backing
off. Wrap the upload in withRateLimitRetry so a transient throttle
becomes a slightly slower success.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TYhvtQe5q6SLAmcCaUk9HD
Bee 2.8's pushsync from homelab to prod consistently routes our trojan
chunks to neighbour ff7adaed instead of prod_bee at ff896a93, even with
both nodes directly peered. The chunk lands at a third-party node that
can't decrypt it, so the gateway never receives provider ack/deliver
envelopes and the chat times out.
Workaround: gateway advertises a clientReplyUrl in job_notify. Provider
POSTs the same signed envelopes there; gateway verifies the signature
(same trust check as the PSS subscription) and dispatches via the same
onEnvelope handler. PSS remains the fallback when no replyUrl is set
(non-hosted clients).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bee closes the pssSubscribe WebSocket after ~8s of idleness (or on any
transient hiccup). The previous handler only logged the close — so the
gateway silently went deaf to every ack/deliver envelope that arrived
after the first close, breaking every chat whose provider takes more
than a few seconds to respond. Re-open the subscription on close with a
short delay; cancel() now blocks further reopens.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The 1M-token headroom (ported from t4t homelab gateway) makes maxPayment
so large that JobEscrow's concurrency cap fires on a fresh provider
with ~70 xBZZ stake — postJob reverts with InsufficientStakeForJob.
50k tokens still covers chat-template overhead with room to spare, and
brings per-job exposure down ~20x so ~20 concurrent jobs fit comfortably.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two bugs that together blackholed every reverse-PSS message from the
provider when the api ran more than one replica:
1) `pssKeys = generatePssKeypair()` ran per-container, so each replica
had a different ECIES identity. Provider encrypts its reply to ONE
of those keys, only that replica can decrypt — so two replicas =
~50% of chats hang forever.
2) `resolveSelfOverlay` swallowed errors and returned 0x00…00 when
Bee wasn't ready yet at boot. That zeroed overlay landed in the
gateway context AND in the job_notify envelope sent to the provider
— provider then sent PSS replies into the void.
Fix: load PSS key from PSS_KEY_PATH (shared volume across replicas);
retry getNodeAddresses up to 5 minutes and throw if still zero.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous formula computed maxPayment from the estimated input length
plus offering.maxContextTokens — but the on-chain offering for some
provider models has maxContextTokens=0, so output cost was 0 and the
posted maxPayment only covered the (under-estimated) input side. The
provider's claimJob then reverted with PaymentTooHigh, leaving the
gateway hung waiting for a PSS job_deliver that never came.
Match the t4t/container homelab gateway: assume both prompt and
completion can be (max_tokens + 1M-token headroom), budget at the full
split rate, let the JobEscrow contract refund the unused portion. The
provider's actual claim is always smaller, so PaymentTooHigh can't fire.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
swagger({ path: '/' }) renders the UI at root but doesn't register the
/json spec route, so the embedded UI loads then 404s on its own spec.
Keep the plugin defaults and bounce / → /swagger instead.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Was only building + pushing; swarm kept running the previous image
until someone ran service update --force. Mirror the frontend workflow:
ssh in and `docker stack deploy --resolve-image always` so every main
push rolls out.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Was at /swagger by default. Apex 404 was confusing — there's no other
content on the api host, so make the API docs the default landing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>