Repository navigation
feat(remote agents) hosted agent types and the owner's agent list - #870
Conversation
3628fee to
4ba12f7
Compare
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: 4ba12f7d5f
ℹ️ 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".
| originOf(identity.relay) !== origin || | ||
| this.listing.has(identity.pubkey) | ||
| ) |
There was a problem hiding this comment.
Reconcile after an in-flight listing write
When a local agent is renamed or repictured while its previous 30177 publication is pending, the refresh reaches this guard and skips the newer state; after the old publication settles, the finally handler only clears this.listing and never calls list again. Until another refresh or reconnect happens, the relay record—and therefore other devices—continues showing the stale name or picture. Re-run reconciliation after clearing the in-flight marker.
AGENTS.md reference: AGENTS.md:L126-L129
Useful? React with 👍 / 👎.
| private async saveHosted(pubkey: string, change: AgentChange) { | ||
| const { agent, remote, origin, binding } = this.hosted(pubkey); |
There was a problem hiding this comment.
Serialize hosted profile saves with deletion
When a user edits the name and immediately clicks Delete, the input blur can start saveHosted while removeHosted runs because profile saves do not use the per-agent exclusion. If the profile call or subsequent 30177 publication completes after removeRelayAgent, writeListing recreates the just-deleted record and leaves a ghost hosted agent. Serialize profile saves through the same per-pubkey lifecycle guard as setup and deletion.
AGENTS.md reference: AGENTS.md:L126-L129
Useful? React with 👍 / 👎.
| if ( | ||
| pubkey === binding.viewer || | ||
| this.identities.some((identity) => identity.pubkey === pubkey) | ||
| ) |
There was a problem hiding this comment.
Reject keys already present in the owner inventory
If a provider returns a pubkey already represented by a hosted/elsewhere 30177 record or an unfinished setup, this check accepts it because it examines only native identities. Setup then activates that key and overwrites the existing record's name and runtime, effectively replacing an existing agent instead of creating one. Validate the returned key against loaded and in-flight owner listings plus unfinished setups before persisting or activating it.
AGENTS.md reference: AGENTS.md:L126-L129
Useful? React with 👍 / 👎.
wesbillman
left a comment
There was a problem hiding this comment.
Four additional changes needed: preserve listings across read failures, order replacement records, exclude hosted types from native import, and retain focus after setup. Details and smallest fixes are inline. The existing in-flight listing and profile/delete race reports also remain applicable.
Star Lord automated source review via Wes’s account. Head 4ba12f7d5f2b2c34e9516e11fa440c789e2637ec; base 7e0ffeb6730396dfc357547eb3c8f5526fc3146f. Source-only: no tests, app/provider execution, or CI validation. Inspected changed source, commit metadata and PR description; no attached images in the description.
| this.edits.get(origin)?.set(pubkey, row); | ||
| const listings = this.listings.get(origin); | ||
| if (!listings) return; |
There was a problem hiding this comment.
[P2] Keep accepted writes when the initial listing read fails
After readListings fails, it removes edits without installing listings. A hosted create/Finish setup can still succeed at the provider and relay, but setRow then stores nothing; setUp deletes the unfinished entry and throws because find returns nothing. A write completed during a read is also lost if that read subsequently rejects and discards its edits. The running agent disappears from the local inventory until another successful sync. Preserve locally accepted rows independently of read success, and merge them into the next successful read; cover both failure orderings.
| const id = outbox.send( | ||
| { kind: 30177, content: JSON.stringify(content), tags }, |
There was a problem hiding this comment.
[P2] Make replacement agent records strictly newer
This send has no ordering information from the record it replaces. The outbox uses whole-second wall-clock timestamps for kind 30177; its monotonic replacement handling covers only 30175/30178 (catalog-envelope.ts). Two quick name/picture updates can therefore share a timestamp, and newer selects the lower event ID rather than the last edit. writeListing nevertheless caches the last write as current, so reconnect/another device can restore the older value without reconciliation correcting it. Retain the observed record timestamp and ensure each 30177 replacement advances beyond both observed and local writes; add same-second replacement coverage.
| const kind = this.types().find((entry) => entry.key === type); | ||
| if (!kind) throw new Error("That agent type is not available"); | ||
| const settings = defaults("", kind); |
There was a problem hiding this comment.
[P2] Exclude hosted types from the native import path
BringOver still receives every registered type and offers each in its As selector. Selecting a new hosted type reaches this shared add path via port, which accepts it and imports the legacy key natively without calling the provider. The result is shown as here, but hosted types cannot have run, so the imported agent never answers; another device misclassifies its published runtime as provider-hosted even though the provider never received it. Filter hosted types out of Bring over and reject them at the native port/add boundary before saving the identity.
| <AgentDelete agents={agents} agent={agent} onRemoved={onClose} /> | ||
| </> | ||
| ) : ( | ||
| <Button variant="primary" onClick={onBuild}> |
There was a problem hiding this comment.
[P2] Preserve keyboard focus when Finish setup succeeds
When agents.finish updates this agent to finished, this conditional replaces the fragment containing the focused Finish setup button with a different Open button. AgentPeek has no focus handoff, so keyboard activation loses its focused control on success instead of allowing the user to continue into the agent. Keep the action mounted across this transition, or explicitly transfer focus to Open only if it was still on Finish setup. This follows DESIGN.md’s States, feedback, and motion rule; the new page test mocks finish without changing the snapshot, so it never exercises the transition.
7e0ffeb to
16bfdda
Compare
4ba12f7 to
31cfcb6
Compare
A type with `remote` steps (provision, activate, profile, remove) is hosted: its provider holds the key and runs each agent. The app makes one by asking the provider for its key, signing the owner's NIP-OA attestation, handing that over, then listing it. A setup that fails partway is kept as unfinished, across restarts, and Finish setup continues it without making another. Rename and picture go through the provider; a hosted agent's settings are its type's tabs calling its provider. Every agent is listed in the owner's kind 30177 records with its type as a `runtime` tag, so each of the owner's devices shows it here, hosted, or on another device. A record keeps the settings and tags of the one it replaces and defaults parallelism and respond_to, as other desktops parse it whole. Records without a runtime are earlier systems' and stay hidden. Agents on another device are read-only and never run here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Bradley Axen <baxen@squareup.com>
- Bring over offers only types that run here, and `add()` refuses a hosted type, whose key stays with its provider. - A rename made while an agent's record is being written is listed once that write finishes. - A hosted agent's profile save takes the same per-agent turn as setup and delete, so a late save cannot relist a deleted agent. - A key from the provider that the owner already lists is refused. - Records written before the community's records are first read are kept through a failed read and merged into the next one. - Focus moves from Finish setup to Open when the setup finishes. Co-authored-by: claude00 <fb8b76a01257932b8ca35060862c29bd09b2137638573731c2d5074fc02c5385@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Bradley Axen <baxen@squareup.com>
31cfcb6 to
1796a8f
Compare
wesbillman
left a comment
There was a problem hiding this comment.
Two changes remain: the existing kind-30177 ordering finding is still applicable, and the new setup-focus handoff can steal focus after the user moves elsewhere (inline). The read-failure preservation, native-import exclusion, in-flight relisting and profile/delete serialization fixes address their earlier reports in source.
Star Lord automated source review via Wes’s account. Head 1796a8f6c8d4360c1e67e12ab41fb30352abb00c; base 8c151a238855802bde7be2ea65ea377c1b31002c. Source-only: no tests, app/provider execution or CI validation; rendered behavior remains unverified.
| useEffect(() => { | ||
| if (agent.unfinished || !handoff.current) return; | ||
| handoff.current = false; | ||
| openButton.current?.focus(); |
There was a problem hiding this comment.
[P2] Cancel the setup focus handoff when the user moves away
handoff.current records focus only when Finish setup is activated. If the provider takes time and the user tabs or clicks to another surviving control (for example the peek’s Close button or a header action), it stays true; this effect then pulls focus back to Open on completion. This fixes lost focus but overrides the user’s newer focus choice. Keep the action mounted across the label change, or cancel the pending handoff when focus leaves it before completion. Add a deferred-setup test that moves focus away before resolving and verifies that it stays there, alongside the existing success case. This preserves the focus behavior required by DESIGN.md’s States, feedback, and motion guidance.
…list
The fixture relay only knew the profile panel's combined 30175/30177
read, so the new `{ authors: [viewer], kinds: [30177], limit: 500 }`
read failed the unexpected-request check in plugin-import and updates.
Co-authored-by: claude00 <fb8b76a01257932b8ca35060862c29bd09b2137638573731c2d5074fc02c5385@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Bradley Axen <baxen@squareup.com>
wesbillman
left a comment
There was a problem hiding this comment.
Two changes still needed: strict ordering for replacement agent records and cancelling the setup focus handoff when the user moves away. Both production paths are unchanged. No additional finding in the fixture-only follow-up: its exact filter assertion matches the owner-list query without relaxing the existing unexpected-request checks.
Star Lord automated source review via Wes’s account. Head d2ba7a65383a83bcb51ef6eb8aae26fa6285a86c; base 8c151a238855802bde7be2ea65ea377c1b31002c. Source-only: no tests, app/provider execution or CI validation; rendered behavior remains unverified.
Stacked on #874, the top of the Agents stack.
Agents could only make agents whose key lives on this device, and the agents page showed only those. This adds hosted agent types, whose provider holds the key and runs the agent, and a generic list of every agent the owner has, so each device can show agents that run elsewhere.
Hosted types
A type with
remotesteps is hosted. It has norun,modelsortimeoutMs, and its agents have no attention.provision, signs NIP-OA throughprepareRemoteAgentAuthorization, runsactivate, then lists the agent.provisionfails, the agent is kept as Setup unfinished, across restarts too. Finish setup continues it without making a second agent.profile, then the listing is updated. The picture must be an HTTPS URL.save(config)rejects for it.The owner's agent list (kind 30177)
["runtime", "<pluginId>/<typeId>"].parallelismandrespond_to(docs/relay-queries.md). So a record keeps every setting and tag of the record it replaces, and defaults those two to1/owner-only.Teams stay local-only, so hosted agents can't join one yet.
Trying it
The example ack-bot plugin and its localhost provider live in a separate draft PR stacked on this one (#875), kept for reference and not for merging.
Testing
tscand biome are clean.publishAgentRecordand the page.Not here yet
claude-code→ claude-local rename, which changes the publishedruntimetoo; the next sync relists agents under the new key.🤖 Generated with Claude Code