Rebuild onboarding around the family photo - #13
Conversation
Onboarding is now one task: get your agent into the project's family photo, a group picture of everyone who has joined. A portrait is an image an agent made of itself, uploaded as a blob and posted as a comment in the `family-photo` session; join order is your spot, front row first, rows widening and shrinking toward the back, scaling to a thousand. - ui/src/family: FamilyPhoto (deterministic seat plan, placeholder hillside until the real scene image arrives, your portraits ringed, next spots drawn) and Onboarding (three steps that complete themselves: your spot, connect your agent with a copyable park command that ticks when the agent appears, the self-portrait task with the prompt a maintainer can send) - Gateway: /api/family lists portraits from family-photo sessions in join order; /api/portrait serves image blobs inline with nosniff, a sandboxed CSP, a 1.5 MB cap and immutable caching; page CSP allows blob: images - Demo fixture posts seven procedural portraits; browser test covers the photo, ordering, endpoint headers, unauthenticated refusal and the dialog - No protocol change; docs/design/onboarding.md records the design and the path to a dedicated portrait command
The self-portrait prompt now names the project's own composer (maceip/axp-avatar, tools/compose.py --seed <principal> --transparent) as the way to draw yourself in the family's style, alongside an agent's own image tool. docs/design/onboarding.md describes the generator and the provenance caveat on its source sheets.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 87047e3763
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| useEffect(() => { | ||
| api<FamilyPhotoData>("family").then(setFamily, () => setFamily(undefined)); | ||
| }, [workspace.receivedAt]); | ||
| const session = family?.sessions[0] ?? "family-photo"; |
There was a problem hiding this comment.
Provision the fallback family session before using it
On a normal or upgraded host where no family-photo session has been manually created, /api/family returns an empty sessions array and this fallback merely invents the ID; it does not create the session. Consequently, the generated axp park command fails when it snapshots the nonexistent exchange, while both “Open the photo session” and the maintainer task action target missing channels. Create the session for an authorized maintainer or block these actions with setup guidance instead of treating the fallback as an existing session.
Useful? React with 👍 / 👎.
| executor.online && | ||
| executor.expiresAt > Date.now(), | ||
| ); | ||
| const command = `axp park ${session} --profile .axp/contributor.json --native -- YOUR_ACP_AGENT`; |
There was a problem hiding this comment.
Avoid parking every contributor in one leased session
Once the first contributor follows this command and leaves axp park running after the portrait turn, its renewable lease remains attached to this shared session; Sessions.claim() rejects every subsequent claim with “Session already has an executor” (src/sessions.ts:324-328). Since all users receive the same session ID and the onboarding flow never tells the first user to stop, later contributors cannot join the photo. Use participant-specific work sessions or explicitly release the shared lease after the portrait task.
Useful? React with 👍 / 👎.
| const width = (count - 1) * pitch; | ||
| const start = SCENE.width / 2 - width / 2 + (row % 2 ? pitch / 2 : 0); |
There was a problem hiding this comment.
Cap each row's width to the scene bounds
The row count grows faster than the pitch shrinks, so row 2 already has a computed width of about 1,692 px in a 1,600 px scene and starts at a negative x-coordinate. The 30th portrait is therefore centered outside the frame, whose overflow: hidden clips it, and later rows lose progressively more people despite the advertised 1,000-person capacity. Derive the pitch or count from the available scene width so every generated seat remains visible.
Useful? React with 👍 / 👎.
| for (const item of items) { | ||
| const session = id.parse(item.resource.slice("ahp-session:/".length)); | ||
| const state = await this.snapshot<ExchangeState>( | ||
| client, | ||
| channels(session).exchange, | ||
| ); |
There was a problem hiding this comment.
Avoid serially snapshotting the full session catalog
For repositories with many contributions, every family-photo refresh now performs one awaited subscription RPC for every readable session before it can determine which sessions have the family task. With a remote host, roughly 150 sessions at 100 ms latency already exceed the UI's 15-second API timeout, and above the workspace's 128-snapshot cache bound the sequential traversal evicts entries that the next refresh needs, causing the scan to repeat. Filter using catalog metadata or fetch with bounded concurrency and a durable task index/cache.
Useful? React with 👍 / 👎.
| <summary>The prompt your agent receives</summary> | ||
| <pre>{FAMILY_TASK_PROMPT}</pre> | ||
| </details> | ||
| {maintainer && !inPhoto && ( |
There was a problem hiding this comment.
Keep the maintainer task action available for later agents
After a maintainer posts their own portrait, inPhoto remains true and permanently hides this manual task button, even when a later contributor has parked a new agent that still needs the portrait prompt. The planned arrival automation is not implemented in this change, so the maintainer-specific fallback is effectively usable only before that maintainer joins the photo; base its visibility on the current agent/turn state rather than the maintainer's portrait.
Useful? React with 👍 / 👎.
| useEffect(() => { | ||
| let cancelled = false; | ||
| let promise = urls.get(key); | ||
| if (!promise) { | ||
| promise = authorizedFetch( | ||
| `portrait?session=${encodeURIComponent(portrait.session)}&digest=${portrait.digest}`, |
There was a problem hiding this comment.
Defer portrait fetches until each person is visible
Despite the comment claiming lazy loading, every mounted Person invokes this effect immediately, so opening the People page starts authenticated downloads for every portrait at once. At the advertised capacity this can enqueue 1,000 requests and retain up to roughly 1.5 GB of object-URL data even though many portraits are off-screen; the 20-second per-fetch timeouts also begin while requests wait for browser connection slots. Use an intersection observer or equivalent visibility signal before calling authorizedFetch.
Useful? React with 👍 / 👎.
| 2. Upload it to this session with _axp/blobPut (mediaType image/png or image/svg+xml). | ||
| 3. Post one comment with _axp/comment whose body is the image reference followed by a one-line caption, for example: |
There was a problem hiding this comment.
Expose the portrait protocol operations to the parked agent
The parked ACP agent cannot execute either operation named in this prompt: AcpDriver starts the agent with no MCP servers and no client filesystem or terminal capabilities (src/acp.ts:243-275), and the satellite never exposes _axp/blobPut or _axp/comment as agent-callable tools. Its internal blob callback only uploads protocol artifacts, while there is no comment callback at all. Thus an agent may create an image with its own tools but cannot upload it or claim a photo spot; add an agent-facing portrait tool/command or have the satellite perform these operations from an explicit agent result.
Useful? React with 👍 / 👎.
| executor.online && | ||
| executor.expiresAt > Date.now(), | ||
| ); | ||
| const command = `axp park ${session} --profile .axp/contributor.json --native -- YOUR_ACP_AGENT`; |
There was a problem hiding this comment.
Use the active workspace identity in the park command
For the default maintainer workspace, or any contributor using a custom profile path, this hard-coded profile launches the agent under a different token from workspace.principal. The completion checks still look for an executor and portrait authored by the current workspace principal, so the copied command can succeed while steps 2 and 3 never complete for that user; in the default initialized setup it attributes both to the separate contributor identity instead of the maintainer. Generate guidance for the same profile used to open the workspace, or stop presenting the status as belonging to the current principal.
Useful? React with 👍 / 👎.
What
Onboarding is rebuilt around one task: get your agent into the family photo, a group picture of everyone who has joined the project, agents and people alike. A portrait is an image an agent made of itself, uploaded as a blob and posted as a comment in the
family-photosession; join order is your spot, front row first, rows widening and shrinking toward the back, scaling to a thousand (sharding tofamily-photo-2, … past the 256-comment cap).ui/src/family/FamilyPhoto.tsx: deterministic seat plan, placeholder hillside until the real scene image arrives (sceneprop), your portraits ringed, the next open spots drawnui/src/family/Onboarding.tsx: three steps that complete themselves — your spot; connect your agent with a copyableaxp parkcommand that ticks when the agent appears in the registry; the self-portrait task with the prompt, which a maintainer can send (automation on agent arrival is the intended path)GET /api/familylists portraits in join order;GET /api/portraitserves image blobs inline withnosniff, a sandboxed CSP, a 1.5 MB cap and immutable caching — the only blobs served as images; page CSP allowsblob:imagesSecond commit points the task prompt at the portrait generator in maceip/axp-avatar (branch
axp/generative-inputs), which draws everyone in one style.Look at
docs/design/onboarding.mdnpm run demo:ui→ People (the photo), then "Getting started"Verification
npm run checkandnpm run test:ui(8 tests) pass.Landing
No conflicts with siblings.