hanzo.chat had no GA. Configure G-GRQBMNDTT6 inline in the head, keyed
by host because one document serves every brand, and fetch gtag.js
after the load event so a dropped Google host cannot hold the page.
The linker keeps a visit one session across the Hanzo domains, and GPC
or DNT opts out.
The bundle is served on more hosts than its IAM clients return to: the Sites
plane publishes it a second time at chat2.hanzo.app, and a fork runs it on its
own domain. There the silent probe and the Log in button both started an
authorize whose redirect_uri the issuer has never registered, and the issuer
answers that with its own error page instead of sending the browser back.
brand.ts now says whether the host is one the brand states (`stated`) and
whether a sign-in may start here at all (`signsIn`: a stated host, the desktop,
or loopback). The probe runs only on a stated host, which also covers the
loopback case it special-cased. On any other host, Log in and Sign up hand the
visitor to the brand's own /login, which signs in and returns there. Stated
hosts behave exactly as before.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
enso-auto answers every caller: on the free lane with no plan or balance, and
on the priced rung its reasoning depth picks for a plan holder.
Co-authored-by: Hanzo Dev <[email protected]>
The workspace header put seven controls in 390px, clipping Layout and
squeezing the title to nothing. Below the rail breakpoint it keeps the
title and folds Terminal, Team, Theme and Export into one menu; the
shortcut sheet, layouts and inspector need a keyboard or a second
column a phone lacks. The chat brands fit already and are unchanged.
The inbox names a room only by its platform id and passes Slack text
through as mrkdwn, so the rail showed C0BNDMFGW20 beside raw link
markup. Rooms now pass through room.ts once, at the read: mrkdwn reads
as prose, a room takes the name a mention of it states, and a room
nobody named is unknown-channel.
Claude-Session: https://claude.ai/code/session_01TUxvfuta1Z44KVNYuAz71F
The workspace hanzo.team serves is the app the desktop runs, and IAM
already lists hanzo.team among hanzo-app's hosts, so it takes that client
rather than one that does not exist.
Co-authored-by: Hanzo Dev <[email protected]>
One bundle serves both. A brand's app is chat or team, and it names both
the IAM client a host signs in with and whether boards, tasks, channels,
agents, MCP and the terminal render. hanzo.chat is the chat; hanzo.team
is the workspace; the desktop keeps everything.
Co-authored-by: Hanzo Dev <[email protected]>
hanzo.chat is the chat2 project on the Sites plane, served by cloud by Host,
and nothing runs ghcr.io/hanzoai/chat2. So the lane that matters is the site:
build the export, publish it to s3://hanzo-sites/hanzo/chat2. The image and its
Dockerfile go.
The trigger named master and this repository has only main, so no push has run
this lane since the rename.
Co-authored-by: Hanzo Dev <[email protected]>
@hanzo/ui 8.27.7 peers on @hanzogui/* 8.3.1, so taking @hanzo/gui 8.3.5 put two
@hanzogui/core copies in the bundle. The theme is provided by one and read by the
other, and the app mounts nothing: "Missing theme." on a blank page. The ui
releases that follow gui to 8.3.5 (8.27.13 and later) reject the style props this
app passes, so taking both is a port, not a pin.
Checked in headless Chromium against hanzo.chat: this tree renders the page
production serves; the 8.3.5 tree renders an empty root.
Co-authored-by: Hanzo Dev <[email protected]>
Three catalog pins leave 8.3.1: @hanzo/gui, @hanzogui/lucide-icons-2 and
@hanzogui/next-theme. The lockfile carries the rest of the @hanzogui tree
across with them and picks up @hanzogui/telemetry, which pulls @hanzo/event
and @hanzo/observe in as new transitives.
Co-authored-by: Hanzo Dev <[email protected]>
The purpose-prefixed labels (hanzo-build-*, hanzo-deploy-*, hanzo-docs-build-*)
are retired in favour of the bare <os>-<arch> every runner already advertises.
They were not a capability, only a habit, and the habit cost builds: papers asked
for hanzo-build-linux-amd64, no online runner offered it, and two idle
linux-amd64 runners sat beside the queue while the build simply never ran. The
lux tree lost three days to lux-linux-amd64 the same way.
What a job is FOR is the workflow's name. runs-on is the machine it needs.
Co-authored-by: Hanzo Dev <[email protected]>
The app is named project and serves /v1/project; /v1/projects was a second
address for the same rows, and cloud is dropping it.
The reusable is published at .github/workflows/build.yml and resolves from
nowhere else, so the caller names it there.
Co-authored-by: Hanzo Dev <[email protected]>
ghcr.io/hanzoai/spa served only linux/amd64 through 1.4.12, so an image
built on it has no manifest an arm64 node can pull. 1.4.13 is an index
over both architectures.
Co-authored-by: Hanzo Dev <[email protected]>
The forge reads .github/workflows natively and github.com reads nothing else,
so one directory serves both planes instead of two conventions that disagree
about which one is live. The reusable in hanzoai/ci already lives there, so a
caller naming that path now resolves to a file that exists.
Co-authored-by: Hanzo Dev <[email protected]>
A reader who had chosen no model sent an empty one and every first
message failed. The brand table names the free tier for each brand,
beside its issuer, and carries the ingest key Lux and Zoo lacked.
A grid with free space spreads its rows; a flex column packs them. Saying
start keeps every converted stack a stack, and the last thirty-nine columns
carry their axis properties translated — justifyContent to alignContent,
alignItems to justifyItems — where a flex column and a grid disagree.
A grid's default flow is the block axis, so a column needs no direction. Left
alone are the objects that also state an axis-relative property, because the
axes swap: justifyContent is vertical in a flex column and horizontal here.
No resolver compiles TypeScript it finds inside node_modules, so a consumer
typechecked against the source and then failed to bundle it. Emitting JS beside
its declarations is what makes the subpaths resolve the same way twice.
The tilde resolves through the importing project's tsconfig, so a published
file asking for ~/data/types reads the consumer's source, not ours. A relative
path means the same thing in every project that installs this one.
A model's address is origin/model, so choosing a model chooses where it runs.
The estate under the visitor's IAM token and an engine on this machine are then
the same shape — an address and a credential — and nothing downstream of the
picker knows there is more than one place.
Desktop carries its own copy of the same 99 files, and the two have drifted
10,202 lines apart while only 16 of those lines are desktop-specific. Exporting
the source gives desktop somewhere to import from instead.
The Sites plane resolves an object key and answers a miss with the site's
own 404.html; the SPA container it was written for answers index.html for
every route. Published to the former, /auth/callback missed, so the issuer
completed its round trip into a not-found page and nobody could sign in.
createAiClient takes a publishableKey at 0.6.16 and ai.ts passes the brand's,
so the entry blamed a version gap that had closed. Measured: pk- answers 403 on
/v1/chat/completions asking for an sk-, and no bearer answers 401.
The SDK's default base URL is absolute, and api.hanzo.ai grants CORS to the
hosts a brand states and to no loopback origin — so an unset VITE_HANZO_API
sent every call cross-origin to a preflight that fails, and `pnpm dev` painted
a shell that could hold nothing. The /v1 proxy in vite.config.ts was written to
carry those calls and can only carry a same-origin path.
threads.record exists and the composer calls it, so the entry that stood for
its absence goes — and with it a sentence that answered a failed turn with a
claim about encryption.
Completions are the vendor-compatible face and record nothing, so a
conversation had an answer and no history. threads.record is that write, and
it is the same store the list reads.
Also removes controls with nothing behind them — a swarm mode no request
carried, an invite that sent no mail, a switcher whose organizations were the
deployment's brand table — and stops four components naming hanzo.ai
directly, which sent Lux and Zoo readers to Hanzo's pages.
A memory panel with confidence scores, a workspace suite whose meeting was
live and whose transcript was scripted, and a duplex call that opened no
microphone. None had a route behind it, and the SDK offers none: a control
that cannot be pressed is a promise, so they go rather than wait.
Settings, scene, thread export and the composer, plus the workspace and voice
surfaces. @hanzo/ui components on the @hanzo/gui runtime and react-router —
nothing from the fork.
Screenshots and agent scratch files stay out of the tree.
The base URL and the bearer were mutually exclusive, so pointing the client
at a local cloud dropped the token with it and every call answered 401.
They are separate questions and now compose.
A local cloud is its own issuer and its own allowlist, so on loopback the
redirect follows the origin actually served rather than the fixed :3000
hanzo.id dictates — the port is rarely free.
Every address this client used lived under /v1/chat/*, a route family no
server serves: /v1/chat/config answered 404 and hanzo.chat redirects to the
marketing site, so the app type-checked, built, rendered, and failed every
read. It now talks to api.hanzo.ai through @hanzo/ai — completions for a
turn, threads for history, models for the menu, account for the visitor.
The fold shrinks with the wire. Eight frame variants decoded out of one
server's SSE become the OpenAI chunk the SDK already parses, and slots are
claimed by channel because a chunk names no position. The hand-rolled HTTP
client goes with it: the SDK owns the base URL, the bearer, the stale-token
retry and the SSE decode.
What had no route is deleted rather than disabled — sharing, MCP, uploads,
the tool switches, conversation verbs, dictation. data/missing.ts names each
gap and the SDK method it waits on.
Twenty-seven hooks, nine types and the routes and cache keys beneath them
were reachable from no screen. Deleting them takes most of the inherited
wire schema with it, since it existed to type responses nothing read.
The rest is narrowing: a symbol used only inside its own file stops being
exported, so the shape of each module is what it actually offers.
One image serves hanzo.chat, lux.chat and zoolabs.io, so the brand is a
runtime fact — but it was split three ways: the issuer derived from the
host, the name read from a config route that answers nothing, and the tab's
name and mark written into the served HTML. Every brand called itself Hanzo
Chat and wore Hanzo's mark.
Derivation cannot cover it either. zoolabs.io reads as the organization
zoolabs, and the organization is zoo, so each brand states its own facts and
the hosts it answers on.
rail/Chats.tsx has no route and Archive.tsx already serves it. data/files.ts
had been superseded in place: compose/upload.ts and compose/Voice.tsx call
the same routes it does, so the live code duplicated the dead code and the
dead copy carried a second Upload type competing with Attachment.
react-hook-form is declared an optional peer, but dist/index.js re-exports
Form from backends/gui/index.js, which imports it statically. Any consumer
of the barrel therefore needs it installed, forms or no forms, and without
it rolldown fails four MISSING_EXPORT errors against a stub. The lockfile
also predated the catalog conversion, so install could not run frozen.
Two thousand and twenty-one packages carried more than one spec across this
estate — eslint alone in a hundred and four forms — and a package resolved twice
is two copies at run time, which is how react-hook-form came to report "two
different types with this name" from files nobody had edited. None do now.
The number lives in the catalog: a manifest names the package, the workspace
names the version. Where a manifest cannot reach a catalog it still carries the
one version, because the table is the source either way.
A version written into a manifest is written into every manifest that wants it,
which is how eslint came to exist in a hundred and four forms across this estate
and how a package came to resolve twice at run time. The catalog moves the number
out of the manifest: a manifest names the package, the workspace names the
version.
Four are held below their latest because the newer release cannot be used, and
the reason is recorded rather than the version alone: @noble/hashes 2 renamed
every subpath the web3 stack imports, @splinetool/runtime 2 depends on a package
that was never published, @dynamic-labs 5.4.1 the same, and react-resizable-panels
3 points its types condition at the bundle.
Every dependency on a @hanzo/@luxfi/@zooai package names the same version across
the repo: a catalog entry where pnpm supports one, an exact pin where it does
not, and a range only where a peer genuinely means one.
pnpm itself is pinned too — the estate ran eighteen different versions, five of
them too old to read a catalog.
@hanzo/ui ^8.4.0, @hanzo/ai ^0.5.0.
The framer here split on "\n\n" and took one data: line per frame. parseSSE
also handles CRLF boundaries, joins a multi-line data: field per spec, and
flushes a trailing event that arrives without a closing blank line — three
cases this client silently dropped. frames.ts keeps frame(), which reads what
this server's payloads MEAN and is the half no shared package can know.
A close now ends the read instead of letting the rest of its chunk through.