ADR 0220: Clankie has one present tense
Status: accepted (James, 2026-10-04). Presence and desktop expressions are implemented; native desktop acceptance remains with the private app. Its first consumer is the desktop pet in the private app (clankie-app ADR 0059, "Clankie lives on the desktop"). The retired menu bar (ADR 0125) stays retired under ADR 0203. Design and playable prototype: https://claude.ai/artifact/HiRjtPWAjaqJLFGxo4doa3.
Context
What Clankie is doing right now is spread across separate reads: captain lanes, Discord presence, live play activity, fleet seats and pending approvals. A client that only wants to know whether he's thinking, talking, playing, leading or waiting on the owner has to poll all of them. Some of those reads also don't pass through the relay, so a remote or hosted client can't reach them.
The desktop pet needs exactly that answer, and so do the TUI header and
clankie status. The pet also needs a body that he can drive himself, plus art
that matches the logo.
Decision
One operator operation: presence. It's a strict, cursor long-poll read
in packages/protocol, shaped like fleet. It returns:
mood:thinking,in_voice,playing,leading,needs_youoridle- a short
detail since- the observed working-worker count (the existing
activeSeatswire field) - the pending owner item, if there is one
The service derives it from the existing sources and adds no persisted presence state. It
joins the hosted operator allowlist, so device clients reach it through the
relay with their device bearer. The TUI and clankie status read it too.
"Unreachable" isn't a mood; a client infers it when the relay or service
doesn't answer, and has to show it differently from sleep. Sleep is the
client's own idle presentation of idle.
A desktop tool for his volition. It lets Clankie emote, move, or say a
line in a bubble on a desktop body when he decides to. It goes out through
presence as a transient expression. Clients show bubbles without taking
keyboard focus, let them fade, and respect owner quiet hours and macOS Focus.
Brief fleet beats are optional metadata (VUH-1680). includeBeats: true
adds at most two {id, kind, at} cues: the latest completed new hire and the
latest confirmed delivered/read worker report accepted within ten seconds.
Failed hires, resumed seats and uncertain reports do not cue the pet. Report
content, worker pane IDs and conversation IDs remain outside this projection.
The hire cue is process-local; restart creates no historical hire event.
Quiet hours suppress these cues. Legacy reads omit them and retain their
existing cursor; opted-in cursors change when a cue arrives or expires.
The desktop consumes each ID once while visible. It also observes a new owner question, a crossing to four active seats and thirty seconds of uninterrupted visible thinking. Those select brief wait, conduct and wilt art. A five-second minimum gap and no queue keep bursts sparse. First reads and resurfacing establish a baseline, and Focus, Reduce Motion, hiding, locking, dragging and unreachable transport consume events without replay. Leading and waiting retain their screen expression after the body settles.
Hero art lives in branding/pet/. The art is hand-authored pixel
animation on the logo's grid: idle, blink, glances, walking, hop, the sleep
cycle, think, talk, play, alert, happy, catch, offline, and worker minis with
a tint mask. It's built from editable text sources into an Aseprite-format sheet
with per-frame timings. Motion feel, such as the spinner easing in think, lives
in those timings, so every client plays it the same way. No frame comes from a
Codex pet.
Concurrent activity labels are opt-in (VUH-1754). includeActivities: true
adds an optional activities array of at most three {label, kind, since}
items. Labels are at most 80 characters and come from fixed public activity
facts: working, leading workers, voice, play, and waiting for the owner. Raw
prompts, tool inputs, conversation titles and credentials never enter them.
Unknown start times stay null; polling does not invent a fresh timestamp.
Legacy requests omit the array, preserving strict older snapshot readers.
A working local native captain also projects thinking rather than idle.
The app rotates authored work loops on its own visible sprite clock. Every busy mood has at least three poses; a seeded tour avoids adjacent repeats, finishes whole loops, and holds ordinary work for roughly eight to fourteen seconds plus the final loop remainder. Short directing fidgets finish once. Reduce Motion uses the mood's first static pose.
The seven new work loops preserve their authored eye directions by skipping
routine working/voice face overlays; error, needs-you and new-message overlays
still convey explicit attention signals through validated screen geometry.
The activity strip consumes these labels, falls back to detail on older
services, and hides for idle or unreachable presence. Its pixels are excluded
from the native mouse mask;
visible hover controls retain their own interactive rectangles below it.
Working seats and error faces (VUH-2158). activeSeats and "Leading N
workers" count only workers whose observed status is working, across local
and remote fleets. Idle, resting, done, blocked and unknown seats are excluded;
an assignment or registered pane does not establish current work. All clients
consume the same projection. The crossed-eye face_error (x_x) comes only
from a recent failed turn in a private owner thread, lasts at most thirty
seconds, and clears on a successful turn in that thread. It is independent of
worker count. A pending owner question takes face precedence. The private app's
pet art README carries the complete screen-state table.
Alternatives
- A separate HTTP presence route. Rejected. It wouldn't reach hosted bodies through the relay, and it would split the operator contract.
- A standalone Swift pet in this repo. Rejected (James). The private app's macOS shell already has the chat, history, work and pairing UI the pet needs.
Consequences
presencereaches the private neighbors throughpackages/protocol.- The pet only ships with the app. Public installs get the art and the operation, so any client can draw him.
- The app's Connect screen also moves to this art.
