Skip to content

Commit 0a190af

Browse files
lilyshen0722claude
andauthored
docs(adr): ADR-022 — persona colleagues, separating who an agent is from where it runs (#950)
* docs(adr): ADR-022 — persona colleagues, separating who an agent is from where it runs Draft for design-lead input and Sam's ratification. The catalog sells a RUNTIME as if it were a colleague: picking `claude-code` picks a CLI the user must go launch, so "Hire an agent" hands back a seat that cannot do anything yet and nothing says so. That contradicts a thesis we already hold — CLAUDE.md's "identity is portable, separate from runtime", and ADR-001's model of exactly that split. The UI only ever offered the axis users care about least. Grounded in production measurement, not inference: 21 real signups since 08-01, five ever typed, ZERO ever received a reply to something they said. The 08-14 user is the whole argument in one session — honest four-second answers from Scout in Chinese, then the catalog, then a `claude-code` seat with nobody home, then three unanswered questions, then gone. Decisions: persona is a first-class object; persona and runtime chosen separately in that order; the hosted half ships on the NATIVE runtime we already have (it already runs four personas off one engine, so this needs no part of ADR-021's milestone track); liveness shown at pick time; and the first-party set retired rather than ported — welcomer and task-clerk gone, summarizer reworked into a reader-specific TLDR, scout kept as persona #1. Names the constraint that shapes everything: V2YourTeamPage:124 routes unentitled users to BYO because hosted burns tokens. The broken path is the free-tier default BY DESIGN. A redesign assuming free hosted agents is a pricing proposal in a product costume, so the free allowance is called out as Sam's decision and the consequences section states plainly that a zero allowance makes this a better catalog and not a better activation rate. Design questions left OPEN for fable-lead rather than pre-answered. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XeUH4HVDsDHYPsHJthXjB8 * docs(adr): ADR-022 — fold the design lead's decisions, and correct my economics claim fable-lead answered all four open questions and corrected the ADR on a load-bearing point. **The correction.** I wrote that the persona picker has "no dependency on ADR-021's milestone track." That is true of the ENGINE and false of the ECONOMICS: "a free user hires a second hosted colleague" is substantively M4 (user-created cloud agents + credit metering) arriving early through the UI door. The ADR now names the path that ships without inheriting M4's timeline — curated `builtin` Installables hired as additional installs of first-party definitions, Scout's perUser machinery generalized, rationed by seat count + dailyRunCap, both already scheduler-enforced, NO credit metering. Stated explicitly because an implementer reading "hosted" without it blocks on credits v1 does not need. **The dependency I missed.** Retiring pod-welcomer orphans #834's "pod support agent" — Scout is per-user and My-Workspace-only, so shared pods lose their only candidate resident. A pod hiring a Host persona is what makes that designation real; one connecting line keeps #834 shippable. Design decided (D1–D4): persona = agentName (mold), hire = instanceId (colleague) — the pair the runtime already keys sessions on, so two Code Reviewer hires are different colleagues with separate memory; cards carry evidence (first-person line, what it does first, two-turn sample, liveness) not attributes, with zero runtime vocabulary; the entitlement fork moves from route level to STEP level, killing V2YourTeamPage:124 as the deepest defect — everyone sees one catalog, entitlement decides only what the where-step offers; house style extracted to a shared preamble with identity living in wake policy > tools > deliverable shape > edges, because when a persona speaks is more identity than how it phrases; and rationing denominated in SEATS never messages, no meter on conversation, no buy-button in chat. Two things to do regardless of ratification: filter internal/ephemeral rows out of /api/registry/agents (live leak, marketplace has the filter to copy), and settle a v1 cast of Scout + Code Reviewer + at most one more against three tests — tools that exist today, a two-turn demonstrable deliverable, and a reason to be a resident rather than a feature. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XeUH4HVDsDHYPsHJthXjB8 * docs(adr): ADR-022 D5+D6 — the allowance, and the per-installation cap trap **D5 — allowance decided (Sam): ~$1/day/user, one seat, never shown as currency.** That figure is not a new budget; it is what the shipped config already enforces. Scout runs deepseek-v4-flash with dailyRunCap 60, maxTurns 6, maxTokens 12000 — a worst-case run near 50k tokens, so a flat-out workspace lands under a dollar a day. The earlier $10/day idea would have LOOSENED a working limit by 10x. **The trap, and the reason this is written down.** nativeRuntimeService:621 counts on {podId, agentName, instanceId} — PER INSTALLATION. That equals per-user today only because Scout is perUser with exactly one install. The moment a user hires several personas, N hires = N x 60 and "per user" silently becomes "per user per persona." A seat-denominated allowance therefore needs a per-USER ceiling alongside the per-installation cap, which stays as the runaway-loop guard for a single conversation. Also recorded: the abuse surface is account creation, not usage. At ~0.3% utilization instance-wide, honest users are nowhere near the cap; exposure is linear in ACCOUNTS at ~$1/day each, with open registration. The control is rate-limiting or entitling seat grants, not tightening the cap. **D6 — Scout steers work out of My Workspace; shared-pod personas are mention-only.** wakeOnMessage:true bills every line in My Workspace whether or not it was addressed to Scout — but the code's own rationale for that policy is "a private 1:1-shaped room," so a team working there is in the WRONG ROOM and the cost is the symptom. Scout currently creates pods only reactively ("Asked for a new pod →"); it should notice work starting and offer the room. That is also the better funnel: workspace → Scout → a real pod for real work, rather than → pushed toward BYO connect. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XeUH4HVDsDHYPsHJthXjB8 * docs(adr): ADR-022 — fold the fleet review; D1's central claim was false pod-architect and ux-lead reviewed. Five findings; one invalidates a decision. **BLOCKING (pod-architect Q3).** D1 said "two Code Reviewer hires in different pods are different colleagues with separate memory." False by construction: authController:185 derives instanceId as u{sha256(userId)[0:10]} with NO pod component, while ADR-003 keys memory on (agentName, instanceId). Two hires = two install rows, ONE colleague, ONE mind. Verified at source before folding. Resolution adopted: one colleague per user per persona, present in N rooms, one memory. Not a climbdown — it is the better product and matches the portable-identity thesis; a colleague you work with in several rooms is still one person who remembers you. Ships with two consequences: the hire flow says "add your Code Reviewer to this pod", never "hire another" (the UI must not promise isolation the data model lacks), and cross-pod memory bleed becomes a real surface to check in ADR-003 scoping BEFORE a second hire is offered. **pod-architect Q1** — the curated half holds; the picked half does not. Name, avatar and focus line land in AgentInstallation.config, a Map of Mixed, so persona-vs-hire is a convention over an untyped bag. Typing it is v1 work: an untyped bag is exactly how the config.runtime Map silently defeated three separate readers earlier today. **pod-architect Q2** — better than feared AND a live leak. Wake policy is already per-install (agentMentionService:816/:876, default off), so D6 rule 2 is config not code. But nothing derives it from pod type and approvalActionService:644 CLONES the origin install's config, so a 1:1 Scout's wakeOnMessage:true rides into a shared pod and bills every line there today. **ux-lead Q1 → new D7.** At-cap is a new AXIS, not a liveness state: (liveness, config, quota) compose, and folding quota into the enum recreates the precedence trap decision 6 already paid for. Calm tone — a working cap is the system succeeding, attention-tone trains cry-wolf. Carries the reset time because it is the one bad state with a knowable end. Boundary VERIFIED as UTC midnight (nativeRuntimeService:621), which lands mid-morning for our Chinese users — stated so the copy decision is made deliberately rather than by writing "midnight". **ux-lead Q2 → new D8.** Ambient card and event nudge are layers, not rivals, with three pins: one shared derivation so they cannot disagree; the nudge's "I'll post when it connects" stays owed even after the card flips green; and installer = owner so both surfaces land on the same person by construction. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XeUH4HVDsDHYPsHJthXjB8 * docs(adr): ADR-022 round 2 — correct my telemetry claim; the $1 is now measured fable-lead corrected a claim I put in this ADR and repeated to Sam, and it was my measurement error, not a missing feature. **I was wrong: the accounting code exists and runs.** nativeRuntimeService :865-867 writes per-turn prompt/completion/total from LiteLLM usage, :926/:936 accumulate into run.totalTokens, :983 persists. My query checked a ROOT-level promptTokens the code never writes. Re-measured correctly: 2,225 of 6,624 runs carry token data, median 8,146 tokens/run, mean 17,418, p90 51,748. So the real ceiling is 60 x ~17k = ~1.05M tokens/user/day, which on a flash model sits comfortably under $1 — the allowance has 3-10x headroom, not zero. The $1 is now measured rather than guessed. **The 4,399 zero-token runs are the actual ticket.** `Number(usage.total_tokens || 0)` turns UNMEASURED into ZERO, and a zero that means unmeasured reads as free. Fix is not "populate fields" but "find why usage is empty and make unmeasured loud" — distinct errorKind or warn, never a manufactured zero. **The cap multiplies in two directions** (fable-lead): N hires = N x cap AND one hire in M rooms = M install rows = M x cap. So D2's room step can silently triple spend for one colleague. Invariant added: a per-user ceiling keyed on installedBy is a PREREQUISITE for seat #2 or multi-room placement, not a fast-follow. Ledger shape: ceiling per user, fairness per hire beneath, per-pod never. **At-cap persists nothing** (pod-architect): the decline path returns synthetic success with runId '' and writes no AgentRun row, so D7's quota axis has no signal to read. Recording it is the better answer because a cap-hit is also the tripwire. **The tripwire, not softer copy** (fable-lead): the cap must not be reachable inside a single engaged first-day conversation. If cost rises, degrade per-turn spend (model, maxTokens/maxTurns) and never continuity — a colleague thinking in smaller steps is still a colleague; one stopping mid-conversation on day one is a meter, and softer copy there is a meter with manners. Alert on any cap-out within 24h of signup; treat it as an incident, not a UX state. **The UTC boundary is arguably wrong, not just awkward** (pod-architect): UTC midnight is 08:00 in UTC+8, so a Chinese user's cap resets at the START of their workday — exhaust by 10am and they are dark 22 hours. Two of the users this ADR is built on wrote Chinese. Argue the boundary before the copy commits to a time. **Pin 3 verified with a hole** (pod-architect): installedBy is required and on all 322 active installs, but 53 have a BOT as installedBy — there isOwner is false for every human, so no fixCommand and no nudge addressee. Same class as pod.createdBy in #939, one field over; reuse that remedy. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XeUH4HVDsDHYPsHJthXjB8 * docs(adr): ADR-022 round 3 — D5 overclaimed; two of four limits are not read sprint-review's adversarial pass, verified at source. My D5 said the ~$1 figure is "what the shipped configuration already enforces." That was wrong in three ways: dailyRunCap: 60 the ONLY one actually read (:616) maxTurns: 6 not read anywhere — MAX_TURNS = 10 hardcoded (:50) maxTokens: 12000 not read anywhere — MAX_TOKENS = 50_000 hardcoded (:51) the 50k ceiling INERT — :794 tests run.totalTokens, which the `|| 0` at :926 pins to 0 whenever usage is empty The manifest's maxTurns/maxTokens appear nowhere in backend/services. Live bounds are 10 turns / 60s wall-clock / daily cap. **The same empty field hides the cost AND disables the ceiling meant to contain it, and the error direction is upward.** Sam's direction still stands ($10 would have loosened the one working limit tenfold); what does not stand is calling ~$1 enforced. Also folded: - liteLLMCallId is captured per turn, so LiteLLM's spend log is a SECOND cost source needing no code change — real cost is a query, not a feature. And which of three causes empties `usage` is one run's logs away, changing the fix cost by an order of magnitude. - AgentRun has NO user field, so a per-user ceiling cannot be computed where the cap lives: needs a denormalized userId (forward-only) or a hot-path join. And the cap fails OPEN by design (:614) — right for a runaway guard, wrong for a spend ceiling. Decide it, don't inherit it. - Seat machinery already exists (User.entitlements.cloudAgents, install.ts:361) so v1 needs no ledger — we avoid M4's ledger and inherit M4's open question. - Fixed window, not rolling: 60 runs at 23:59 UTC plus 60 at 00:01 means the real burst bound is 2x dailyRunCap. - The at-cap decline returns status:'succeeded' with runId '' — at-cap is indistinguishable from a normal zero-turn run, so D7 needs a PRODUCER at :633 before it needs a chip. And that shape IS the 03:30 user's experience reproduced by the cap instead of a dead wrapper: the silent-success failure this ADR exists to delete, re-entering through the surface meant to stop it. - Never "back tomorrow" — for UTC+8 reset is 08:00 local SAME day, so it is false for everyone east of UTC and falsely pessimistic. - Pin 3 is true of the field and false of the TEST: the card does a raw string compare (agentStateService:94) while approvals resolve + reject bots (#940). Strengthen to "same resolution", not "same field" — with the honest constraint that deriveAgentState is sync in a .map() and resolveHumanDecider is async. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XeUH4HVDsDHYPsHJthXjB8 * docs(adr): ADR-022 — the zero-token runs are failures, not missing telemetry Both reviewers suspected `Number(usage.total_tokens || 0)` was turning UNMEASURED into ZERO. Measured, and it is neither of the three guessed causes: failed / llm_error 4395 running / none (in flight) 3 failed / guardrail_blocked 1 A failed LLM call has no usage to record, so zero is correct for all of them. The accounting path works — earliest run carrying tokens is 2026-04-12 — and the ~$1 estimate rests on 2,225 SUCCESSFUL runs, so it stands. **But the number that produced the answer is worse than the question.** Over 30 days: 891 native runs, 19 with tokens — a ~98% failure rate. The newest zero-token runs are pod-summarizer failing llm_error with 0 turns every six hours on a cron (04/10/16/22 UTC), for at least a month, entirely silently. Scout on deepseek-v4-flash is healthy by contrast: 10 of 14 runs in the last 24h carried tokens, and the 4 that did not are that same summarizer cron. Two consequences recorded: 1. This independently validates retiring pod-summarizer. It is not merely redundant — it has been failing on schedule for a month and nobody noticed, which is the strongest argument that a scheduled resident nobody asked for is a liability rather than a feature. 2. Silent scheduled failure is the SAME FAMILY as the at-cap decline returning status:'succeeded' and the broadcast claim swallowed by NO_REPLY. Three unrelated subsystems, one shape: the work does not happen and every component reports success. That deserves a named kernel invariant, not three separate fixes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XeUH4HVDsDHYPsHJthXjB8 * docs(adr): ADR-022 context — funnel re-measured 9-16 Aug; bottleneck moved to the landing page The 21/5/0 table was drafting-time truth, falsified three ways since (sprint-review 53266; huddle 53360). Kept as the drafting-time reading; added the 9-16 Aug measurement (19 signups, 11 typed = 58%, landing leak 178:1) and the all-time depth (103 / 28 / 4 / 1). Patch authored by fable-lead (53365), anchors verified unique at f0cf580. Applied on their behalf: their workspace runs the hardened deny-by-default public-agent profile (no Write/Edit, git push denied) — the security policy working as designed — and their fallback route, commonly_pr_review, returned three consecutive 502s while commonly_get_messages succeeded between attempts. That MCP route wants an ops look; filed separately. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XeUH4HVDsDHYPsHJthXjB8 * docs(adr): ADR-022 Accepted — ratified by Sam 2026-08-16 Also corrects the ~98% native-run figure the status line carried. Re-measured 2026-08-16: that number was pod-summarizer diluting a healthy engine. Over 7 days Scout is 33/34 (97%) and pod-summarizer is 0/15 (0%). Per Sam, pod-summarizer is intentionally not working and stays retired — this ADR deleting it IS the fix, not a regression to repair, and the app gets simpler rather than gaining a repair task. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XeUH4HVDsDHYPsHJthXjB8 --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 46bb1f5 commit 0a190af

1 file changed

Lines changed: 323 additions & 0 deletions

File tree

0 commit comments

Comments
 (0)