Skip to content

fix(app): edit queued prompts in place - #63251

Closed
jlongster wants to merge 4 commits into
devfrom
atomic-queued-edit
Closed

jlongster wants to merge 4 commits into
devfrom
atomic-queued-edit

Conversation

@jlongster

@jlongster jlongster commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

Issue for this PR

Closes #52275

Type of change

  • Bug fix
  • New feature (inbox edit API)
  • Refactor / code improvement
  • Documentation

What does this PR do?

Previously, the composer admitted B2 and then cancelled queued prompt B. If A finished between those requests, the model could receive both B and B2.

The composer now sends one experimental.session.inbox.edit request (PUT /api/experimental/session/:sessionID/inbox/:inboxID). The server edits the queued row under the same lock as delivery, preserving its ID, creation time, and position. The durable event records the inbox reference, replacement payload, and delivery mode. Mini uses the updated text and attachments for Undo.

If delivery wins, the server returns 409. The composer restores B2 immediately on a definitive rejection. If the response is lost, it checks the refreshed inbox and, if the row has already left, the delivered message before restoring a draft. Confirming an untouched editor preserves edits made by another client. The endpoint is experimental; the composer correction is a bug fix and has no feature flag.

How did you verify your code works?

  • Built Chromium session-queue.spec.ts, retries 0: 49 passed. The existing cases exercise a consumed row returning HTTP 409 and an applied edit whose response is lost.
  • Core prompt/projector: 98 passed. Server media: 2 passed. Schema event manifest: 7 passed. Protocol experimental identifiers: 1 passed.
  • App queue and queue-machine: 42 passed. Client Solid inbox, data, and pending status: 52 passed. TUI Mini transport and footer: 196 passed, 2 existing skips.
  • typecheck:tsc passed in core, server, app, client, schema, protocol, and TUI. Both check:generated scripts, Prettier, oxlint, and git diff --check passed.
  • The separate app typecheck:e2e command reports diagnostics outside the changed files, including composer fixtures and missing CSS/SVG declarations.

Screenshots / recordings

The regression exercises the existing composer and checks its draft after a conflicting edit.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

Added by the opencode bot (AI-generated).

Related: #55179

Public API: adds experimental PUT /api/experimental/session/:sessionID/inbox/:inboxID, the session.inbox.edited event and client.experimental.session.inbox.edit (backwards compatible)

@jlongster

jlongster commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor Author

Editing a queued prompt after delivery wins

Before the fix, the edit reported success and the model received a third request containing both B and B2. With the fix, editing the delivered prompt returns ConflictError; the model receives two requests, ending with B.

Event visualization from synthetic SDK receipts comparing the three model requests before the fix with two after it

Static PNG · Public replay

Watch the before/after video (MP4)

Revision scope: The replay and video compare legacy v2 base 4617210822 with draft head 7c21d0a8bb. Current V2 development is on dev; the core replay against 1002a979f9 is pending a resource-limited worker.

Recorded checks, traces and reproduction command

The embedded SDK probe uses a fresh SQLite database and a gated test model for each schedule. Base 4617210822: 4/5 schedules matched. The overlap case first differed at finish, /inputs/2: [A] → [A,B] → [A,B,B2]. Head 7c21d0a8bb: 5/5 matched, including [A] → [A,B] for overlap. Each bracket is one request’s user-role history. When the edit won instead, both revisions delivered B2 in place of B; head also kept B’s original ID. The separate composer regression covers draft retention.

Base trace · Head trace · Replay scripts

Extract the scripts into an empty directory. Set BASE_CHECKOUT and HEAD_CHECKOUT to writable OpenCode checkouts at the revisions above with workspace dependencies installed. From the extraction directory, run on Linux with Bun 1.4.2, Bash, Git, GNU coreutils, sed and permission to use unshare -n:

bash replay.sh base "$BASE_CHECKOUT" "$PWD/out-base"
bash replay.sh head "$HEAD_CHECKOUT" "$PWD/out-head"

Use fresh output paths. Read trace.status in each overlap.json; a completed replay can exit zero with a property mismatch.

User decisions:
- Reproduce the queued edit through a normal caller on current v2 before fixing it.
- Keep the fix minimal and verify a regression test fails before and passes after.
- Publish the topic branch as a draft PR for review.
User decisions:
- Validate the queued edit fix on v2.
- Publish the fix as a draft PR.
User decisions:
- Verify the queued edit regression and run the relevant suite.
- Keep the change on a draft topic branch.
@jlongster
jlongster changed the base branch from v2 to dev November 12, 2026 16:58
User decisions:
- Apply the review fixes to queued prompt editing
- Keep tests minimal; don't add race tests
@jlongster
jlongster marked this pull request as ready for review November 12, 2026 18:42

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The single in-place edit fixes the race between admitting B2 and cancelling B. I ran opencode serve from this branch and called the new endpoint with curl. Editing a queued row kept its ID, creation time and position. Editing a row that had left the queue returned 409, and an edit to steer was delivered straight away. Core, app queue and server media tests pass, and typecheck:tsc passes in core, server, app, client and tui. I did not run the Playwright composer spec because no browsers are available here.

Public API:

  • Adds PUT /api/experimental/session/:sessionID/inbox/:inboxID (experimental.session.inbox.edit). It takes the prompt fields plus metadata and delivery, and returns 204, 409, 400 or 404.
  • Adds the durable event session.inbox.edited { sessionID, inboxID, payload, delivery }, which is also in event.subscribe filters and the session log union.
  • Adds the client method client.experimental.session.inbox.edit and its ExperimentalSessionInboxEdit* types.
  • The generator also emits deprecated aliases client.session.inbox.edit and SessionInboxEdit* for a path that never existed.

All of this is additive and backwards compatible. Clients that switch exhaustively over event types will see the new type.

Concern: packages/client bun test has two new failures in test/promise.test.ts, caused by that generated alias (see the inline comment).

Related: #55179

readonly cancel: SessionInboxCancelOperation<E>
readonly update: SessionInboxUpdateOperation<E>

/** @deprecated Use `client.experimental.session.inbox.edit`. Removed in the next minor. */

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The codegen adds a deprecated pre-namespace alias for every experimental route. For this new endpoint, that creates a client.session.inbox.edit (and SessionInboxEdit* types) that never existed and is already marked for removal. It also breaks two tests in packages/client/test/promise.test.ts: "exposes every standard HTTP API group" gets an unexpected inbox key, and the experimental-nesting test gets an unexpected deprecated alias session.inbox.edit. Either keep new experimental endpoints out of the alias generation, or at least update the test's expected lists so bun test in packages/client passes.

@jlongster jlongster closed this Nov 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE]: v2 API to edit the prompt of an undelivered inbox item

1 participant