Skip to content

fix(channels): render the approval card on a park, answer by the real interaction id - #6653

Draft
mmabrouk wants to merge 2 commits into
channels/docs-and-decisionsfrom
channels/fix-approval-card-on-park
Draft

fix(channels): render the approval card on a park, answer by the real interaction id#6653
mmabrouk wants to merge 2 commits into
channels/docs-and-decisionsfrom
channels/fix-approval-card-on-park

Conversation

@mmabrouk

@mmabrouk mmabrouk commented Sep 8, 2026

Copy link
Copy Markdown
Member

Context

Completes approvals (F100, F101). PR #6650 built the answer path but live QA showed the card never rendered, so the loop could not be driven. Two gaps, both fixed here.

Changes

  • The card renders on a park. A parked turn never calls complete_turn, so the channels outbox was never told to fold it. The record-ingress publishes a turn_ended to streams:sessions on every terminal record. That stream is consumed only by the channels outbox, which keys its output by (turn_id, index), so a turn that also ends through complete_turn edits the same message rather than posting twice. This also covers the approval continuation, a detached run that bypasses complete_turn the same way.
  • The answer uses the right id. The outbox stored the fold's ACP token as the pending choice's interaction id, but the sessions respond path answers by the SessionInteraction row id. The outbox now resolves the row id for the turn's open interaction, matched on the token, before storing it.

Tests

  • Channels unit tier: 714 pass. Integration tier against the stack: 65 pass.
  • Live, end to end on the Agenta channel with an ask-mode Claude agent: the card renders in 7.7 s, an "approve" reply resumes the parked turn, the approved Write runs, and the agent posts "Done! I've created hello.txt...". A normal turn still answers exactly once, with no duplicate outbox row.

Note

This publishes a turn-ended on every terminal record. I considered a dedicated turn_paused event instead (cleaner vocabulary), but the outbox's existing (turn_id, index) dedup makes the single-event approach safe and far smaller. The decision and the alternative are in the takeover record.

https://claude.ai/code/session_01HXt8WBUK9MrWKNTYbUKpaT

@vercel

vercel Bot commented Sep 8, 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 8, 2026 2:54pm UTC

Request Review

@coderabbitai

coderabbitai Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
📝 Summary

Summary by CodeRabbit

  • Bug Fixes
    • Approval-card responses now correctly reference their underlying session interaction.
    • Turn completion notifications are now sent for paused turns and approval continuations.
    • Pending choices use the correct interaction identifier, improving response handling and tracking.

Walkthrough

The records worker now publishes terminal turn events for committed terminal records. The channels outbox resolves approval interaction tokens to database row IDs before storing pending choices. Production wiring and unit tests provide the interaction service.

Changes

Approval turn completion

Layer / File(s) Summary
Terminal turn event publishing
api/oss/src/tasks/asyncio/sessions/records_worker.py
The records worker identifies terminal turns in committed batches and publishes turn_ended events, including paused turns and approval continuations.
Approval interaction row resolution
api/oss/src/tasks/asyncio/channels/outbox.py, api/entrypoints/worker_streams.py, api/oss/tests/pytest/unit/channels/test_channels_outbox_worker.py
The channels outbox resolves approval tokens through SessionInteractionsService and stores the resolved interaction row ID. Worker wiring and tests provide and verify this behavior.

Priority: ➖ Normal — Schedule this approval-flow fix because parked turns and detached continuations need reliable approval cards and correct interaction IDs.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: 🟡 Moderate · up to 7e1d3

Parked approval cards can fail to render when two terminal turns for one session are committed together, leaving an approval flow without its completion notification. The event collection must preserve every session-and-turn pair before this change is merge-ready.

Sequence Diagram(s)

sequenceDiagram
  participant RecordsWorker
  participant ChannelsOutboxWorker
  participant SessionInteractionsService
  participant ChannelsService
  RecordsWorker->>ChannelsOutboxWorker: publish terminal turn event
  ChannelsOutboxWorker->>SessionInteractionsService: resolve approval token
  SessionInteractionsService-->>ChannelsOutboxWorker: return interaction row ID
  ChannelsOutboxWorker->>ChannelsService: store pending choice
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 46.15% which is insufficient. The required threshold is 60.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 13 functions across 4 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main fix: rendering approval cards for parked turns and using the real interaction ID for answers.
Description check ✅ Passed The description directly explains the parked-turn approval fixes, implementation changes, tests, and end-to-end verification.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch channels/fix-approval-card-on-park

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 force-pushed the channels/fix-approval-card-on-park branch from 9620117 to 9e94419 Compare September 8, 2026 14:11
@mmabrouk

mmabrouk commented Sep 8, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Head commit changed.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@mmabrouk

mmabrouk commented Sep 8, 2026

Copy link
Copy Markdown
Member Author

Codex (astra, medium, simplify) reviewed this lane: "request changes" on an ordering race plus two smaller points, all applied in the last commit:

  1. P1 ordering race: the turn-ended was published from the record ingress, before the record committed, so the outbox could fold stale content and, since it stops on any existing answer, acknowledge it permanently. The signal now fires from the post-commit records-worker path (terminal_turns_in_batch over the committed records), so the record is durable before the outbox reads it.
  2. The interaction lookup no longer takes rows[0] on a token mismatch (which could answer the wrong interaction when a turn holds resolved approvals beside the open one): it requires an exact token match, and falls back only when the turn has exactly one interaction.
  3. The broad except that swallowed a lookup failure into a None id is gone; the stream worker's retry handles a transient failure.

Codex also confirmed streams:sessions is consumed only by the channels outbox. Re-verified live on the Agenta channel after the move: card renders, approve resumes the parked turn, the file is created, the agent posts "Done..."; a normal turn still answers once. Channels + sessions (non-Postgres) unit tiers green.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== PR metadata =='
gh pr view 6653 --repo Agenta-AI/agenta --json number,title,baseRefName,headRefName,commits --jq '{number,title,base: .baseRefName,head: .headRefName,commits: [.commits[] | {oid,messageHeadline}]}'

echo '== Changed files against PR base =='
git diff --name-status "origin/$(gh pr view 6653 --repo Agenta-AI/agenta --json baseRefName --jq -r .baseRefName)"...HEAD

echo '== Recent commit diff summary =='
git show --stat --oneline HEAD

echo '== Candidate symbol locations =='
rg -n -C 3 'terminal_turns_in_batch|turn_ended|streams:sessions|SessionInteraction|interaction_id|interaction.*token|token.*interaction' . \
  -g '!**/node_modules/**' -g '!**/dist/**' -g '!**/build/**' | head -n 500

echo '== AST outlines for changed Python files =='
git diff --name-only "origin/$(gh pr view 6653 --repo Agenta-AI/agenta --json baseRefName --jq -r .baseRefName)"...HEAD \
  | grep -E '\.py$' \
  | while IFS= read -r f; do
      echo "--- $f"
      ast-grep outline "$f" --items all || true
    done

echo '== Full PR diff =='
git diff --find-renames --find-copies "origin/$(gh pr view 6653 --repo Agenta-AI/agenta --json baseRefName --jq -r .baseRefName)"...HEAD

Length of output: 2294


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== Available refs relevant to the PR =='
git branch -a --no-color | grep -E 'channels/(docs-and-decisions|fix-approval-card-on-park)' || true

echo '== Latest follow-up commit =='
git show --format=fuller --stat --find-renames 7e1d388f8fbee3929d03c2a4f0bea4d21ad26f13
git diff --find-renames 7e1d388f8fbee3929d03c2a4f0bea4d21ad26f13^ 7e1d388f8fbee3929d03c2a4f0bea4d21ad26f13

echo '== Files changed by both PR commits =='
git diff-tree --no-commit-id --name-only -r 16c494e43cf8f86c1f51d9f5c6b05b738d665447
git diff-tree --no-commit-id --name-only -r 7e1d388f8fbee3929d03c2a4f0bea4d21ad26f13

echo '== Python structure in latest changed files =='
git diff-tree --no-commit-id --name-only -r 7e1d388f8fbee3929d03c2a4f0bea4d21ad26f13 \
  | grep -E '\.py$' \
  | while IFS= read -r f; do
      echo "--- $f"
      ast-grep outline "$f" --items all
    done

echo '== Relevant implementation context =='
rg -n -C 12 'terminal_turns_in_batch|turn_ended|streams:sessions|SessionInteraction|interaction_id|interaction.*token|token.*interaction' \
  . -g '*.py' -g '!**/node_modules/**' -g '!**/dist/**' -g '!**/build/**' | head -n 800

Length of output: 94093


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== Records worker: helpers and process_batch =='
nl -ba api/oss/src/tasks/asyncio/sessions/records_worker.py | sed -n '1,130p;350,490p'

echo '== Records worker base version near process_batch =='
git show 7e1d388f8fbee3929d03c2a4f0bea4d21ad26f13^:api/oss/src/tasks/asyncio/sessions/records_worker.py \
  | nl -ba | sed -n '350,465p'

echo '== Channels outbox: terminal handling and interaction resolution =='
nl -ba api/oss/src/tasks/asyncio/channels/outbox.py | sed -n '40,330p;400,495p'

echo '== Channels outbox tests: outlines and targeted cases =='
ast-grep outline api/oss/tests/pytest/unit/channels/test_channels_outbox_worker.py --items all
rg -n -C 10 'turn_ended|_resolve_interaction_row_id|interaction.*token|token.*interaction|multiple|batch|park|PAUSED' \
  api/oss/tests/pytest/unit/channels/test_channels_outbox_worker.py \
  api/oss/tests/pytest/unit \
  -g '*.py' | head -n 700

echo '== Stream consumer batching contract =='
fd 'consumer.py' api/oss/src/tasks -x sh -c 'echo "--- $1"; rg -n -C 10 "class StreamConsumer|process_batch|xreadgroup|batch" "$1"' sh {} 

echo '== Record stream publisher and worker registration =='
rg -n -C 8 'publish_record\(|RecordsWorker|records_worker|streams:records|process_batch' \
  api/oss/src api/entrypoints -g '*.py' | head -n 700

Length of output: 1720


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== Records worker: helpers and process_batch =='
sed -n '1,130p;350,490p' api/oss/src/tasks/asyncio/sessions/records_worker.py

echo '== Records worker base version near process_batch =='
git show 7e1d388f8fbee3929d03c2a4f0bea4d21ad26f13^:api/oss/src/tasks/asyncio/sessions/records_worker.py \
  | sed -n '350,465p'

echo '== Channels outbox: terminal handling and interaction resolution =='
sed -n '40,330p;400,495p' api/oss/src/tasks/asyncio/channels/outbox.py

echo '== Channels outbox tests: outlines and targeted cases =='
ast-grep outline api/oss/tests/pytest/unit/channels/test_channels_outbox_worker.py --items all
rg -n -C 10 'turn_ended|_resolve_interaction_row_id|interaction.*token|token.*interaction|multiple|batch|park|PAUSED' \
  api/oss/tests/pytest/unit/channels/test_channels_outbox_worker.py \
  api/oss/tests/pytest/unit \
  -g '*.py' | head -n 700

echo '== Stream consumer batching contract =='
fd 'consumer.py' api/oss/src/tasks -x sh -c 'echo "--- $1"; rg -n -C 10 "class StreamConsumer|process_batch|xreadgroup|batch" "$1"' sh {} 

echo '== Record stream publisher and worker registration =='
rg -n -C 8 'publish_record\(|RecordsWorker|records_worker|streams:records|process_batch' \
  api/oss/src api/entrypoints -g '*.py' | head -n 700

Length of output: 145178


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== Initial PR commit diff =='
git show --find-renames --format=fuller 16c494e43cf8f86c1f51d9f5c6b05b738d665447 -- \
  api/entrypoints/worker_streams.py \
  api/oss/src/apis/fastapi/sessions/router.py \
  api/oss/src/tasks/asyncio/channels/outbox.py \
  api/oss/tests/pytest/unit/channels/test_channels_outbox_worker.py

echo '== Records worker unit-test files and terminal helper coverage =='
fd -t f -e py . api/oss/tests/pytest/unit | grep -E 'records.*worker|worker.*records|sessions.*record' | while IFS= read -r f; do
  echo "--- $f"
  ast-grep outline "$f" --items all
  rg -n -C 6 'RecordsWorker|terminal_turns_in_batch|finished_turns_in_batch|publish_turn_ended|process_batch' "$f" || true
done

echo '== Session-turn event publishers and consumers =='
rg -n -C 7 'publish_turn_(started|ended)\(|SessionTurnEvent\(|streams:sessions|worker-sessions-channels-outbox' \
  api/oss/src api/entrypoints -g '*.py' | head -n 900

echo '== Interaction lookup test implementation =='
sed -n '400,435p;590,675p' api/oss/tests/pytest/unit/channels/test_channels_outbox_worker.py

Length of output: 59441


@mmabrouk I found one remaining correctness issue.

  • P1 — Do not deduplicate terminal turns by session_id. In api/oss/src/tasks/asyncio/sessions/records_worker.py, terminal_turns_in_batch() stores results as session_id -> turn_id. StreamConsumer can accumulate up to 50 records, and RecordsWorker groups them only by project. If the same session has terminal records for two turns in one batch, Line 48 overwrites the first turn. The worker then publishes turn_ended only for the later turn. If the omitted turn is parked, the channels outbox never renders its approval card. Return distinct (session_id, turn_id) pairs, or key the mapping by both identifiers. Add a test with two terminal turns for one session in one batch.
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
api/oss/tests/pytest/unit/channels/test_channels_outbox_worker.py (1)

427-427: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test exact-token selection with multiple interactions.

The fake returns one row with the matching token. Both the exact-match path and the single-row fallback return the same ID. A regression that always selects the first interaction would pass.

Add a case with two rows where the matching token is on the second row. Assert that the stored ID is the second row ID.


ℹ️ Review info
⚙️ Run configuration

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

Review profile: CHILL

Plan: Advanced

Run ID: dfa0eeb0-0ab0-46ab-b9a2-a0efd79dadb1

📥 Commits

Reviewing files that changed from the base of the PR and between 89f4dd9 and 7e1d388.

📒 Files selected for processing (4)
  • api/entrypoints/worker_streams.py
  • api/oss/src/tasks/asyncio/channels/outbox.py
  • api/oss/src/tasks/asyncio/sessions/records_worker.py
  • api/oss/tests/pytest/unit/channels/test_channels_outbox_worker.py

Included review availability: Your plan provides up to 8 included reviews per hour; 2 remain after this review.

terminal: Dict[str, str] = {}
for record in events:
if record.record_type == TERMINAL_RECORD_TYPE and record.turn_id:
terminal[record.session_id] = str(record.turn_id)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve each terminal turn in the batch.

This mapping uses only session_id as its key. If one committed batch contains terminal records for two turns in the same session, the later record overwrites the earlier turn ID. The publish loop then omits turn_ended for the overwritten turn, so its outbox result can remain unrendered.

Store distinct (session_id, turn_id) pairs and add a regression test with two terminal turns for one session.

…real interaction id

This completes the approval loop (F100/F101), which PR #6650 left with a
working answer path but no visible card.

- A parked turn never calls complete_turn, so the channels outbox was
  never told to fold it and the card never drew. The record-ingress now
  publishes a turn-ended to streams:sessions on every terminal record.
  That stream is consumed only by the channels outbox, and the outbox
  keys its output by (turn_id, index), so a turn that also ends through
  complete_turn just edits the same message instead of posting twice. It
  also covers the approval continuation, a detached run that bypasses
  complete_turn the same way.
- The outbox stored the fold's ACP token as the pending choice's
  interaction id; the sessions respond path answers by the
  SessionInteraction row id. The outbox now resolves the row id for the
  turn's open interaction (matched on the token) before storing it.

Verified live on the Agenta channel: the card renders, an "approve"
reply resumes the parked turn, the approved tool runs, and the agent's
answer ("Done! I've created hello.txt...") posts back. A normal turn
still answers exactly once, no duplicate.

Claude-Session: https://claude.ai/code/session_01HXt8WBUK9MrWKNTYbUKpaT
- Ordering race: the turn-ended for a parked turn or a continuation was
  published from the record INGRESS, before the record committed, so the
  channels outbox could fold stale content and, because it stops on any
  existing answer, acknowledge it permanently. Move the signal to the
  post-commit records-worker path (terminal_turns_in_batch over the
  committed records), so the record is durable before the outbox reads it.
- The interaction lookup took rows[0] on a token mismatch, which could
  answer the wrong interaction when a turn holds resolved approvals beside
  the open one. Require an exact token match, and fall back only when the
  turn has exactly one interaction.
- Drop the broad except that swallowed a lookup failure into a None id
  (an unusable card); let the stream worker's retry handle a transient
  failure instead.

Re-verified live on the Agenta channel: card renders, approve resumes,
the file is created, the agent posts "Done...". A normal turn answers once.

Claude-Session: https://claude.ai/code/session_01HXt8WBUK9MrWKNTYbUKpaT
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant