Repository navigation
Conversation
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>
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.
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