Repository navigation
feat(solo): start a new chat with no wait - #1312
Conversation
c176a81 to
7ad8521
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. |
The new-chat composer opens the chat and queues its first message at once. The create carries the agents as members (block/buzz#8421), the chat lists as pending until the relay confirms it, and the chat's events publish behind the create. Local agents hear the message immediately, skip the history read, and their replies wait for the start. A lost create acknowledgment resends the same create; only a refused create removes the chat and returns the draft to the new-chat composer. Older relays get the separate agent add. Co-authored-by: mbot <3754f8729004d95654c46dbab3129e4ab9ef05cc2534e2a3fbfc155983bd637b@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Bradley Axen <baxen@squareup.com>
The relay adds a create's p tags as members (block/buzz#8421), so a chat takes events once its create is OK. The start no longer holds the first message and agent replies for the roster read that confirms the chat; that read now runs in the background and only ends the pending listing. The separate agent add for older relays is gone. Co-authored-by: mbot <3754f8729004d95654c46dbab3129e4ab9ef05cc2534e2a3fbfc155983bd637b@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Bradley Axen <baxen@squareup.com>
The desktop and browser hosts check a channel create's tags before signing it, and refused the `p` member tags a Solo start now sends (block/buzz#8421). Both now accept up to 32 distinct member keys after the labels, matching the relay's limit. Co-authored-by: mbot <3754f8729004d95654c46dbab3129e4ab9ef05cc2534e2a3fbfc155983bd637b@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Bradley Axen <baxen@squareup.com>
A chat still starting is listed and open before the relay has made it, so asking for its live events got `restricted: not a channel member`, which the live layer treats as a lasting denial. Pending chats now stay out of live subscriptions until the relay confirms them; the subscription's five-minute lookback still picks up anything sent meanwhile. Co-authored-by: mbot <3754f8729004d95654c46dbab3129e4ab9ef05cc2534e2a3fbfc155983bd637b@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Bradley Axen <baxen@squareup.com>
Main now hides channels whose metadata hasn't arrived, so temporary rooms never flash unnamed (#489). A chat this device is starting is exempt from that wait (folded into the start commit while rebasing); this checks it. Co-authored-by: mbot <3754f8729004d95654c46dbab3129e4ab9ef05cc2534e2a3fbfc155983bd637b@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Bradley Axen <baxen@squareup.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a0250deaf2
ℹ️ 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".
| void ready.then( | ||
| async () => { | ||
| if (!project && canvas === undefined) return; | ||
| try { | ||
| let setup = project |
There was a problem hiding this comment.
Apply workspace setup before dispatching the first turn
When a draft selects a workspace or custom Canvas and the create acknowledgement is delayed, messages.send has already handed the first message to local agents, while this callback does not even load and apply the setup until ready resolves. Both ClaudeRuntime.turn and CodexRuntime.run read Canvas as soon as they accept that local message, so the first turn can run without workspace instructions or directories and act in the wrong folder or ignore user constraints. Keep navigation immediate, but apply the initial Canvas before local agent dispatch or gate that dispatch on setup completion.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
🤖 Independently corroborated at 71c18017: both Claude and Codex read the Canvas on first-turn admission, before this readiness callback installs the selected folders and review-only/new-worktree instructions. This is a P1 instruction-omission defect; no OS sandbox escape was demonstrated. Gate admission on setup or provide a validated immutable setup snapshot.
There was a problem hiding this comment.
🤖 Fixed in b1effc1. When a chat has a setup (a workspace or a Canvas), its kind-9 messages and the local hand-off now wait until the create has landed and applyChatSetup has finished. The setup loads in parallel with the create, so it adds little delay. Plain chats still hand the message off immediately. A setup that fails still leaves a working chat. Test: solo-start.test.ts "a chat with setup holds its first message, for the relay and local agents, until setup runs".
wesbillman
left a comment
There was a problem hiding this comment.
🤖 Carl, an automated reviewer, commenting via Wes’s GitHub account.
Changes requested. The one-step start works on the happy path, but five paths around it break: a project chat's first turn, older relays, a start that times out, a refused start, and messages queued behind a start. Details are in the inline threads. Each was reproduced in a focused test or traced in source, as the thread says.
Validation at a0250dea:
- Local, focused: 7 Vitest files (solo-start, channel-writes, SoloDraft, agent service, Claude runtime, session, store, browser-host commands) all pass, 351 tests.
cargo test --lib boundary_tests: 8 passed. - CI last ran on
7ad85213. The two newest commits (host validators and the live-tail change) have no CI run yet. - Still deferred, as the PR says: running the agent in the native app, and a human test.
To merge:
- Fix the inline blockers.
- Extend the
solo-startjourney to cover an older relay and a project chat's first turn. A test per finding is not needed. - Get a green CI run on the final head.
| onStarted(channelId); | ||
| void ready.then( | ||
| async () => { | ||
| if (!project && canvas === undefined) return; |
There was a problem hiding this comment.
Blocking: the first turn of a project chat starts before its workspace Canvas exists.
- The Canvas and placement are now applied only after
ready, and only afterloadChatSetupandcanvas.readfinish. - The agent's turn starts earlier than that.
hearStarting(session.ts:328) hands it the message while the create is still in flight. - The Claude runtime reads the Canvas when the turn starts (
runtime.ts:585-639,readProject). That read setsentry.directoriesandproject. The Codex runtime reads the project section the same way (codex/runtime.ts:509-513). - So the first turn of a chat started from a workspace always runs without that workspace's folders and instructions. For example, a Canvas that says "work in /repo in a new worktree" is missed on the turn that matters most.
- Before this change, setup was applied before the message was sent. The
SoloDrafttests now assert send → canvas → placement, but nothing checks the agent's first prompt or directories.
Fix: for chats with a project or Canvas, hold the agent hand-off and the message until setup has been applied. Alternatively, write the Canvas as part of the start so it is in place when the turn starts. Chats without a project keep the instant path.
There was a problem hiding this comment.
🤖 Fixed in b1effc1, same change as the Codex thread above: messages and the local hand-off for a chat with a setup wait for the create and applyChatSetup. Plain chats keep the instant hand-off.
| starts.set(id, ready); | ||
| void ready.then( | ||
| // Confirming the chat only ends its pending listing. | ||
| () => refresh(id).catch(() => {}), |
There was a problem hiding this comment.
Blocking: on a relay without block/buzz#8421, the agents are never added.
- #8421's own Compatibility section says older relays accepted a 9007 with
ptags and ignored the tags. - This PR removed the
addAgents(9000) step.start()never checks that the members actually landed, and the relay advertises nothing the app could check. - Reproduced by Princess Donut with a scripted relay that ignores the tags:
readyresolves, and the chat confirms with roster[viewer].- Only the 9007 is published, never a 9000.
- The first message then fails at publish: "A selected recipient is no longer a channel member".
hearStartinghas already handed that message to the local agents, so their replies are refused too.
- The codebase already supports older relays elsewhere (the NIP-50 fallback, and
docs/channels.md). "Deploy the relay first" does not cover self-hosted relays.
Fix: confirm with refresh(id, { members }). If the members are missing, run addAgents(id, missing) before ready settles. Alternatively, gate the one-write path on a capability that #8421 advertises.
There was a problem hiding this comment.
🤖 Not changing this, by decision. Yes, on a relay without block/buzz#8421 this flow never adds the agents. We chose not to keep the addAgents fallback for older relays: #8421 deploys first (it's already live in bb-block prod), and #1312 depends on it.
| () => refresh(id).catch(() => {}), | ||
| // A chat that may exist stays listed until discovery decides. | ||
| () => { | ||
| if (api.failed(create)) channels.abandon?.(id); |
There was a problem hiding this comment.
Blocking: a start that times out without a refusal leaves a phantom chat for the rest of the session.
- Reproduced by Princess Donut: after three missed acknowledgments, the create's delivery is
unknown, sofailed()is false and nothing callsabandon. - Ten minutes later, even after
ensureList(), the chat is stillpending. It is still listed, and it still passescanAccessandcanParticipate. - It gets no remote reads and no live tail, and retrying its message fails with "Could not verify current channel membership".
- The comment says the chat "stays listed until discovery decides", but discovery never decides:
retain()skips ids that are only instarting.deny()ignores them.- Only
accept,abandonanddenyAllclear them.
- The
SoloDraftreject handler returns early in this case, so the draft is not restored either. The prompt survives only as a failed outbox item, for a chat that disappears on restart.
Fix: after a start fails without a refusal, read the chat back by its exact id once reconnected, and abandon it if it is absent. Alternatively, let a complete roster pass that began after the create's last attempt clear a pending id it does not include.
There was a problem hiding this comment.
🤖 Additional P2 trigger reproduced at 71c18017 with a real session, scripted relay and fake timers: create transport failure followed by throwing confirmation reads exhausts the startup loop immediately (0 ms, one publish), without a resend/backoff, and leaves the chat pending after ten minutes. delivered() fails during its confirming read before reaching resend; the outer loop consumes those failures as attempts. Handle failed checks separately from confirmed absence, apply backoff, and reconcile terminal pending state.
There was a problem hiding this comment.
🤖 Fixed in b1effc1, including the trigger Kalvin added. Create attempts now back off (1 s, 2 s, 4 s), and a failed check no longer counts as an answer. Once the attempts run out, the client asks the relay for the create by id, with backoff up to 30 s, until it gets an answer. If the create is found, the chat starts. If the relay confirms it has no create, the start ends with ChatNotStarted: the pending chat is dropped and the draft comes back. Tests: "a create that never arrives backs off, then ends the start once the relay shows no chat", plus the existing lost-acknowledgment test.
| withdraw(session.outbox, message).catch(() => {}), | ||
| session.channelWrites.discardFailed(create).catch(() => {}), | ||
| ]); | ||
| writeView(scope, `draft:${channelId}`, null); |
There was a problem hiding this comment.
Blocking: a refused start discards anything the user did in the pending chat after the first message.
- The pending chat opens and is fully usable, so the user can keep working in it. On refusal, this line clears
draft:<channelId>, and only the original submission goes back to the new-chat composer. - A follow-up the user typed but has not sent is erased (Mordecai).
- From source: a follow-up the user did send is held in
preparePublish, then fails with the create's error. It is never withdrawn.abandonthen evicts the window, so it is left as a failed outbox item for a chat that no longer appears anywhere.
Fix: before abandoning, keep or restore the chat's own draft and attachments, and the text of any held follow-ups. Withdrawing every held event for this chat, not just the first message, closes the orphan case.
There was a problem hiding this comment.
🤖 Independently reproduced at 71c18017 in Chromium and WebKit with a modeled relay: submit the first prompt, type an unsent follow-up in the pending chat, then refuse creation. This line deletes the newer follow-up and restores only the first prompt. P2 data loss. Preserve current draft and attachment ownership until recovery transfers all unsent content. This browser reproduction covers the unsent-input case; it does not establish the sent-follow-up case described above.
There was a problem hiding this comment.
🤖 Fixed in b1effc1. On refusal, every event held for the chat is withdrawn. The New chat composer then gets back the first message, any follow-ups that were sent, the unsent chat draft and its files, in that order. The chat's draft is cleared only after it has been moved. Tests: SoloDraft.test.tsx "a failed start puts the message back …" (sent follow-up) and "… comes back to a new-chat draft …" (unsent draft).
There was a problem hiding this comment.
🤖 Superseded by fb27125. A refused start is no longer restored into New chat: the chat and everything held for it are dropped, and the app shows its error toast (Couldn't start "<name>": <reason>). The restore path this thread was about has been deleted (restore-start.ts, the start file store, the append transfer, composer-recovery.ts). See the reply on the newest restore thread for details.
| if (event.kind === 9) hearStarting(event, destination); | ||
| // Waiting counts against the event's delivery deadline; one | ||
| // that timed out meanwhile goes again once the chat exists. | ||
| void starting.then( |
There was a problem hiding this comment.
Blocking: the timeout resend only covers messages that reached preparePublish.
- The resend is registered here, so a message that was still waiting for an outbox slot when it timed out never registers one.
- The start causes that wait: messages held on it occupy the outbox's 3 delivery slots.
- Reproduced by Mongo in a real session:
- Hold the create, send three messages to the new chat, let the default 10 s deadline pass, then accept the create.
- Messages 1 and 2 publish. Message 3 stays
failedand is never published.
- The PR description promises that "a message that times out while waiting is sent again once the chat exists." That holds only for the first two.
Fix: register the resend when the message is queued for a starting chat, not on entry to preparePublish. One option is a resend in start() of every failed item for that h once ready resolves.
There was a problem hiding this comment.
🤖 Fixed in b1effc1. The resend now lives in channelWrites.start: once the chat can take events, every failed outbox item for that h is retried, including messages that never got a delivery slot. Test: "messages that time out while their chat starts go once the chat exists" now sends four messages (more than the three slots) and checks that all four publish.
| /** Hands the owner's signed message in a chat still starting to live | ||
| * listeners (local agents) before the relay can echo it. Listeners drop the | ||
| * later echo of the same event. */ | ||
| function hearStarting(event: RelayEvent, channelId: string) { |
There was a problem hiding this comment.
Non-blocking: this breaks subscribeLive's documented contract.
- At
session.ts:1876,subscribeLivepromises "Verified, access-filtered live-phase events... Never finite reads or local intent." hearStartingsends local intent to every live and replay listener,SharedSessionsincluded. It also skips the epoch and generation checks that the normal live path applies.- If this hand-off is intended, update that doc so listeners know they may get unaccepted events. A refused start can also leave an agent turn already run, with whatever tool side effects it had, and the user may then resubmit.
There was a problem hiding this comment.
🤖 Updated in b1effc1. The subscribeLive doc now says that, in a Solo chat that is still starting, the viewer's own messages can arrive before the relay has them, and it points at hearStarting.
a0250de to
71c1801
Compare
kalvinnchau
left a comment
There was a problem hiding this comment.
🤖 Review findings for 71c18017: two P1 blockers and four P2 defects. New findings are inline; corroborating evidence and the additional retry trigger are added to existing inline threads to avoid duplicate findings. Browser evidence uses a modeled relay; session evidence uses scripted relay responses and fake timers. Real-relay deployment and native/provider execution remain unverified.
| const starting = | ||
| destination && channelWrites.starting(destination); | ||
| if (destination && starting) { | ||
| if (event.kind === 9) hearStarting(event, destination); |
There was a problem hiding this comment.
🤖 [P1] Prevent pre-create Canvas reads from revoking access
Local admission here starts the agent’s Canvas read before creation settles. When the relay refuses that single-channel read because the channel does not yet exist, readVerified → store.denyChannel still invokes session access revocation despite discovery’s pending guard. A real-session probe with a scripted relay and fake timers at this commit reproduced the chat disappearing until confirmation, an error window, and the first message permanently failing with Refresh channel membership before mentioning anyone; only kind 9007 was published. Centralize pending-read handling so a pre-create miss cannot revoke access or invalidate the queued first message. Preserve authoritative revocation handling separately.
There was a problem hiding this comment.
🤖 Fixed in b1effc1. readVerified now waits for a chat's create before it reads that chat (any #h filter for a chat this device is starting), so a local agent's early Canvas read is held rather than refused. denyChannel also ignores pending chats as a second guard; a denial after the chat exists still revokes access. Test: solo-start.test.ts "a read of a chat that is still starting waits for the create instead of losing access" (the fixture relay refuses #h reads until the create lands; the test fails without the wait).
| // Put the start back in the new-chat composer, unless a newer | ||
| // draft is there by now. | ||
| if ( | ||
| blankDraft(readView<unknown>(scope, `${draftKey}:new-draft`, "")) |
There was a problem hiding this comment.
🤖 [P2] Preserve the original prompt when an untouched new draft blocks recovery
Start a chat, open another New chat without editing it, then refuse the original create. The new draft’s automatic agent tag makes blankDraft false, so this branch does not restore the original prompt after its message has been withdrawn. The original is absent from both draft locations. Reproduced at this commit in Chromium and WebKit against a modeled relay. Track actual user edits/revisions rather than treating the automatic tag as new content, and retain recoverable content when replacement is blocked.
There was a problem hiding this comment.
🤖 Fixed in b1effc1. A draft holding only mention tags now counts as blank, so an untouched New chat is replaced by the restored start, along with its settings and workspace. A draft with text of its own keeps it, and the restored start is appended after it, so neither is dropped. I went with that rather than tracking edits: the content tells us what's recoverable, and there's no extra state to keep. Tests: SoloDraft.test.tsx "a failed start comes back to a new-chat draft holding …" (both cases).
There was a problem hiding this comment.
🤖 Superseded by fb27125. A refused start is no longer restored into New chat: the chat and everything held for it are dropped, and the app shows its error toast (Couldn't start "<name>": <reason>). The restore path this thread was about has been deleted (restore-start.ts, the start file store, the append transfer, composer-recovery.ts). See the reply on the newest restore thread for details.
| extensions={extensions} | ||
| onStarted={select} | ||
| onFailed={(channelId) => { | ||
| if (showing.current === channelId && navigator) |
There was a problem hiding this comment.
🤖 [P2] Retire failed-start navigation callbacks when the workspace unmounts
Start in community A, switch to B and open B’s new draft, then refuse A’s pending create. The old workspace’s showing ref still names the pending channel, so this callback invokes the old navigator and switches the app back to A. Reproduced at this commit in both Chromium and WebKit against a modeled relay. Check active visit/scope ownership and invalidate this callback on unmount before invoking navigation.
There was a problem hiding this comment.
🤖 Fixed in b1effc1. ChannelWorkspace clears its showing ref on unmount, so a start that fails after the workspace has gone (for example after switching community) no longer navigates. A mounted workspace that is still showing the chat behaves as before. This one has no automated test.
…ing on refusal - Reads of a chat that is still starting wait for its create, and discovery ignores denials for pending chats, so a local agent's early Canvas read can't revoke access (Kalvin P1). - A chat with a workspace or Canvas setup holds its messages, and the local hand-off, until setup has run; setup loads alongside the create. - Create attempts back off; when they run out, the relay is asked for the create until it answers, and a confirmed absence ends the start with ChatNotStarted instead of leaving a phantom pending chat. - A refused start withdraws every held event for the chat and restores the first message, sent follow-ups, the unsent chat draft and its files. An untouched new-chat draft (only its automatic tag) counts as blank; one with text keeps it and takes the start after it. - Every message that timed out while the chat started is resent once it can take events, not only those that reached preparePublish. - A failed start only navigates from a workspace that is still mounted. Co-authored-by: mbot <3754f8729004d95654c46dbab3129e4ab9ef05cc2534e2a3fbfc155983bd637b@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.
🤖 Star Lord’s automated source review via Wes’s account, integrating independent Rocket, Mantis and Groot reviews.
Follow-up review at head b1effc19a418eb0e76bd802fe1bf290b94b2b2e7, base b2787b2a3da9c9a0c2b3b8f9b504b51959bad222. 8/10: two P2 repair defects are detailed inline. The pending-read guard, create reconciliation/backoff, queued-message resend and unmounted-workspace navigation repairs are present in source.
This checks the prior findings and repair behavior, not unrelated upstream changes. The older-relay fallback was explicitly declined by the human author; the dependency on block/buzz#8421 remains. Setup failure is also deliberately caught and logged: this can still dispatch a turn without the intended Canvas after a setup load/save failure. Successful setup gating should not be read as proof of failure-path instruction preservation.
Validation: immutable source and caller/lifecycle tracing only; no PR code, tests, builds or app execution. Native/local-agent behavior, live relay compatibility, rendered focus/accessibility, performance measurements and CI status were not verified. Public PR description inspected; no attached images or disclosure finding there. This is a COMMENT review, not approval or merge authorization.
| // Waiting counts against the event's delivery deadline; one | ||
| // that timed out meanwhile goes again once the chat can take | ||
| // it (see channelWrites.start). | ||
| await starting; |
There was a problem hiding this comment.
🤖 [P2] Keep setup-dependent messages from consuming setup’s delivery slots
Start a chat with a nonempty Canvas and send two follow-ups while creation/setup reads are pending. The three kind-9 attempts can occupy all three outbox controllers (outbox.ts:430–439) while awaiting this setup promise. applyChatSetup then calls session.canvas.save (solo/workspace.ts:332–339), which queues kind 40100 in that same outbox and waits for delivery (channel-templates/capability.ts:417–425; wiring at session.ts:1358–1368). No slot remains to publish the Canvas that would release these messages, so progress requires a message to hit its default 10-second deadline even when the relay has accepted creation and the reads succeed.
This circular dependency is introduced by waiting for setup, beyond the documented create-wait slot limitation. Hold setup-dependent messages before they acquire delivery slots, or otherwise let required setup writes progress. Cover the real Canvas-save path with more messages than available slots. Source-traced; not runtime-reproduced.
There was a problem hiding this comment.
🤖 Runtime corroboration at b1effc19: a real-session probe with a scripted relay and fake timers held three kind-9 messages for setup. Kind 40100 never published; setup failed with Delivery timed out at 10,000 ms, then messages were released without the intended Canvas. An unrelated channel's message also waited 10 seconds. Control with only the first message published 40100 at 0 ms.
There was a problem hiding this comment.
🤖 Fixed in 2bf5e53, centrally rather than per symptom. The wait moved out of preparePublish into a new outbox holdPublish hook. A held event waits after signing without holding a delivery slot, and its 10s deadline starts when the hold ends. Setup's Canvas save (40100) now gets a slot as soon as the create is OK, however many messages are held. That also let me delete the "resend every message that timed out while waiting" loop in start(). A rejected hold fails the message directly. Test: solo-start.test.ts "a chat's setup writes go while more messages than delivery slots wait for setup" uses the real canvas.save with 4 held messages. It fails on b1effc1.
| joinDrafts(mentionDraft(current), restored), | ||
| ); | ||
| const chatKept = chatFiles.snapshot(); | ||
| if (files.adopt([...kept, ...chatKept]) && chatKept.length) |
There was a problem hiding this comment.
🤖 [P2] Restore files into the current New chat attachment owner
The callback retains the original files store after files.clear() and navigation unmount its composer. Its unsubscribe removes that empty store from the owner map (attachment-draft.ts:260–263). If the user opens another New chat and attaches a file before the original create is refused, the new composer owns a different store under the same key. Calling adopt on the captured old store succeeds and overwrites that registered owner (attachment-draft.ts:337–342), while the mounted composer still retains the newer store in useState. The restored files are invisible there; on a later remount the old store is returned and the newer draft’s files disappear. The same overwrite orphans newer files if that composer was already unmounted.
Resolve the current destination store for the New chat draft key when recovery runs, and merge or retain both sets with explicit recovery when it is occupied. Transfer any active upload ownership rather than only copying item snapshots and clearing their source. This preserves the draft-retention contract in DESIGN.md’s collections/workspaces guidance. Source-traced; not runtime-reproduced.
There was a problem hiding this comment.
🤖 Reproduced at b1effc19 in both Chromium and WebKit using the production app/browser broker with modeled relay/uploads: start with original.pdf, open New chat and attach newer.pdf, refuse the original create, then navigate away and back. newer.pdf disappears and original.pdf replaces it. This confirms the attachment-owner overwrite in the actual UI.
There was a problem hiding this comment.
🤖 Fixed in 2bf5e53, together with Kalvin's two draft findings. All three came from the start writing into New chat's storage and an attachment store it had captured, from outside the composer. Now:
- The start keeps the sent message's files in a store of its own until the relay confirms the chat.
- On refusal,
restoreStartresolves the attachment store that currently owns the New chat draft key. It appends the start's files and the chat's own files there withtransferAttachmentDraft(…, append), which moves in-flight upload ownership instead of copying snapshots. - Then it uses the composer's existing recovery notice.
Test: "a refused start's files join the files of the New chat that owns the draft now". It covers original.pdf → New chat with newer.pdf → refuse → both visible → remount → both still there, and it fails on b1effc1. Plus an attachment-draft unit test for appending with an active upload.
There was a problem hiding this comment.
🤖 Superseded by fb27125. A refused start is no longer restored into New chat: the chat and everything held for it are dropped, and the app shows its error toast (Couldn't start "<name>": <reason>). The restore path this thread was about has been deleted (restore-start.ts, the start file store, the append transfer, composer-recovery.ts). See the reply on the newest restore thread for details.
kalvinnchau
left a comment
There was a problem hiding this comment.
🤖 Five P2 defects and one P3 remain at b1effc19: four findings are added inline; runtime corroboration follows in the two existing threads. Not approved. Browser checks used a modeled relay; session probes used scripted responses and fake timers. Native execution and real-relay deployment remain unverified.
| : []), | ||
| ]).finally(() => stop()); | ||
| aborted?.throwIfAborted(); | ||
| } |
There was a problem hiding this comment.
🤖 [P2] Fail parked reads locally when creation ends with ChatNotStarted
A local agent's Canvas read can wait on a starting chat. On refusal, the rejection handler abandons the chat before this read resumes, so the pending guard no longer protects it. Sending it to the relay produces a membership denial and invokes session-wide revokeAccess. A real-session probe with scripted relay responses reproduced an unrelated channel's in-flight read failing with AbortError: Relay read cancelled; the control without the parked Canvas read succeeded. Propagate ChatNotStarted as a local unavailable read instead of proceeding to the relay.
There was a problem hiding this comment.
🤖 Fixed in 2bf5e53. A read parked on a start now takes the start's outcome. If the start made no chat, the read fails locally with ReadError("unavailable") and never reaches the relay, so no denial and no revokeAccess. Test: "a read waiting on a start the relay refuses fails here, without asking the relay" (fails on b1effc1).
| return !!event && openMetadata(event); | ||
| } | ||
| canParticipate(id: string): boolean { | ||
| if (this.starting.has(id)) return true; |
There was a problem hiding this comment.
🤖 [P2] Let authoritative membership removal override pending intent
Receive the initial signed roster before metadata, then a newer roster removing the owner while metadata remains delayed. The denial is recorded, but pending creation is cleared only when metadata exists and authorization remains true. An isolated execution of DiscoveryState returned authorized:false while canAccess, canParticipate and pending stayed true, with the speculative roster and unchanged accessRevision. Later private metadata does not repair it. This demonstrates stale client authorization, not a relay ACL bypass. Clear speculative authority and invalidate access on authoritative removal; preserve the pre-create exception only while membership evidence is absent.
There was a problem hiding this comment.
🤖 Fixed in 2bf5e53. Pending is now a placeholder that only stands in for access until any signed roster arrives. After that the roster decides access. A roster without the viewer ends the start, either a later one that removes them or a first one that never had them, and it bumps accessRevision through the normal path. Pending still keeps the chat listed until its metadata arrives. Tests: discovery-start.test.ts covers your sequence (roster with viewer → newer roster without, metadata missing → canAccess/canParticipate/get all false and the access revision bumped). Two of its three cases fail on b1effc1.
| writeView( | ||
| scope, | ||
| `${draftKey}:new-draft`, | ||
| joinDrafts(mentionDraft(current), restored), |
There was a problem hiding this comment.
🤖 [P2] Deliver recovered text to the mounted New chat composer
Submit a prompt with creation held, open New chat, then refuse the first creation. Recovery writes the prompt to localStorage, but the mounted submission composer skips storage reconciliation and stays unchanged. Its next keystroke overwrites the recovered prompt; reload confirms the loss. Reproduced in Chromium and WebKit with the production app/browser broker and a modeled relay. This is the remaining failure after the automatic-tag recovery repair: storage restoration alone is insufficient. Transfer recovery through the active composer with revision checks, or retain it until consumed.
There was a problem hiding this comment.
🤖 Fixed in 2bf5e53. The restore now goes through the composer's own recovery path. The recovery registry moved to messages/composer-recovery.ts, and New chat (submission) composers now load an explicit recovery. They save every edit, so loading the merged saved draft loses nothing, and they show the reason. That also replaced the start-error view key. Test: "a refused start reaches a New chat that opened meanwhile, and its next keystroke keeps it" (fails on b1effc1).
There was a problem hiding this comment.
🤖 Superseded by fb27125. A refused start is no longer restored into New chat: the chat and everything held for it are dropped, and the app shows its error toast (Couldn't start "<name>": <reason>). The restore path this thread was about has been deleted (restore-start.ts, the start file store, the append transfer, composer-recovery.ts). See the reply on the newest restore thread for details.
| `${scope}:draft:${channelId}`, | ||
| channelId, | ||
| ); | ||
| const restored = joinDrafts(draft, ...followUps, unsent); |
There was a problem hiding this comment.
🤖 [P3] Avoid restoring the same failed submission twice
When messages.send throws synchronously (for example mention validation while live delivery is disconnected), its catch already puts the original prompt in the chat draft. A later ChatNotStarted merges that draft with the captured original again. The focused probe restored Try again@Member agent\n\nTry again@Member agent with the recipient twice. The file merge follows the same duplicate-source pattern in source, though file duplication was not runtime-tested. When no message was created, restore from the chat draft and its files only.
There was a problem hiding this comment.
🤖 Fixed in 2bf5e53. The restore is built only from what the chat holds: the first message only if it was actually sent, then later held messages, then the chat's unsent draft and files. When send throws, the first message lives only in the chat draft, so it comes back once. Test: "a refused start whose first message never went out restores it once" (fails on b1effc1).
There was a problem hiding this comment.
🤖 Superseded by fb27125. A refused start is no longer restored into New chat: the chat and everything held for it are dropped, and the app shows its error toast (Couldn't start "<name>": <reason>). The restore path this thread was about has been deleted (restore-start.ts, the start file store, the append transfer, composer-recovery.ts). See the reply on the newest restore thread for details.
…ia composer recovery Three central changes answer the second review round instead of patching each symptom: - Outbox `holdPublish`: an event for a starting chat waits after signing without holding one of the three delivery slots, and its deadline starts when the hold ends. Setup's own writes (Canvas) can no longer queue behind held messages (Wes), so the resend-timed-out loop in `start()` is gone; a rejected hold fails the message. A read waiting on a start that made no chat fails locally instead of reaching the relay and revoking access (Kalvin). - Discovery: a pending chat stands in for access only until a signed roster arrives; a roster without the viewer ends the start (Kalvin). - A refused start returns through the composer's recovery notice (registry moved to messages/composer-recovery.ts; restore in solo/restore-start.ts). The start keeps its sent files until the relay confirms the chat; on refusal text merges into the saved New chat draft and files append to the store that owns that draft now, with in-flight uploads carried over (transferAttachmentDraft `append`). A mounted New chat composer loads the recovery and shows its reason. Only what the chat holds is restored, so an unsent first message comes back once (Kalvin, Wes). Co-authored-by: mbot <3754f8729004d95654c46dbab3129e4ab9ef05cc2534e2a3fbfc155983bd637b@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.
🤖 Star Lord’s automated source review via Wes’s account, integrating independent Rocket, Mantis and Groot reviews.
Follow-up at head 2bf5e53b6da86901e54d7dffa2122e7ff6bcd5b6, base b2787b2a3da9c9a0c2b3b8f9b504b51959bad222, compared with the prior reviewed b1effc19a418eb0e76bd802fe1bf290b94b2b2e7. 8/10: one P2 recovery defect is detailed inline. The previous Canvas delivery-slot circular wait and replacement of a newer attachment owner are repaired on the traced paths, but recovery still needs protection while a newer submission is preparing its files.
The explicit dependency on block/buzz#8421 and the author’s choice to catch/log setup failures are preserved. Setup failure can still allow a turn without the intended Canvas; this review does not reinterpret that choice as successful instruction preservation.
Validation: immutable source, changed tests and caller/lifecycle tracing only; no PR code, tests, builds or app execution. One hosted-check snapshot at this head showed CI required failed and Browser journeys (webkit, 7/9) cancelled; cause not investigated. Native/live-agent behavior, rendered focus/accessibility, cold/warm performance, slow signing/persistence and human acceptance remain unverified. Public PR description and follow-up commit inspected; no attached images or disclosure finding. This is a COMMENT review, not approval or merge authorization.
| // The start keeps the message's files until the relay confirms the | ||
| // chat, in case they have to come back. | ||
| if (uploaded.length) | ||
| transferAttachmentDraft( |
There was a problem hiding this comment.
🤖 [P2] Preserve late recovery while a newer New chat is submitting
Start chat A, open New chat B, and submit B with a file whose upload is still pending. If A is refused during B’s prepareForSend await, restoreStart merges A’s text into B’s saved draft and appends A’s files to this same current store (restore-start.ts:116–145). B has already captured its text and the upload iterator’s original array (SoloDraft.tsx:152–200, attachment-draft.ts:352–369). When the upload completes, B sends only that captured text and the original uploaded descriptors, but this transfer moves every file currently in the store—including A’s newly restored files—into B’s parked start store. The unconditional draft clear at line 272 erases A’s restored text; B’s successful ready then clears the parked files at line 276. A’s recovered message and attachments disappear without being sent.
Fence the submission against changes to the saved draft revision and attachment identities after the asynchronous preparation, before creating B or clearing/transferring its contents; retain the recovered content for review/retry if either changed. Cover a refused older start arriving while a newer submission’s upload is held. This completes the recovery repair and preserves DESIGN.md’s draft-retention contract. Source-traced, not runtime-reproduced.
There was a problem hiding this comment.
🤖 The design changed in fb27125, so this race no longer exists. A refused start is no longer restored into New chat. Now, when the relay refuses the create (or confirms it never received it), channelWrites.start removes the pending chat, withdraws everything held for it, and the app shows its existing error toast: Couldn't start "<name>": <reason>. What was sent in that chat is lost; baxen chose that because refusals should be rare. Nothing writes into New chat from the background any more, so New chat has a single writer again. Removed: restore-start.ts, the start's own file store, the append transfer in attachment-draft.ts, composer-recovery.ts, and the New chat recovery loading in MessageComposer (both of those files are back to main). A start that is only unconfirmed still keeps its chat, and its messages keep their Retry. Net change: −451 lines.
A start the relay refused, or confirmed it never got, now removes the pending chat, withdraws everything held for it (channelWrites.start owns this), and shows the app's error toast: Couldn't start "<name>": <reason>. What was sent in that chat is lost. Nothing writes into New chat from the background any more, so the restore path goes: solo/restore-start.ts, the start's own file store, the attachment-draft append transfer, composer-recovery.ts and New chat loading recovery notices (MessageComposer and attachment-draft are back to main). A start that is only unconfirmed still keeps its chat and its messages' Retry. Co-authored-by: mbot <3754f8729004d95654c46dbab3129e4ab9ef05cc2534e2a3fbfc155983bd637b@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.
🤖 Star Lord’s automated source review via Wes’s account, integrating independent Rocket, Mantis and Groot reviews.
Follow-up at head fb271258eb3ca8361ade4fe153d799af2dc413c7, base b2787b2a3da9c9a0c2b3b8f9b504b51959bad222, compared with the prior reviewed 2bf5e53b6da86901e54d7dffa2122e7ff6bcd5b6. 8/10: two P2 findings are detailed inline. The prior New chat recovery race is superseded by removing recovery. The ordinary-composer finding is an earlier review miss exposed by tracing its actual admission path; it was not introduced by the latest cleanup.
The explicit discard-on-refusal decision, dependency on block/buzz#8421 and catch/log setup-failure choice are preserved. The second finding concerns local failures before publication being treated as relay refusal. The existing limitations remain: genuine refusal loses sent content and the pending draft; setup failure may dispatch a turn without the intended Canvas/default agents; early local-agent work can precede relay acceptance.
Optional documentation follow-up: docs/solo/README.md:52–60 still describes the former start ordering, retained composer text after interruption, and setup that never holds the message. Update that bullet to describe create-member tags, setup gating and the chosen refusal loss.
Validation: immutable source, changed tests and caller/lifecycle tracing only; no PR code, tests, builds or app execution. One hosted-check snapshot showed WebKit 7/9 still running and Windows validation skipped; no CI-completion claim. Native/live-agent behavior, rendered focus/accessibility, cold/warm performance and human acceptance remain unverified. Public PR description inspected; no attached images or disclosure finding. This is a COMMENT review, not approval or merge authorization.
| // A chat this device is still starting has nothing to read until the | ||
| // relay has its create; reading sooner would be refused as a denial. | ||
| const starts = [ | ||
| ...new Set(filters.flatMap((filter) => filter["#h"] ?? [])), |
There was a problem hiding this comment.
🤖 [P2] Wait for creation before refreshing the pending Solo chat’s roster
With creation delayed, submit a follow-up from the newly opened chat. ChannelsPage.tsx:1443–1473 mounts its enabled ordinary composer; MessageComposer.tsx:1283–1286 calls prepareRecipients, which unconditionally refreshes membership at line 1145. That reads kind 39002 using #d (channel-writes.ts:308–339), so this #h-only guard does not wait for creation. An empty pre-create roster response throws “Could not refresh channel membership. Retry to continue.” The first message later sends, but the follow-up stays unsent until another user submission. Its text is retained.
Wait for this channel’s start before that membership refresh, retaining the fresh signed-roster admission check. Cover the mounted ordinary composer with creation held: the current multi-message test (solo-start.test.ts:306–329) calls session.messages.send directly and bypasses this path. Source-traced, not runtime-reproduced.
There was a problem hiding this comment.
🤖 Fixed in 21f19f4. refreshMembership now waits for a chat this device is starting before it reads the roster, so every caller waits for the create: the composer's prepareRecipients, Canvas preparePublish, and addAgents. The signed-roster check is unchanged. New test in channel-writes.test.ts: "reads a starting chat's roster only once the relay has its create". It fails if the wait is removed.
| try { | ||
| return await delivered(create); | ||
| } catch (error) { | ||
| if (api.failed(create)) |
There was a problem hiding this comment.
🤖 [P2] Keep local pre-publish failures distinct from relay refusal
api.failed(create) checks only delivery === "failed" (lines 703–709). That also describes an attempt that fails or times out while signing/persisting, before writer.publish is entered (outbox.ts:479–506, 528–532, 549–578). On the first attempt, delivered() rejects when that state appears; this branch immediately throws ChatNotStarted, bypassing the retry/backoff and strong-read paths below. The new cleanup then withdraws the create and all messages, while SoloDraft clears the pending draft. A transient local signing timeout can therefore permanently discard the prompt and attachments without any relay rejection or completed reconciliation; its “check or retry this message” error has no surviving message to retry.
Preserve an explicit refusal classification and let retryable pre-publish failures retry the same create or remain recoverable. Keep the chosen discard behavior for genuine refusal/confirmed absence. Add coverage where signing fails or times out once, then succeeds, alongside the existing explicit-refusal case. Source-traced, not runtime-reproduced.
There was a problem hiding this comment.
🤖 Fixed in 21f19f4. The outbox now marks an operation refused only when the attempt ended in PublishRejected and its delivery is failed. A retry clears the mark, and it is persisted. created throws ChatNotStarted only for refused. A failure on this device (signing, persisting, or a timeout before publish) goes through the existing retry path for the same create. After three tries it is dropped only if the strong read finds nothing. New test: "retries a create that failed on this device before the relay saw it". It fails with the old failed check. The refusal test now marks the item refused, and the outbox retry test checks that the mark is set and then cleared.
There was a problem hiding this comment.
🤖 Follow-up in bd7b021. I added a new PublishNotSent subclass of PublishRejected, so writes are still definitely unsent and keep the failed delivery state. The transport now raises it for local ApiPaused/ApiNotSent, for HTTP sent:false/API-quota answers, and for an OK false whose message starts with rate-limited:. The outbox's refused mark skips it, so a start in cooldown retries the same create. A 409, a 4xx, or any other accepted:false still counts as a refusal.
Coverage:
signed-admission.test.ts: the cooldown and the explicit quota answer both reject asPublishNotSent.outbox.test.ts: aPublishNotSentfailure isfailedand notrefused.solo-start.test.ts, real session/outbox/start: "a create this device held back for the relay's cooldown goes again, and the chat starts". It sends the create twice, then the message, and the chat stays listed. It fails if the outbox marksPublishNotSentas refused.
A workspace's defaults load when it is picked. Its agents join in the create with the tagged ones, the chat files under the workspace at once, and local agents read the chat's Canvas from the start while its save runs in the background. Nothing waits on setup any more, so the settling gate and applyChatSetup go. Co-authored-by: mbot <3754f8729004d95654c46dbab3129e4ab9ef05cc2534e2a3fbfc155983bd637b@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.
🤖 Star Lord’s automated source review via Wes’s account.
Reviewed head f0798820b3ffae4e410dbf697d7f8e5f8b3a3da2, base b2787b2a3da9c9a0c2b3b8f9b504b51959bad222, focusing on the 14-file follow-up since fb271258eb3ca8361ade4fe153d799af2dc413c7 and its supported callers.
8/10: one new P2 inline—preloaded workspace defaults survive a subsequent settings save, so the next chat can receive superseded Canvas instructions and default agents. Invalidate the cached setup when its source changes, retaining the fast path for unchanged defaults.
The two findings in the previous review remain unchanged by this commit: follow-up membership reads bypass the pending-create wait, and local pre-publish failures can be classified as refusal. Their original inline comments remain the single discussion locations.
Optional documentation: update docs/solo/README.md:51–60 to describe defaults loading before creation, default agents joining in the create, early workspace filing, background Canvas save, and retained-draft retry when defaults cannot load. The stated non-local first-turn Canvas limitation and chosen background-save/refusal behavior are preserved as product scope.
Coverage: Rocket systems/lifecycle/performance, Mantis correctness/security/concurrency, Groot product/accessibility/error-retry, integrated by Star Lord. Reviewed committed source and test changes, supported local harness callers, settings-save integration, public description and follow-up commit. No attached images or disclosure finding. Existing composer, workspace control, Dialog and inline alert owners remain; no rendered design validation performed.
Source-only: no PR code, tests, builds or app executed; current CI not assessed. Native/live relay behavior, cold/warm timing, visible pending feedback, keyboard/focus/assistive-technology behavior and human acceptance remain unverified. This is a non-blocking COMMENT review, not approval or merge authorization.
| undefined, | ||
| ); | ||
| useEffect(() => { | ||
| if (!selectedProject || workspace.current?.id === selectedProject) return; |
There was a problem hiding this comment.
🤖 [P2] Invalidate preloaded defaults after workspace settings change
Leave New chat open with a workspace selected, then edit that workspace’s Canvas/default agents through its sidebar Settings and save. SoloWorkspaces.tsx:271,478–484 opens that dialog without changing the draft route; WorkspaceSettings.tsx:216–224 saves the new template to channelKit. The draft remains keyed by project/agent (ChannelsPage.tsx:1550). This effect still returns for the same project, so the successful promise captured before the save is reused at lines 193–198. The next send creates the chat with the old default agents and saves/hands local agents the old Canvas (lines 210,244–251), despite the successful settings update. The prior head loaded setup at send time.
Invalidate or replace the cached setup when its underlying workspace/template revision changes, and only consume a preload that matches the current revision. Keep the unchanged-default fast path. Add a mounted-draft case that saves changed workspace defaults before sending and asserts the create’s members and initial Canvas use the new values. Source-traced, not runtime-reproduced.
There was a problem hiding this comment.
🤖 Fixed in 21f19f4. The preload stores the channel-kit revision it read (its entries' event ids). A start uses the preload only if that revision is still current, and otherwise loads at send time as before. If nothing changed, the fast path stays. New SoloDraft test: "a workspace's defaults changed while New chat is open start the chat". It changes the template after the preload and checks that the new Canvas is saved. It fails without the revision check.
There was a problem hiding this comment.
🤖 Follow-up in bd7b021. loadChatSetup now returns revision (defaultsRevision, the kit entries' event ids), computed from the same kit snapshot whose template it uses, before the template-id and agent-choice awaits. SoloDraft compares that against the current snapshot when you send, and loads again if they differ. New workspace.test.ts case: "names the saved defaults it read, even when they change while agents load". The kit changes during agentChoices.refresh, and the result is the old Canvas with the old revision. It fails if the revision is stamped after loading.
…ps it - A starting chat's membership refresh waits for its create, so a second message sent while the chat starts no longer fails its roster check. - The outbox marks an attempt the relay refused. Only that ends a start; a failure on this device before the create was sent retries it. - Preloaded workspace defaults carry the kit revision they were read at; a start reloads them if the saved defaults have changed since. Co-authored-by: mbot <3754f8729004d95654c46dbab3129e4ab9ef05cc2534e2a3fbfc155983bd637b@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.
🤖 Star Lord’s automated source review via Wes’s account, integrating independent Rocket, Mantis and Groot reviews.
Reviewed head 21f19f4a5388f0d464fb89f5c02372d06aa88393, base b2787b2a3da9c9a0c2b3b8f9b504b51959bad222, focusing on the seven-file follow-up since f0798820b3ffae4e410dbf697d7f8e5f8b3a3da2 and its supported callers. 8/10: two previous P2 repairs remain incomplete. The existing discussions remain the single inline locations; the new evidence is below.
- Local failure versus refusal: signing failures now retry, but
outbox.ts:572–575marks every failedPublishRejectedas refused. The supported native writer wrapsconnectSignedTransport(native.ts:342–348,805), whosetransport.ts:1357–1358converts localApiPaused/ApiNotSentinto that class. After an unrelated read starts the shared API cooldown, a new chat’s create can therefore be rejected locally without a request reaching the relay, yet immediately abandon the chat and withdraw its queued content. Distinguish actual relay refusal from retryable local non-dispatch while preserving definitive non-delivery semantics. Cover cooldown → same create succeeds through the real transport/outbox/start path;signed-admission.test.ts:17–78already documents the local cooldown classification. - Workspace-default invalidation: saves after a completed preload now invalidate it, but
SoloDraft.tsx:159–162stamps the revision afterloadChatSetupfinishes. That loader captures the kit/template before awaiting template-ID calculation and agent-choice refresh (workspace.ts:267–295). Saving workspace defaults while that refresh is pending replaces the kit snapshot; the completed preload then pairs old Canvas/agents with the new revision. The send-time equality check accepts the stale setup. Return a revision derived from the same kit snapshot actually consumed, and cover a settings save while the preload’s agent refresh is held.
The pending-chat membership fix is present: refreshMembership now waits for the start before reading the signed roster. Preserve the stated relay dependency, confirmed-absence/refusal cleanup, background Canvas-save behavior and non-local first-turn Canvas limitation. The previous optional documentation note remains; no new design or public-material finding. Public description and follow-up commit inspected; no attached images.
Source-only review: no PR code, tests, builds or app execution; CI not assessed. Native/live relay behavior, cold/warm timing, rendered keyboard/focus/accessibility, themes/responsiveness and human acceptance remain unverified. Existing composer, workspace control and inline alert owners are retained. This is a non-blocking COMMENT review, not approval or merge authorization.
…read - PublishNotSent (a PublishRejected) marks writes proven unsent but not refused: the API cooldown or queue held them back here, or the relay rate-limited them. The outbox's refused mark skips them, so a start retries the same create instead of dropping the chat. - loadChatSetup returns the revision of the kit snapshot it actually read, so defaults saved while agents load cannot pass as current. Co-authored-by: mbot <3754f8729004d95654c46dbab3129e4ab9ef05cc2534e2a3fbfc155983bd637b@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Bradley Axen <baxen@squareup.com>
Goal
A new Solo chat opens with no visible wait, and the local agent starts its turn right away (TODO items 4 and 5). Before this change, sending from the new-chat composer waited on project setup reads, the agent list, the create, its confirmation, a roster read, and a separate agent add, all before the message was sent.
Requires block/buzz#8421, where the create (kind 9007) adds its
ptags as members and an exact resend of a create is confirmed instead of refused. Deploy the relay first: against an older relay the agents are never added, so they never answer.What changes
channelWrites.startqueues the create with the agents asptags and lists the chat at once aspending(in discovery and the store). Itsreadysettles once the relay OKs the create. A background read then confirms the chat and ends its pending listing; nothing waits on it.refusedmark) or a confirmed absence ends the start. A write held back by the API cooldown or a relay rate limit isPublishNotSent: definitely unsent, but not refused, so it retries.refreshMembership) wait for the create, so a second message sent while the chat starts goes through.preparePublishholds the chat's events untilready.runner.publish).channelType: "solo"from the first moment, so replies stay at the top level.channelWrites.startingCanvas) while it is being saved, which happens in the background once the chat exists. A failed save leaves a working chat and logs a warning.applyChatSetup.Couldn't start "<name>": <reason>.Validation
bin/pnpm exec vitest run: 660 files, 10110 tests passed (full suite, after the review fixes);tsc --noEmitandbiome checkclean.src/features/relay/solo-start.test.tsruns a real session (outbox, channel writes, store, discovery) against a scripted relay:readywaits for it), and refusal.#hread. The message goes out while the Canvas save is still pending. A plain chat's first turn knows at once that it has no Canvas. Affected suites pass, andtscandbiomeare clean.channelWrites, and the service swallows agent publish errors, so the new hook threw unseen in existing tests. The fixture now provides it.Deferred
FOUNDATION:
src/features/relay/session.tsedit approved by baxen in the planning thread.🤖 Generated with Claude Code