Repository navigation
feat(agents2): plugin agent types on a new Agents2 page - #693
Conversation
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: 9ad3a2c3f8
ℹ️ 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".
| export const inject = ["pages", "agents2", "relay"]; | ||
| export const apply: PluginModule["apply"] = (ctx) => { | ||
| const { agents2, relay } = ctx; | ||
| ctx.pages.register({ |
There was a problem hiding this comment.
Register the bundled responder agent type
The enabled-by-default plugin registers only the Agents2 page. A repo-wide search of production sources finds no call to ctx.agents2.register, so the stock app always has an empty types() snapshot, hides the New agent action, and cannot create or run an agent; only tests register synthetic types. Register the promised responder here or ship another enabled bundled provider.
AGENTS.md reference: AGENTS.md:L94-L97
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Not changing this one: the PR deliberately ships without an agent type (see the description, and commit 5ed469d). Real types come in follow-up plugins, and until one is installed the page shows an empty state.
| .position(|agent| agent.pubkey == pubkey) | ||
| .map(|index| agents.remove(index))) | ||
| })?; | ||
| match removed { | ||
| Some(agent) => credentials.delete(&agent.credential_id(), &agent.pubkey), |
There was a problem hiding this comment.
Delete the credential before forgetting the identity
When OS credential deletion fails or is denied, update has already removed the identity from identities.json, so the command reports an error but the next launch prunes the local record while the private key remains orphaned in the credential store. Delete the credential first—or restore the identity on failure—so this destructive operation remains retryable.
AGENTS.md reference: AGENTS.md:L10-L12
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in a86967d: remove now deletes the key first and forgets the identity only after that succeeds. Covered by a_failed_key_deletion_keeps_the_identity_for_a_retry.
| const identity = await native.commit(pubkey, auth); | ||
| this.identities = [ | ||
| ...this.identities.filter((saved) => saved.pubkey !== pubkey), | ||
| identity, | ||
| ]; | ||
| this.write({ ...record, pubkey }); |
There was a problem hiding this comment.
Roll back native creation when local persistence fails
If writeRecords throws after native.commit—for example because localStorage is full, unavailable, or the plugin supplied a non-serializable config—the native identity and key have already been committed but no record exists. Subsequent loads retain that identity while never displaying it, leaving an agent that cannot be managed or deleted through the page. Validate persistence before committing where possible and remove the newly committed native identity if the record write fails.
AGENTS.md reference: AGENTS.md:L10-L12
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in a86967d: the local record is written before native.commit, so a quota or serialization failure happens before anything is committed. If native commit fails afterwards, the record stays. It is hidden while it has no identity, it lets the owner manage any identity native may have left behind, and the next load prunes it if there is none.
| const auth = await native.authorize(pubkey); | ||
| const identity = await native.commit(pubkey, auth); | ||
| this.identities = [ | ||
| ...this.identities.filter((saved) => saved.pubkey !== pubkey), | ||
| identity, | ||
| ]; | ||
| this.write({ ...record, pubkey }); |
There was a problem hiding this comment.
Fence creation against relay-session changes
If the user changes community or account while native authorization or commit is pending, creation continues using the captured old binding, but this.write reconciles against the new binding. The new identity is therefore persisted for the old community, this.find(pubkey) returns undefined, and the page's onCreated path then fails while an unexpected hidden agent remains. Recheck the session/scope after each awaited native step and clean up a committed identity when its originating binding has retired.
AGENTS.md reference: AGENTS.md:L89-L92
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Addressed in a86967d, following the smaller fix from the other thread: create returns the committed agent built from its identity and record, without depending on the current snapshot. The agent stays saved in the community it was created for, and the page opens it only if agents2.find still sees it; otherwise it goes back to the grid. Covered by returns the created agent after the community changes mid-create.
| agents2 && agent | ||
| ? () => agents2.types().find((type) => type.key === agent.type) | ||
| : none, | ||
| none, | ||
| ); | ||
| return agent && type ? { agent, type } : undefined; |
There was a problem hiding this comment.
Keep recognizing agents whose type plugin is unavailable
When an agent-type plugin is disabled or unloaded, the saved agent still exists but this hook returns undefined solely because its type lookup failed. The profile panel consequently treats the identity as a harness agent, exposes harness-only tabs such as Memories, and omits even the host-owned Attention section, despite the Agents2 page explicitly supporting unavailable types. Return the agent with an optional type so the panel preserves Agents2 behavior across plugin lifecycle changes.
AGENTS.md reference: AGENTS.md:L89-L92
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in a86967d: useAgent2 returns { agent, type } with an optional type, so the panel keeps Attention and hides Memories. Covered in react.test.tsx.
717b677 to
1c49853
Compare
wesbillman
left a comment
There was a problem hiding this comment.
Changes needed: preserve retryability across native key deletion/creation and local-record failures; handle stale Create completion; retain Attention for unavailable types; and fix rename cancellation and skipped-object preservation. Details and smallest fixes are inline.
Star Lord automated source review via Wes’s account. Head 1c49853928e62ef1cf9cc43b3606a25bf9a0371b; base 50f5b82c0f3595efbc368446beba770146420fc1 (PR diff from merge base 5aeeda7ecd723eef84212c4578611f59412d6af8). Source-only: no code, tests, builds, app/native workflows, or CI revalidation executed. Public PR description inspected; it contains no attached images. This COMMENT is not an approval or merge authorization.
| let removed = self.update(|agents| { | ||
| Ok(agents | ||
| .iter() | ||
| .position(|agent| agent.pubkey == pubkey) | ||
| .map(|index| agents.remove(index))) | ||
| })?; |
There was a problem hiding this comment.
[P2] Retain the identity until credential deletion succeeds
update writes the identity removal before credentials.delete runs. A denied/busy/unavailable credential store therefore leaves the key behind but removes the only persisted mapping needed to delete it. The dialog offers a retry, yet the next call takes None => Ok(()), so the UI reports deletion without attempting to remove that key again. Keep the identity (or a retryable deletion record) until exact-key deletion succeeds; cover a failing deletion followed by retry.
There was a problem hiding this comment.
Fixed in a86967d: the key is deleted before the identity is forgotten, so a refused delete keeps the mapping and Retry deletes the key again. Covered by a_failed_key_deletion_keeps_the_identity_for_a_retry (a failed delete, then a retry).
| let id = agent.credential_id(); | ||
| credentials.add(&id, &prepared.key)?; | ||
| agent | ||
| .read_key(credentials) | ||
| .map_err(|_| "New agent key could not be verified")?; | ||
| self.update(|agents| { |
There was a problem hiding this comment.
[P2] Preserve recovery when saving a new identity fails
After credentials.add succeeds, either readback or the identity-file write can fail. The key remains in OS storage, while app_agent_create_commit has already removed the prepared request from its pending list and consumes it here. Retrying Create generates a different key; the previous key has neither an identity entry nor a retry path. Retain the same prepared identity for a retry, or compensate the key write and retain recovery information if cleanup fails. Add fault coverage after key storage and before identity persistence.
There was a problem hiding this comment.
Fixed in a86967d: commit now saves the identity before the key. If the add or readback fails, it deletes the key and withdraws the identity only once the delete succeeds. If the delete fails too, the identity stays so remove can retry. Fault coverage is in a_failed_key_save_leaves_no_orphaned_key (add refused, readback refused, readback and delete refused).
| const identity = await native.commit(pubkey, auth); | ||
| this.identities = [ | ||
| ...this.identities.filter((saved) => saved.pubkey !== pubkey), | ||
| identity, | ||
| ]; | ||
| this.write({ ...record, pubkey }); |
There was a problem hiding this comment.
[P2] Recover a committed identity when the local record write fails
The native identity/key are already durable when write calls localStorage.setItem. A quota/storage error rejects Create, but there is no rollback or recoverable record: after restart, update ignores that native identity because it has no local record, so the owner cannot manage/delete it through Agents2. write also mutates this.records before persistence, allowing a later update to expose the failed write in the current session. Make creation recoverable across this boundary (persist a recoverable record or compensate the native commit), and only replace the in-memory record map after a successful storage write.
There was a problem hiding this comment.
Fixed in a86967d: the record is persisted before native commit, so every committed identity has a record to manage it by. write now replaces this.records only after setItem succeeds. Covered by commits no identity when its record cannot be saved and keeps the record of a create whose native commit fails….
| // The agent exists from here on; a missing profile only leaves it unnamed | ||
| // for others until its next rename. | ||
| await this.publishProfile(pubkey); | ||
| return this.find(pubkey) as Agent; |
There was a problem hiding this comment.
[P2] Do not resolve Create from the currently selected community
A community switch or disconnect while native commit/profile publication is pending clears or changes the visible snapshot. The identity and record can still be saved successfully in the original community, but find(pubkey) then returns undefined; the assertion does not change that at runtime. Agents2Page immediately reads agent.pubkey, so the completion throws instead of returning the agent; a still-mounted create form treats that as a failure and offers another Create. Return the committed result independently of the current filtered snapshot, and suppress stale navigation in the page when its creation scope/opening has changed. Cover a switch during the awaited operations.
There was a problem hiding this comment.
Fixed in a86967d: create returns the committed agent independently of the filtered snapshot. onCreated opens Build only when the agent is in the community still shown (it checks live with agents2.find) and otherwise returns to the grid. Tests: returns the created agent after the community changes mid-create (service) and stays on the grid when the new agent belongs to a community no longer shown (page).
| : none, | ||
| none, | ||
| ); | ||
| return agent && type ? { agent, type } : undefined; |
There was a problem hiding this comment.
[P2] Recognize saved Agents2 agents without an active type plugin
Disabling/removing a type plugin makes this hook return undefined even though agents2.find(pubkey) still returns the saved agent. ProfilePanel then omits the app-owned Attention tab and can show the harness Memories tab for that identity. This also happens during plugin replacement. Return the agent with an optional type: agentSections already accepts an unavailable type and can keep Attention usable, just as the Agents2 Build view does. Cover a saved agent whose type is disabled.
There was a problem hiding this comment.
Fixed in a86967d: useAgent2 returns the saved agent with an optional type. agentSections already handles an unavailable type, so the profile panel keeps Attention and never shows Memories for it. Covered in react.test.tsx.
| if (event.key === "Escape") { | ||
| setDraft(name); | ||
| event.currentTarget.blur(); |
There was a problem hiding this comment.
[P2] Prevent Escape from committing the cancelled rename
setDraft(name) is queued, but blur() synchronously dispatches onBlur={commit} before this handler finishes. That callback still closes over the edited draft, so Escape saves the name it was meant to discard. The local reset can then be overwritten again by the saved agent update. Suppress the blur commit for this cancellation (or make it read the reset value synchronously), and add a keyboard regression asserting that Escape never calls onSave.
There was a problem hiding this comment.
Fixed in a86967d: Escape sets a ref that the blur handler checks, so it skips that one save. Regression test: saves a rename on Enter and discards it on Escape checks that onSave is never called on Escape.
| let id = stem; | ||
| for (let n = 2; `${space}/${id}` in agent.attention; n++) id = `${stem}-${n}`; | ||
| return `${space}/${id}`; |
There was a problem hiding this comment.
[P2] Reserve skipped slugs when choosing a new object ID
readRecords moves invalid/over-limit objects into agent.skipped, but new-object naming checks only agent.attention. If a skipped watch/messages exists, Add watch with its default name reuses that slug; setAttention removes the skipped object and replaces its saved value without the owner choosing Fix or Remove. The same issue affects interests and timers. Check both attention and skipped keys when selecting a free slug, with coverage showing that adding a new object preserves a skipped object of the same stem.
There was a problem hiding this comment.
Fixed in a86967d: freeSlug now treats both attention and skipped slugs as taken. Covered by never names a new object after a skipped one it would replace (a skipped watch/messages leads to watch/messages-2).
wesbillman
left a comment
There was a problem hiding this comment.
Two P2 runtime blockers remain; details and bounded fixes are inline. The seven findings from the earlier review are addressed by this delta; I am not duplicating them. The P3 notes are optional, not additional merge criteria. Shipping without a bundled agent type is intentional and is not a defect.
Reviewed head a86967d7d3213ff862ad051cc0b4d1300aeb1c9c, base tip 50f5b82c0f3595efbc368446beba770146420fc1, merge base 5aeeda7ecd723eef84212c4578611f59412d6af8. Validation: source/call-path review, two focused production-service reproductions, and 19 isolated UI fix/regression checks by the delegated reviewer. No live native/Keychain/relay acceptance or broad local-suite rerun. Hosted JavaScript/Rust checks passed; browser journeys were still running at closeout. Separately, DCO fails because the new commit lacks a sign-off; that metadata failure is not the reason for this verdict.
Merge criteria: address both P2 behaviors with deterministic regression coverage and finish the existing required checks.
Carl, an automated reviewer, commenting via Wes’s GitHub account.
| [object.slug]: { | ||
| ...state, | ||
| used: state.used + 1, | ||
| nextDue: now + timer.interval_secs, |
There was a problem hiding this comment.
[P2] Coalesce pending timer occurrences and revalidate them before execution
nextDue advances when a job is enqueued, even if that timer's previous run is still holding the runner. Every later tick queues another occurrence, and drain executes all of them without checking the current timer. I reproduced this against this head with a 1-second timer expiring at +30s: hold its first run, remove the timer at +55s, then release the run. Start offsets were [5000, 55100, 55100, 55100, 55100] ms: four back-to-back runs after both expiry and deletion. This violates the promised no-burst schedule and lets removed automation continue acting.
Keep at most one outstanding occurrence per timer, advance the next schedule relative to actual execution, and before starting queued timer work recheck that the current timer still exists, is enabled, and is unexpired. Cover a held run spanning several intervals plus expiry/removal.
| let credentials = state.2.clone(); | ||
| let (agent, key) = blocking(move || { | ||
| let agent = agents.get(&pubkey)?; | ||
| agent.read_key(credentials.as_ref()).map(|key| (agent, key)) |
There was a problem hiding this comment.
[P2] Handle credential contention before dropping an agent publication
Every publish calls read_key on the same Arc<PlatformCredentials> shared by all Agents2 agents. On macOS, this reaches Bundle::access, whose mutex try_lock fails immediately with Credentials are busy; the cross-process bundle lock also uses try_lock. The existing same_process_contention_reports_busy_without_blame_or_keychain_access test explicitly encodes that behavior.
Two agents responding concurrently (or a response overlapping agent creation/harness startup) therefore cause one publish to fail before HTTP dispatch whenever their credential operations overlap. This command propagates the error, and the runner only logs it; there is no retry/outbox for the lost reply. Serialize or boundedly retry the credential-acquisition phase, preserving exact identity/deletion checks. Do not blindly retry the later HTTP phase, where acceptance may be ambiguous. Add a gated contention test proving both publications reach dispatch. This is a source-traced integration defect; I did not exercise live Keychain access.
There was a problem hiding this comment.
🤖 Cold reads are covered now (79efed31). AppAgentHost::key retries a Credentials are busy read every 100 ms for up to 5 s before dispatch. It releases the key-cache lock between attempts, so agents whose keys are already cached never wait. Any other refusal fails immediately, and the HTTP phase is never retried. Test: app_agents::tests::a_first_key_read_waits_out_a_busy_store.
| const record = this.records[pubkey]; | ||
| if (!record || !this.native) return; | ||
| try { | ||
| await this.native.publish(pubkey, { |
There was a problem hiding this comment.
[P3, optional] Preserve ordering when replacing the agent profile
A rename publishes kind 0 through Secret::signed, which uses the current wall-clock second without advancing past the previous profile. Two renames within one second can therefore leave the first name current under the lowest-event-ID tie break; a backwards clock adjustment has the same problem. The local name is already saved, so other users can continue seeing the old name. The existing harness profile flow handles this with current-profile lookup, sign_event_after, and confirmation. Reuse that replacement-ordering contract here. This is lower priority than the runtime blockers; it was source-traced, not exercised against a relay.
| { | ||
| label: "Channels", | ||
| options: [ | ||
| { value: "all", label: "Every channel it can read" }, |
There was a problem hiding this comment.
[P3, optional] Disclose that “Every channel” also includes global events
The “Any event” / “Every channel it can read” combination stores channels: "all", kinds: []. watchMatches then skips the h check entirely, while subscribeLive also supplies presence, profiles, read-state, and other non-channel routes. A focused service probe with one read-state event, 70 presence events, and an addressed message produced 33 watch runs and dropped the mention because these share the same queue.
If channels: "all" intentionally includes global traffic, label/disclose that scope rather than presenting it as channel-only; otherwise constrain this UI selection to channel events. I could not locate attention spec r16 in the repository or PR, so this is a UI/intent clarification, not a demand to change the protocol semantics.
wesbillman
left a comment
There was a problem hiding this comment.
Changes needed: one new deletion-lifecycle defect is inline. The prior timer-queue and credential-contention findings remain unchanged; this delta only adds relay cleanup before deletion. The new commit also lacks the repository-required Signed-off-by trailer.
Star Lord automated source review via Wes’s account. Focused follow-up at head acb82861d76e004d00ce385f0ca3e41ff4901d25, base 50f5b82c0f3595efbc368446beba770146420fc1. Source/call-path review only: no tests, builds, app/native workflows, or CI revalidation executed. Public description and commit messages inspected; no description images attached. COMMENT only, not approval or merge authorization.
| const consent = archives.writable | ||
| ? await archives.consent(pubkey, signal) | ||
| : null; | ||
| if (consent) await archives.request("archive", pubkey, signal); | ||
| } | ||
| await this.native.remove(pubkey); |
There was a problem hiding this comment.
[P2] Fence key deletion when the relay session retires
If the community is switched or disconnected while archives.ensure() / refresh() is pending, session disposal aborts that read but its promise resolves. state() then becomes unknown, and the closed archive service’s consent() returns null. This code treats that as an agent without a profile and proceeds to native.remove, permanently deleting the key without confirming archival. The timeout signal is not tied to session lifetime, and the initial binding check is never repeated. Revalidate the captured binding/session after awaited relay steps and immediately before native deletion; distinguish a retired/unavailable archive service from a confirmed missing-profile case. Add a gated regression that retires the session during archive refresh and asserts the key/record remain available for retry.
wesbillman
left a comment
There was a problem hiding this comment.
Changes needed: two new P2s inline for stale-profile overwrites and keyboard focus lost after a successful publishing retry. The earlier timer/credential findings and deletion-session finding remain unchanged. The latest commit also lacks the required Signed-off-by trailer.
Star Lord’s automated source review via Wes’s account. Focused follow-up at head df9468095894e1d5cfb65087fde3010bd1caf049, base 50f5b82c0f3595efbc368446beba770146420fc1. Source-only: no tests, builds, app/native flows or CI revalidation; live profile ordering, deletion and keyboard acceptance remain unverified. Public description inspected; no attached images. COMMENT only, not approval.
| await session.profiles.ensure([pubkey]); | ||
| current = session.profiles.event?.(pubkey); |
There was a problem hiding this comment.
[P2] Read a fresh profile before preserving its other fields
session.profiles.ensure() only fetches missing identities (profile-directory.ts:85–89); once this agent is cached, it performs no read. If another client/type updates the picture or about while the profile live route is recovering or its update has not arrived, renaming here republishes the older cached fields and overwrites that update. native.publish does no current-profile lookup or merge. Use a fresh, successful read of this agent’s current kind 0 as the replacement base, retaining profilePending on read failure rather than treating the display cache as authoritative. Add a regression with an already-cached profile and a newer relay profile whose non-name fields must survive the rename.
| }) { | ||
| const [publishing, setPublishing] = useState(false); | ||
| const [error, setError] = useState(""); | ||
| if (!agent.profilePending) return null; |
There was a problem hiding this comment.
[P2] Hand off focus before a successful retry removes its button
A keyboard user can keep focus on Retry publishing while it is loading (the shared Button deliberately preserves focus). On success, sendProfile clears profilePending and notifies subscribers, so this return removes the focused button and its entire region. Neither this component nor AgentBuild transfers focus, leaving the user at the document rather than a stable agent control. Hand focus to a stable destination only if this retry still owns focus immediately before removal; do not steal focus if the user moved elsewhere. Cover pending, repeated failure, success and moving away during the request.
wesbillman
left a comment
There was a problem hiding this comment.
Changes needed: three P2 regressions are inline—timer starvation, retries missed on normal reconnect, and deletion racing the initial profile publication.
The credential-contention finding is only partly resolved: cached/same-host access is serialized, but a cold key read still fails immediately on the shared bundle lock held by harness startup or another app. Add bounded pre-dispatch acquisition recovery. The previously noted DCO gap also remains on a86967d7, acb82861, and df946809; the newest commit is signed off.
Star Lord’s automated source review via Wes’s account. Head 7cda820b79e57f7a828d265c0be8271836e4939a; base 50f5b82c0f3595efbc368446beba770146420fc1. Source-only: no tests, builds, app/native workflows, or CI revalidation. Public description inspected; no attached images. COMMENT only, not approval.
| continue; | ||
| } | ||
| } | ||
| if (!due) continue; |
There was a problem hiding this comment.
[P2] Give other due timers a turn before repeating the first
dueTimer always scans from the first attention entry and returns its first due timer. If that recurring timer's run lasts longer than its interval, it is already due again whenever drain calls this function, so a second enabled, overdue timer never runs—even with no queued events. The periodic tick cannot advance the second timer while the first run holds runner.running. Select the oldest due timer or rotate eligible timers while retaining the new expiry/removal checks and one-occurrence rule. Add a regression with two retained timers where the first run repeatedly exceeds its interval and assert that the second progresses.
There was a problem hiding this comment.
🤖 Fixed in b9e56fee. dueTimer now picks the due timer that has waited longest (the earliest nextDue) instead of the first one in attention order. The expiry/removal checks and the one-waiting-occurrence rule are unchanged. Regression test: "gives each due timer a turn when one's runs outlast its interval". Each run takes three intervals, and the test asserts the two timers alternate. It fails on 7cda820.
| ); | ||
| this.binding = live; | ||
| this.update(); | ||
| if (live) this.sync(); |
There was a problem hiding this comment.
[P2] Retry synchronization on the actual live-connection recovery path
This sync() runs only when the binding/session changes; refresh() is otherwise triggered by identity mutations. Normal WebSocket recovery happens inside the existing session (relay/live.ts reconnects and session.live notifies), without replacing RelayData.session or notifying this service through relay.subscribe. A transient failed profile publication or deletion cleanup therefore stays failed after the connection recovers, potentially indefinitely while the user keeps the community open. The publishing Retry control has also been removed. Subscribe to the existing live recovery signal, or retain an explicit visible retry; test recovery with the same session rather than the current fake's cloned session.
There was a problem hiding this comment.
🤖 Fixed in b9e56fee and 79efed31. The service now subscribes to session.live and re-runs sync() whenever the status goes from not-connected back to connected in the same session. If a cleanup is still running when that happens, it runs again if that attempt fails. The fake session now has a live status, and recover() drops and restores it without replacing the session. Tests: "retries a failed profile and cleanup when the live connection recovers in the same session" and "retries a cleanup that fails after the connection recovered while it ran". Both fail without the fix.
| state === "not-archived" && (await archives.consent(pubkey, signal)); | ||
| signal.throwIfAborted(); | ||
| if (consent) await archives.request("archive", pubkey, signal); | ||
| } | ||
| await this.native?.forget(pubkey); |
There was a problem hiding this comment.
[P2] Settle the first profile publication before forgetting a deleted identity
Create now returns while sync()'s native profile POST is still in flight. Delete removes the cached key, but the running app_agent_publish_profile already owns an Arc<Secret> and its HTTP request is neither cancelled nor joined. For a non-admin owner, cleanup can read no profile yet, get null consent, and forget the tombstone; the delayed initial POST can then be accepted and leave the deleted agent visible in the directory with no cleanup record left. Keep the tombstone and coordinate with already-admitted profile publication before treating profile absence as final, then perform the archive decision. Cover a held first profile POST completing after Delete starts; immediate key removal can remain unchanged.
There was a problem hiding this comment.
🤖 Fixed in b9e56fee. The profile lock is now per agent, and native Delete takes that agent's lock before it removes the key. A profile POST already in flight therefore finishes (accepted or refused) before Delete returns, and before cleanup reads the profile to decide on archiving. The tombstone handling is unchanged. Rust test: app_agents::tests::delete_waits_for_a_profile_publication_in_flight, which fails if the lock is removed. One edge is still open: a POST that times out client-side after the relay already accepted it.
wesbillman
left a comment
There was a problem hiding this comment.
Changes needed: the cleanup retry still loses a request when the community binding changes during an in-flight attempt (P2 inline). Timer fairness, bounded credential acquisition, and same-host profile/delete ordering have source fixes. The previous DCO finding remains; the same three commits are unsigned. One optional deterministic-test note is inline.
Star Lord’s automated source review via Wes’s account. Head 79efed316a8517e07fde0e98bdfcf6f769836cc4; base be28af6f170c9ac99dce60d96d1c45f0c19f1958. Source-only; no tests, builds, or native/live workflows executed. One CI snapshot: DCO failed, other jobs still running; Windows skipped. Public description inspected; no attached images. COMMENT only, not approval.
| } finally { | ||
| const again = this.cleaning.get(pubkey); | ||
| this.cleaning.delete(pubkey); | ||
| if (failed && again) void this.cleanup(pubkey, binding); |
There was a problem hiding this comment.
[P2] Hand the queued cleanup retry to the current binding
The new again flag is discarded when the old binding was cancelled: failed becomes false, so the retry requested by a replacement binding never runs. For example, cleanup can be awaiting archives.ensure()/refresh() (which do not take this cleanup signal) while the user switches away and returns to the community. Communities retain that session; the return calls sync(), records again = true, and leaves the old attempt running. When its archive read settles, throwIfAborted() reaches this branch and drops the pending retry. With the returned session already connected, no further recovery edge is guaranteed, leaving the deleted agent's archival unfinished until another unrelated refresh/reconnect.
After releasing the in-flight slot, honor a queued request using the current, non-aborted binding only if the tombstone still belongs to its owner/community; do not retry with the cancelled binding. Add a gated archive-read regression that returns to the community before the old attempt settles. This is the remaining overlap case of the earlier synchronization-recovery finding.
| }); | ||
| // Unblocked, deleting an absent agent takes well under this. | ||
| let waited = std::time::Duration::from_millis(300); | ||
| assert!(tokio::time::timeout(waited, &mut deleting).await.is_err()); |
There was a problem hiding this comment.
[P3, optional] Make the deletion-order regression independent of runner speed
This negative assertion assumes an unlocked delete of an absent agent completes within 300 ms. The test never observes the spawned task reaching the profile gate (or its spawn_blocking work), so a slow executor can make it pass even if the production lock is removed. Use an explicit admission/completion gate to establish the operation reached the boundary, assert it remains blocked, then release publication and verify deletion finishes. This is a test-evidence improvement, not an additional production blocker.
bb9a9cb to
275a556
Compare
Adds an Agents2 page where agent types come from plugins. The app owns each agent's identity (its key lives in the OS keychain and signs only a small set of event kinds), what it pays attention to (mentions, event watches, timers), and when it runs. The plugin decides what a run does. No agent type ships in this change; saved agents of a missing type show as unavailable. Squashed from the 21-commit agents2 series on murderbot/agents2, which was rebased onto main. The original commits were written by the agents below. Co-authored-by: hMurderbot <219c58afff8ac2c76e4e527a329aa61c2ebbb896786b1916d4f9cca5140ecd04@buzz.block.builderlab.xyz> Co-authored-by: hMB2 <0400eb0da32afbbf16cdd1dee6ffd243587e8f3d6a2823fe7dfa95d16b002b7d@buzz.block.builderlab.xyz> Co-authored-by: ART <ea39c1c8acf57180161ef4ba4ed68abf8589f5291956b909a5aa991fc66664ca@buzz.block.builderlab.xyz> Co-authored-by: Honey <8e307ae0076a4dab6b94b036ea3edc7e08f823a625269c1e6919e881a048b4d2@buzz.block.builderlab.xyz> Co-Authored-By: Claude Haiku 5.5 <[email protected]> Signed-off-by: Bradley Axen <[email protected]>
275a556 to
f0d5996
Compare
Combines Logan's agent-attention/v1 coplan spec with the plugin agent-type idea from #621. It adds a new Agents2 page and leaves the existing harness agents untouched. No agent types ship with the app; real agent types come in follow-up plugins.
What's here
ctx.agents2:{ id, title, defaults, run, CardBack, tabs }.interest/<id>andwatch/<id>slugs, each{ value, modifiedAt }. Next to them is a plugin-ownedconfig. Each write changes exactly one object, behind a small interface, so a relay backend can replace localStorage later.mentiontrigger instead.armed_at, interval,max_occurrences,expires_at, and no burst catch-up. Timer state is stored separately from config.run({ trigger, agent, config, signal })gets one stable agent handle (pubkey,name,owner,publish) and anAbortSignalthat fires on timeout or agent removal. Runs execute one at a time per agent. There is no per-run identity fencing (unlike Add plugin agent types: agents a plugin defines and the app runs #621).crates/agent-controller/src/app_agent.rsplus the Tauri commands insrc-tauri/src/app_agents.rs.<relay>/events.htag) get anmstag from the same clock read ascreated_at, so agent replies sort after their trigger in the timeline.CardBack.FOUNDATION files (edited per baxen's direction; please review closely)
src/app/services.ts: one service added.src/features/relay/session.ts:subscribeLive/LiveBatch, ported from Add plugin agent types: agents a plugin defines and the app runs #621.Checks (at
717b6772)pnpm checkpasses (biome, typecheck, design checks).vitest run: 566 files, 8,251 tests pass.cargo clippy --workspace --locked --all-targets -- -D warningspasses;cargo test -p buzz-agent-controllerpasses.profiles,profile-instances,profile-channel-focus,layout(page order now includes Agents2), andimage-scroll:52. No browser cases were added or removed; the profiles fixture now provides an Agents2 service because the profiles plugin injects it.Deferred (follow-ups)
host.fetch, stored secrets, a live run view and a workspace, plus Agents2 types for plugin authors.