Skip to content

[feat] Session control: server-held pending input, Queue then Steer - #6555

Merged
mmabrouk merged 29 commits into
feat/session-approvals-queuefrom
feat/session-queue-steer
Sep 5, 2026
Merged

[feat] Session control: server-held pending input, Queue then Steer#6555
mmabrouk merged 29 commits into
feat/session-approvals-queuefrom
feat/session-queue-steer

Conversation

@mmabrouk

@mmabrouk mmabrouk commented Sep 4, 2026

Copy link
Copy Markdown
Member

Context

Today a message sent while a turn runs is refused, and the desktop keeps a local queue in the browser that other tabs and the mobile app cannot see. Increment 7 of the session-control design (PR #6495) moves pending input to the server: Queue first, then Steer. It builds on the durable approvals of #6530 because Steer extends the same command path.

What this adds

  • Pending input on the server. An invoke that arrives with on_busy=queue while a turn runs is stored as a pending input with a stable position and an idempotency key, and the API answers 202 with the input id. A pending input can be removed until it is promoted. The session snapshot lists pending inputs in order. Migration oss000000028 adds the table, nullable and with a downgrade, after 027.
  • Promotion. When the running turn completes normally, exactly one pending input is promoted in first-in first-out order and starts through the same admission and command path as a Send. A manual Stop and a failed turn promote nothing; the queue pauses until the next Send.
  • Steer. A steer saves its input before it stops the current work, is positioned ahead of older queued inputs, and is bound to the Stop's settlement; if the Stop fails or is refused the input stays pending.
  • Clients. Desktop and mobile composers show Queue and Steer controls only when the server advertises the capability; every browser renders the server rows as ordered, non-editable, removable entries, with the "Stop pauses the queue" copy.

Both behaviours ship dark behind their own switches in env.py, default off. With the switches off a busy session still answers 409 as today.

Tests

  • Seven Postgres transaction tests: single FIFO promotion, manual Stop, concurrent idempotent retry, conflict 409, removal after promotion, save-before-failed-Stop, steer-bound promotion.
  • API sessions, runner, SDK, and web suites green on the branch; SDK sweep 2,557 passed with one unrelated pre-existing failure in the runner CLI empty-request test.
  • Not yet done: a Codex review, a rebase onto the current feat(sessions): make approval answers and continuation durable #6530 head, and the browser pass (Queue and Steer controls during a busy session, rows visible across browsers, removal before promotion, Steer running first, Stop keeping queued work, switches off).

Agent-generated, low weight. Draft until the review and the browser pass are done.

https://claude.ai/code/session_0164kzT6ttwpBtzvcDC6YzYk

@vercel

vercel Bot commented Sep 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
agenta-documentation Ready Ready Preview Sep 5, 2026 1:42pm UTC

Request Review

@coderabbitai

coderabbitai Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (1)
  • release/.*

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Team

Run ID: ca3f83b9-3550-454e-8680-3e3bc9fda465

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@mmabrouk mmabrouk left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Agent-generated Codex review, low weight.

CHANGES NEEDED

Seven inline correctness findings block this increment: successful-completion classification, admission/settlement fencing, retry idempotency, cross-tab admission, Steer binding, recoverable-input visibility, and an optional delivery receipt dereference.

The oss000000028 migration is additive; its nullable audit/promotion columns are safe, and the expanded command-kind check accepts every previously valid live value. I found no dropped release code in the removed lines. With Queue and Steer disabled, existing clients omit on_busy and both desktop and mobile retain the legacy send path. The runner's explicit error stopReason does not itself add a terminal event or change parking, but the completion watchdog must honor that new non-completing value as noted inline.

Validation: api/oss/tests/pytest/unit/sessions/test_session_inputs_dao.py passed 7 tests with xdist explicitly disabled; web/packages/agenta-chat/tests/unit/hooks/useAgentChatQueue.test.ts passed 21 tests with maxWorkers=2. Both ran serially at nice 19. The API file was rerun with -n 0 after the repository defaults unexpectedly started xdist on the first invocation.

Non-blocking simplification: SessionReadSnapshot/read is a constant latest_sequence=0, history_complete=true placeholder unused by Queue or Steer. It and its generated client type can be deferred until replay/cursor support without losing this milestone's RFC behavior.

Comment thread api/oss/src/tasks/asyncio/sessions/orphan_sweep.py
Comment thread api/oss/src/core/sessions/inputs/service.py
Comment thread api/oss/src/core/sessions/inputs/service.py
Comment thread web/packages/agenta-chat/src/hooks/useAgentChatQueue.ts Outdated
Comment thread api/oss/src/core/sessions/commands/service.py Outdated
Comment thread api/oss/src/apis/fastapi/sessions/router.py Outdated
Comment thread api/oss/src/core/sessions/commands/service.py Outdated

@mmabrouk mmabrouk left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Agent-generated Codex review, low weight.

CHANGES NEEDED

Two P1 interleavings remain. The other five findings and the SessionReadSnapshot cut are closed. The 408023c admission lock adds no cycle with normal completion or answer transactions, and no external delivery or invoke work runs while it is held. bb6b6fe preserves the Queue-off Send control flow. e9ce73d removes only unused projection definitions, generated export, schema default, and fixtures.

Focused checks passed at nice 19: test_session_inputs_dao.py, 8 tests with pytest -n 0; useAgentChatQueue.test.ts, 22 tests with maxWorkers=2.

Comment thread api/oss/src/core/sessions/inputs/service.py Outdated
Comment thread api/oss/src/core/sessions/commands/service.py

@mmabrouk mmabrouk left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Agent-generated Codex review, low weight.

CHANGES NEEDED

The Stop/Steer lock inversion and stale command read are fixed: terminal settlement and collapsed binding now both acquire the source execution before the command. The non-empty settlement regression covers a recoverable successor, but the first P1 remains when that promoted continuation has successfully started; see the inline finding.

Comment thread api/oss/src/core/sessions/inputs/service.py Outdated

@mmabrouk mmabrouk left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Agent-generated Codex review, low weight.

SHIP

Reviewed 47bbb114d3..1d7d250b3e as one unit. The round 4-6 fixes preserve durable retry identity, serialize admission with settlement, keep promoted/recoverable work visible without changing the running snapshot semantics, bind Steer to the winning Stop under a consistent execution-first lock order, and handle lost delivery reservations. The final active-successor revalidation also locks the unsettled successor while inserting, so a running continuation cannot be overtaken or settle past the new input.

No remaining correctness findings. The full API sessions suite passed (744 tests), Ruff 0.15.12 format and lint passed, the pushed SHA matches the remote, and all review threads are resolved.

@mmabrouk mmabrouk left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

SHIP

The shared capability helper now keeps the stream and snapshot responses aligned for durable approvals, queue, and queue-gated steer. The API regression covers enabled and disabled flag combinations. Desktop and mobile both use the shared snapshot-backed session-input adapter, and the new client regression verifies that a queue-capable snapshot produces durable admission with on_busy: "queue".

Validated with the API sessions suite (746 passed), chat unit suite (674 passed), OSS unit suite (431 passed, 1 skipped), both package typechecks and lints, and Ruff 0.15.12.

Reviewed-by: Codex

@mmabrouk mmabrouk left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

SHIP

Agent-generated, low weight.

Reviewed the two Round 8 commits at 7f6fd63. Stop and Escape now remain available for both locally streaming and server-owned durable runs on desktop and mobile, including a server-busy turn with one queued input. Enter and the Queue action now share awaited durable on_busy admission; a failed admission restores the draft instead of creating a browser-only queued badge.

No blocking findings. Full @agenta/chat, @agenta/oss, and @agenta/mobile unit suites, type checks, package lint, repository lint-fix, and the @agenta/chat dependency build passed.

@mmabrouk mmabrouk left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

SHIP

Round 9 review: the live delay came from the done-record settlement promoting immediately while the completed runner generation still owned Redis liveness; the continuation heartbeat was rejected, leaving the abandoned-command sweep to retry it 98 seconds later. The shared normal-completion settlement now reconciles that source generation after commit and before delivery, so both record-ingest and watchdog completion paths admit the promoted continuation immediately. The regression requires done-record-to-admission latency below one second and makes delivery contingent on source reconciliation.

The client now suppresses the stale running-elsewhere banner only when the authoritative session snapshot is idle and a durable queued item is visible; genuinely running remote work still shows the warning and the queued badge remains.

Reviewed the round-9 diff and the live thread set (10/10 resolved). Validation: API sessions 747 passed; ruff 0.15.12 format/check clean; chat 677 passed plus types/lint; OSS 434 passed, 1 skipped, plus types/lint.

@mmabrouk mmabrouk left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

SHIP

Round 10 review found the live Steer loss was confined to the client admission path. The initial fresh-run HTTP 200 kept the composer single-flight guard locked for the full SSE lifetime, so the later Steer click returned without issuing a second admission request. The Steer promise was also fire-and-forget, which prevented a real admission refusal from reaching the existing draft-restoration failure card.

The pushed fix releases the submit guard once accepted response headers arrive while draining the active stream in the background, and propagates Steer admission failures through both desktop and shared/mobile conversation paths. The API/runner chain remains intact: durable priority insert -> Stop command bound to steer_input_id -> registered execution abort -> atomic promotion of only that Steer input.

Verified locally: API sessions 747 passed; runner 2712 passed; chat 681 passed; OSS 434 passed with 1 skipped; mobile 150 passed. Chat/OSS/mobile and runner typechecks pass; requested lint and Ruff 0.15.12 checks pass. No blocking findings remain.

@mmabrouk mmabrouk left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

SHIP

Round 11 reviewer pass found no blocking findings.

The 8880 API and runner logs confirm the reported Queue losses died before durable admission: neither 08:17:47Z nor 08:22:11Z has an inputs/admit request or control event. Round 10 already fixed the shared cause for both Queue and Steer: the fresh 200 admission releases the common submit guard at response headers, and both policies then route through the same durable server adapter.

The new regression recreates the detached local stream/server-running state while the first response body remains open. Enter, the Queue button, and Steer each send the expected on_busy admission and render the pending badge; a 409 restores the draft and shows the failure card.

Local validation: chat 685 passed; OSS 434 passed and 1 skipped; mobile 150 passed; runner 2712 passed. Chat, OSS, mobile, and runner type checks passed; lints passed with the three pre-existing mobile hook warnings.

Include promoted inputs while their continuation is pending delivery or recoverable so snapshots do not hide accepted work after delivery failure.

Remove them from the queue view once the continuation reaches running.

Claude-Session: https://claude.ai/code/session_0164kzT6ttwpBtzvcDC6YzYk
Treat a missing delivery reservation receipt as a recoverable continuation failure after normal completion and Steer settlement.

Cover both paths when another worker moved the continuation command first.

Claude-Session: https://claude.ai/code/session_0164kzT6ttwpBtzvcDC6YzYk
Remove the unused constant read projection from the session snapshot contract and generated TypeScript client. Keep the durable queue snapshot focused on execution, pending work, and capabilities until replay semantics are implemented.

Claude-Session: https://claude.ai/code/session_0164kzT6ttwpBtzvcDC6YzYk
Recheck pending inputs after a source execution settles so admission preserves the successor selected by settlement. Queue the new input behind that continuation and report its execution id.

Claude-Session: https://claude.ai/code/session_0164kzT6ttwpBtzvcDC6YzYk
Serialize Steer binding and Stop settlement by locking the execution before reading or updating the command. Re-read the command inside settlement so a winning bind is promoted.

Cover both bind-first and settlement-first interleavings against Postgres.

Claude-Session: https://claude.ai/code/session_0164kzT6ttwpBtzvcDC6YzYk
Revalidate promoted successors independently of the public pending list and lock the active continuation until admission finishes. Cover delivery advancing the promoted continuation to running before admission resumes.

Claude-Session: https://claude.ai/code/session_0164kzT6ttwpBtzvcDC6YzYk
Build session capabilities through one helper so stream and snapshot responses expose the same feature flags.

Cover both API flag combinations and the shared desktop/mobile durable admission request.

Claude-Session: https://claude.ai/code/session_0164kzT6ttwpBtzvcDC6YzYk
Derive the composer Stop state from local or server-owned execution state on desktop and mobile. Keep Escape scoped to the active composer and use the mobile server command when only durable liveness remains.

Claude-Session: https://claude.ai/code/session_0164kzT6ttwpBtzvcDC6YzYk
Await Queue-capable composer submissions and propagate admission failures instead of presenting them as browser-only queued messages. Restore the desktop draft on failure; mobile reuses its existing retry surface.

Claude-Session: https://claude.ai/code/session_0164kzT6ttwpBtzvcDC6YzYk
Reconcile the completed execution's Redis generation after the durable settlement commit and before delivering its promoted continuation. This prevents the continuation heartbeat from colliding with the source turn's final heartbeat and waiting for the abandoned-command sweep.

Add a release-aware regression that measures done-record-to-admission latency below one second.

Claude-Session: https://claude.ai/code/session_0164kzT6ttwpBtzvcDC6YzYk
Expose the authoritative snapshot execution state to the desktop conversation and suppress stale running-elsewhere liveness only when that execution is idle with durable queued work.

Keep the queued badge visible, and preserve the banner for genuinely running remote executions.

Claude-Session: https://claude.ai/code/session_0164kzT6ttwpBtzvcDC6YzYk
Release accepted fresh-run admissions when their response headers arrive while continuing to consume the stream in the background. This lets later Queue and Steer actions reach durable admission during the active turn.

Propagate refused Steer admissions through desktop and mobile callers so the existing failure card restores and keeps the draft instead of clearing it silently.

Claude-Session: https://claude.ai/code/session_0164kzT6ttwpBtzvcDC6YzYk
Exercise Enter, Queue, and Steer after a fresh admission releases at response headers while the source tab is observing the server-owned run. Assert each durable request refreshes the pending badge, and a refusal restores the draft with the existing failure card.

Claude-Session: https://claude.ai/code/session_0164kzT6ttwpBtzvcDC6YzYk
The strip was reworded on the milestone 2 base: it now says the turn is still running and
warns that a still transcript may mean the run already ended. The durable admission suite
still asserted the older sentence.

Claude-Session: https://claude.ai/code/session_0164kzT6ttwpBtzvcDC6YzYk
Milestone 2 renames the liveness flag this component destructures, and its steer sender
takes only text and file parts. The rebase carried increment 7's call sites unchanged, so
the remote-run gate referenced a name that no longer exists and the steer call passed a
staged-file list the sender does not accept.

Claude-Session: https://claude.ai/code/session_0164kzT6ttwpBtzvcDC6YzYk
Milestone 2 and increment 7 each built a snapshot endpoint on GET /sessions/{session_id},
independently: increment 7 was cut from a milestone 1 head that did not carry milestone 2's.
Assembled, the API defined SessionSnapshotResponse twice and registered two handlers on one
path, so the second definition shadowed the first and only one endpoint was reachable. The
web client could export only one fetchSessionSnapshot, which orphaned the other reader.

Milestone 2 merges first, so its contract is the base. There is now one response model that
keeps milestone 2's required session and read watermark and adds increment 7's fields, one
handler that fills both halves, and one client function both readers call.

The two execution questions stay separate because they are different questions. `execution`
is the last turn, whose end_time tells the live preview whether that turn is still running.
`execution_state` is the session's current lifecycle, derived from the stream row, which is
what the durable queue admits against. Capabilities come from the same helper the streams
endpoint uses, so a client can never see the two disagree. The pending-input list is
optional in effect: a deployment without the inputs service still gets the reconnect half
and reports an empty queue.

The generated client was hand-edited rather than regenerated. The generator builds from a
live OpenAPI document; the spec this worktree can produce covers OSS only, so a full
regeneration would have deleted the cloud surface the committed client carries.

Both feature sets keep their tests: the milestone 2 snapshot, sequence and replay suites and
the live preview hook, and increment 7's queue admission and capability suites. Two new API
tests pin that one call carries both halves, and that the queue half degrades to empty when
the inputs service is absent.

Claude-Session: https://claude.ai/code/session_0164kzT6ttwpBtzvcDC6YzYk
@mmabrouk mmabrouk added the lgtm This PR has been approved by a maintainer label Sep 5, 2026

@mmabrouk mmabrouk left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Agent-generated Codex review, low weight.

CHANGES NEEDED

The 24-commit range-diff preserves the reviewed queue/steer ordering and delivery invariants, and the unified model/client supplies both reader contracts. One availability regression remains: the queue half is unreachable with the shared-reader flag off or before a fresh session has its first stream row, so desktop and mobile disable durable Queue/Steer and use the browser-local fallback. See the inline P1.

Focused validation: unified snapshot tests 4 passed; API client, entities, and chat typechecks passed.

Comment thread api/oss/src/apis/fastapi/sessions/router.py
Return queue lifecycle, pending work, and capabilities when the shared reader is disabled or a fresh session has no stream row yet.

Make reconnect-only fields nullable across the API and client contracts, and keep reconnect readers from consuming missing session data.

Add API and client regressions for flag-off, fresh-session, full-contract, and queue-enable behavior.

Claude-Session: https://claude.ai/code/session_0164kzT6ttwpBtzvcDC6YzYk

@mmabrouk mmabrouk left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Agent-generated, low weight.

SHIP

Reviewed dcf4753 as the Round 12 fix. The unified snapshot now preserves the queue/steer control plane when reconnect data is unavailable, keeps reconnect fields nullable across the API, Fern, and Zod contracts, and guards both milestone-2 reconnect reads. The shared-reader-off, fresh-session, full-contract, schema-boundary, queue-hook, and null-session reader paths are covered.

Validation: API sessions 859 passed; chat 815 passed; entities 1,553 passed; OSS 483 passed with 1 skipped; mobile 163 passed. Ruff 0.15.12, generated client build, all requested typechecks, and lint completed cleanly (mobile retained 3 pre-existing warnings).

@mmabrouk
mmabrouk marked this pull request as ready for review September 5, 2026 13:44
@mmabrouk
mmabrouk changed the base branch from feat/session-durable-approvals to feat/session-approvals-queue September 5, 2026 13:44
@mmabrouk
mmabrouk merged commit ffa7765 into feat/session-approvals-queue Sep 5, 2026
31 of 32 checks passed
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Railway Preview Environment

Preview URL https://gateway-pr-6555.up.railway.app/w
Project agenta-oss-clone-spike
Image tag pr-6555-5f74809
Status Deployed
Railway logs Open logs
Workflow logs View workflow run
Updated at 2026-09-05T13:57:09.328Z

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lgtm This PR has been approved by a maintainer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant