Skip to content

fix(web): follow a new thread when the first send's reply is lost - #503

Merged
incognitojam merged 2 commits into
mainfrom
fix/web-draft-send-lost-reply
Sep 28, 2026
Merged

incognitojam merged 2 commits into
mainfrom
fix/web-draft-send-lost-reply

Conversation

@incognitojam

@incognitojam incognitojam commented Sep 27, 2026 •

Copy link
Copy Markdown
Owner

Note

If the connection drops after the server receives a new thread's first message but before it replies, the web client stayed on the draft with the sent prompt still in the composer. It now follows the thread once it reconnects and clears the restored prompt.

Problem

When the first send from a new-thread draft fails, the web client restores the prompt. Since #195 it also gives the draft a fresh thread id after any failure that is not an interrupt, including a dropped socket. #195 meant to do this only after a definitive bootstrap failure. After a transport failure the server may have created the thread, turn and worktree already. The draft then pointed at a different id, so after reconnecting the client could not match the thread the server created and stayed on the draft with the sent prompt in its composer. The error was set on the discarded id and transport errors are hidden, so nothing explained it.

This was seen with a Mac client dispatching a new thread to a remote host: the host's database shows the thread created and its turn started within two seconds, while the client showed a reconnecting banner and never cleared the composer or switched to the thread.

Fix

  • The draft reserves a fresh thread id only when the server rejected the bootstrap (OrchestrationDispatchCommandError), which is the case fix(web): preserve drafts after thread bootstrap failure #195 handled and matches upstream. After a transport failure the draft keeps its id, so the existing draft promotion follows the thread once its turn syncs.
  • When a send whose outcome is unknown restores the prompt, the draft store records the restored content. Finalizing the promotion requires a started turn, which proves the message arrived, so the restored content is cleared then if the user has not edited it. Content typed while reconnecting is kept.

The server can still abandon a bootstrap partway when it notices the disconnect before finishing, leaving a thread with no turn. In that case the client keeps the prompt and stays on the draft. Upstream pingdotgg#11372 makes the bootstrap outlive the requesting connection, so it is added to the tracked upstream PRs. Mobile's outbox already waits for the shell to resync and checks whether the thread exists; desktop inherits the web client.

Validation

A headless browser used a worktree dev server through a TCP proxy that can stop server-to-client traffic on the app WebSocket. From a new-thread draft in New worktree mode: stop replies, send, wait for the server to create the thread and start the turn, cut the socket, and let the client reconnect.

Before After
Before: after reconnecting, the created thread is running in the sidebar but the client is still on the draft with the sent prompt in the composer After: after reconnecting, the client is on the created thread and the composer is empty
  • Before the fix, the client stayed on the draft with the prompt in the composer after reconnecting, while the thread ran in the sidebar.
  • With the fix, the prompt is restored while disconnected; after reconnecting the client opens the thread and the composer is empty.
  • apps/web/src/composerDraftStore.test.ts covers clearing the restored prompt after an unrelated store write replaces the draft object, and keeping it when the user edited it. The first test fails without the fix.
  • Web typecheck and the fork feature ledger check pass.

Not tested: a desktop client connected to a remote host through styal Link; the browser run exercises the same client code.


Written by an agent (Claude Code, claude-opus-5-5).

When the connection dropped after the server received a new thread's
first send but before it replied, the draft reserved a fresh thread id
and restored the prompt. The client could no longer match the thread
the server created, so after reconnecting it stayed on the draft with
the sent prompt still in the composer.

The draft now reserves a fresh id only when the server rejected the
bootstrap. After a transport failure it keeps its id, follows the thread
once its turn syncs, and clears the restored prompt if it was not edited.

Written by an agent (Claude Code, claude-opus-5-5).
Upstream `pingdotgg#11372` keeps a new thread's bootstrap running after the requesting client disconnects.

Written by an agent (Claude Code, claude-opus-5-5).
@incognitojam
incognitojam merged commit 5a68cbc into main Sep 28, 2026
18 checks passed
@incognitojam
incognitojam deleted the fix/web-draft-send-lost-reply branch September 28, 2026 14:02
incognitojam added a commit that referenced this pull request Sep 29, 2026
Release notes listed every commit since the previous release, so docs,
CI, and intake bookkeeping commits such as #527 appeared next to app
changes. The desktop update popover renders the same notes, so users saw
them there too.

## Change

`render_release_notes` now lists a commit only when it changes shipped
code: a file under `apps/web`, `apps/desktop`, `apps/mobile`,
`apps/server`, `packages/`, or `patches/`, excluding tests, test
fixtures and helpers, `scripts/` folders, integration suites, and
Markdown. The remaining commits are counted on the Full Changelog line:

```
**Full Changelog**: https://github.com/incognitojam/styal/compare/…430...…431 (includes 6 docs, CI, test, and tooling changes not listed above)
```

The desktop updater already skips the Full Changelog line, so the count
does not appear as a change in the popover. Nightly and stable releases
share the renderer, so both get the filter.

The rule uses changed paths rather than the commit subject. #38 removed
an earlier subject-based filter because it could hide user-facing
changes made in `ci` or release commits; with paths, a `ci(release)`
commit that changes desktop code is still listed. Relay changes are not
listed because the relay deploys from `main` through `deploy-relay.yml`,
not with releases.

## Validation

- Rendered notes with the new function against the real tags for three
published nightlies and compared them with the published bodies:
- `v0.1.0-nightly.20260928.430`: only the web fix (#503) remains;
`docs(agents)` #505 and `chore(upstream)` #506 are counted instead.
- `v0.1.0-nightly.20260928.431`: 6 commits dropped, all `ci` and
`chore(upstream)`, including #527.
- `v0.1.0-nightly.20260926.422`: 15 dropped. Apart from docs, CI, tests,
and intake bookkeeping, these are three `infra/relay` fixes and two
upstream macOS installer artwork cherry-picks that are empty in the fork
(their intake notes say "Not applied"), so the published notes listed
changes that were not in the build.
- Ran the rendered notes for nightly 430 through
`normalizeDesktopUpdateReleaseNotes`, as Markdown and as GitHub-rendered
HTML. Both produce the single listed change and no count item.
- `vp test run scripts/release-changelog.test.ts` (36 passed): path
classification cases, a mixed fork and upstream render with the omitted
count, and a release where every commit is internal.
- Not verified: a full nightly or stable workflow run with this change.

Still listed: version bump commits (`chore(release): prepare …`,
`chore(mobile): bump app version`) and refactors, because they change
app files. Commits that change only the lockfile are not listed.

---
Written by an agent (Claude Code, claude-opus-5-5).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant