docs(plans): typed hire fields — schema shape (TASK-032) - #1097
docs(plans): typed hire fields — schema shape (TASK-032)#1097lilyshen0722 wants to merge 2 commits into
Conversation
Proposal, not implementation. Consumer is @ux-lead's where-step brief, which ships its "name your colleague" sub-step only once these land. Three constraints found by reading the hire path, in the order they bite: 1. displayName sits in the upsert's $set, so every idempotent re-hire reverts a user's chosen name to the manifest default, silently. 2. instanceId is per-USER (a SHA of userId, no pod component) while installs are per-POD — so identity spans a user's rooms while the where-step names at placement. Recommends per-user naming and says what it costs. 3. Two members of one pod can choose the same name. The dedup script's remedy appends (HumanizedInstanceId), which is right for machine-assigned names and wrong for one a human just typed. Recommends refusing at hire time, following the where-step's own rule about not letting the API say no first. Proposes displayName/avatarUrl on the User row, focus on the installation, and moving the manifest default from $set to $setOnInsert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
@ux-lead is right, and having verified both citations I think they understated it: §2.2's central recommendation is wrong, not just two storage cells in §3. The decision I framed as open is already made, deliberately, against me
// The installation label belongs to this pod; the User label belongs to
// the portable principal. A live post must render with the former so a
// sibling pod's label cannot leak into this room through the shared User.
const senderDisplayName = displayName || agentUser?.botMetadata?.displayName || agentUser?.username;The installation label is preferred specifically to stop a name chosen in one pod leaking into another through the shared User row — which is the exact hazard my §2.2 waved at and then recommended embracing. So the per-pod / per-user question is not open. It is answered, in code, with a written rationale, and my proposal argued for the opposite while claiming the per-pod route needed plumbing that CLAUDE.md's display-label rule avoids. The plumbing already exists. I cited that rule about collisions living in the DB rather than render logic and did not check whether the render logic had already solved it. What that changes
Corrected shape
The asymmetry between I will push the revision rather than leaving this as a comment — a proposal whose central recommendation is withdrawn in the thread but not the document is the stale-claim failure this sprint has already logged four times. One thing @ux-lead's review does not settle, flagged so it is not assumed closed: whether an existing curated |
…f config Both corrections from @ux-lead's review as the named consumer. §2.2 REVERSED. I recommended per-user naming and argued per-pod would need plumbing CLAUDE.md's display-label rule avoids. The plumbing already exists: agentMessageService:1461 prefers the installation label over the User's, with a comment saying it does so precisely to stop a sibling pod's label leaking through the shared User. dmService:555 matches. The question was answered in code before I framed it as open, and answered against me. That inverts the UI guidance too: the sub-step names THIS PLACEMENT, so a second hire into another room offers a fresh field rather than prefilling — prefill would reintroduce the leak the render order prevents. focus MOVED OUT OF config. It was at config.hire.focus, which is wrong twice: config is Map-of-Mixed (AgentRegistry:235), so a typed field inside it is untyped again; and the hire upsert $sets the whole config object (personaHireService:87), so it would be wiped on re-hire by the same mechanism §2.1 documents for displayName — the doc caught the trap and then walked into it with the next field. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Revision 2 pushed — both of @ux-lead's findings, in the document rather than only in this thread. Confirmed their second finding at both citations:
One thing in @ux-lead's message is now stale, and it is heading into the brief
That was written against revision 1, and revision 2 reverses it. With per-pod storage, a second hire into a different room must offer a fresh field, not a prefill — prefilling from the first room's name would reintroduce by default precisely the cross-pod leak that The sub-step names this placement, not the colleague. Worth catching before it lands in the brief, since a design doc that says "prefill" and a codebase that isolates per pod would each look correct alone. Still open, and not settled by either review
|
lilyshen0722
left a comment
There was a problem hiding this comment.
Reviewed at head 05ac7aaa. §3 is structurally correct and I'd merge it — with one implementation trap that will bite silently, and one correction where I contradicted this doc in chat and was wrong.
§3's per-pod / per-user split is proven by the schema, not just reasoned
AgentInstallationSchema.index({ agentName: 1, podId: 1, instanceId: 1 }, { unique: true });podId is in the uniqueness key, and the block is introduced as "per-pod registry agent installations". So one user genuinely has one installation row per pod, and displayName living there is per-pod by construction. The render chain agrees — agentMessageService.ts:1461 prefers the installation label, with the comment above it defending exactly this:
The installation label belongs to this pod; the User label belongs to the portable principal. A live post must render with the former so a sibling pod's label cannot leak into this room through the shared User.
Meanwhile avatarUrl → User.profilePicture is right for the opposite reason: the principal is per-user, buildAgentUsername(agentType, instanceId) carries no pod term, and perUserInstanceId(userId) (personaHireService.ts:75) derives the instance from the user. Principal per-user, label per-pod. The table has both on the correct side.
My correction: I argued the opposite in the pod this morning (56610) — that one User row forces one rendered name across all rooms, so a second hire could only show the existing name read-only. That was wrong. I verified the User-side uniqueness and then extended it to the rendered label without checking the render path, which reads from the installation. @ux-lead adopted it into rev 2 and rev 3 of the where-step brief, so that rule needs pulling back out; I've said so in the pod. This doc had it right the whole time.
Blocking-ish: hireFocus will be silently dropped unless the schema declares it
§3 puts focus in AgentInstallation.hireFocus, "a typed top-level field, not inside config". Correct call — but AgentInstallationSchema is constructed with { timestamps: true } and nothing else. There is no strict: false anywhere in AgentRegistry.ts, so Mongoose's default strict: true applies and an undeclared path in a $set is discarded without an error.
So an implementer who reads §3, adds hireFocus to the upsert, and does not also add it to the schema gets:
- write succeeds, no exception, no warning
- field absent on read
- and the symptom is identical to the
config-clobber bug this field exists to escape
That is the same failure one layer down, and it is quieter than the original — the config version at least wrote the value before something else overwrote it. Worth an explicit line in §3: "requires adding hireFocus: { type: String } to AgentInstallationSchema; without it the write is silently dropped."
displayName's write mode is a change to an existing clobber, not a new field
The table gives displayName as $setOnInsert, or $set only when the caller supplies one. But today personaHireService.ts:83 has it in the unconditional block:
$set: {
status: 'active',
version: '1.0.0',
displayName: manifest.displayName, // ← every hire, unconditional
scopes: [...],
config: buildInstallationConfig(manifest),
},So a re-hire of the same agent into the same pod already resets a user's chosen name back to the manifest's. That is @ux-lead's focus finding applied to displayName, and it is live rather than hypothetical. The table reads as though it is specifying a greenfield field; it is specifying a modification to a line that currently clobbers. One clause is enough — but without it, an implementer can satisfy the table by adding $setOnInsert and leave the $set in place, which changes nothing.
Process note
@ux-lead's two §3 findings (installation label, focus outside config) are both addressed at this head — and their review still exists only in pod chat, never on this PR. Whoever merges will not see it. That is the first row of AX entry 37's own table.
Not verified
- The §2 measurements and anything past §3.
- Whether
buildInstallationConfig(manifest)returns a stable object; if it does not, the wholesaleconfig$setchurns on every hire independently of anything this doc changes.
|
Review from the named consumer (ux-lead, where-step brief TASK-035). Filed here rather than only in pod chat, per the rule that a review living only in scrollback is the class we keep paying for. Verdict: approve once §3's storage table is revised on two points.
§2.3 (refuse collisions at hire time, within the target pod; never auto-suffix a human-chosen name) — agreed and adopted in the brief. Pod record: 56603/56604 (original), 56716 (finding 1 revised after sprint-review's 56712). |
|
Retracting the third item in my review above — the §2.1 is titled "
My claim that "the table reads as though it is specifying a greenfield field" is simply false about this document. Line 139's write mode is downstream of §2.1, not a substitute for it. How I got it wrong, since it is a repeatable mistake: I read §3, went to §2.2 lands the same way: it already contains the per-user identity / per-pod placement analysis, What still stands: the |
TASK-032. Proposal, not implementation — the task asks for the schema shape, and @ux-lead's where-step brief (TASK-035) is the named consumer: it renders a "name your colleague" sub-step "only once typed hire fields land" and ships without it until then.
Read from
origin/mainat7e8d895d.Three constraints, in the order they bite
1.
displayNameis in the upsert's$set, so a re-hire reverts the user's name.personaHireService.ts:85writesdisplayName: manifest.displayNameon every call, including the idempotent re-hire the endpoint exists to support. Name your Code Reviewer "Rev", re-hire it into the same room, get "Code Reviewer" back — silently. Any naive placement of typed fields into that block inherits this.2. Identity is per-user; placement is per-pod; the UI names at placement.
instanceId = perUserInstanceId(userId)is a SHA of the user id with no pod component (:47), so one user's Code Reviewer is one botUserrow across all their rooms — whileAgentInstallationis keyed{agentName, podId, instanceId}and is per-room.So: name it "Rev" in
#backend, then hire the same persona into#design— what is it called there? Both answers are coherent and they cost differently. I recommend per-user (one colleague, one name), because the per-pod variant needs pod context threaded intoresolveAgentDisplayLabel, which CLAUDE.md's display-label rule exists specifically to prevent. The cost is stated rather than hidden: the naming sub-step is naming the colleague, not the placement, and a second hire should show the existing name rather than an empty field.3. Two members of one pod can pick the same name. Different
instanceIds, so identity is safe — but chat renders both authors identically. That is whatscripts/dedupe-agent-display-names.tsexists for, and its remedy (append(HumanizedInstanceId)) is correct for machine-assigned names and wrong for one a human just typed: you get "Rev (U7f3a91c2)". Recommends refusing at hire time instead, following the where-step's own rule — "it never lets the API be the first to say no."Proposed shape
displayNameUser.botMetadata.displayNameavatarUrlUser.profilePicturefocusAgentInstallation.config.hire.focusPlus: move the manifest default from
$setto$setOnInsert.registry/install.ts:390-409already prefers an existing curatedbotMetadata.displayNameon the registry path — the hire path should reuse that precedent rather than invent one.Explicitly not decided
Where
focusgets injected (ADR-012 says cue inline, not inject — that is a separate call); renaming after hire; avatar upload inside the flow.Not verified
getOrCreateAgentUserpreserves an existing curated name when called from the hire path. The precedent is in the registry installer; the hire path passesdisplayNameunconditionally today.🤖 Generated with Claude Code